feat: add a-route uia send connector

This commit is contained in:
zhaoyilun
2026-07-11 11:16:53 +08:00
parent 456ef44e8e
commit 8aa952a491
15 changed files with 941 additions and 19 deletions

View File

@@ -61,7 +61,7 @@ func NewServerFromEnv() (*mcp.Server, error) {
if err != nil {
return nil, err
}
return NewServerWithSourcesAndMsgLibReceive(source, displaySource, msglibReceiveSource), nil
return NewServerWithSourcesMsgLibReceiveAndSendConnector(source, displaySource, msglibReceiveSource, tools.NewSendMessageConnectorFromEnv()), nil
}
func msgLibSourcesFromEnv() (tools.DisplayEntitySource, tools.ReceiveMessagesSource, error) {
@@ -101,6 +101,10 @@ func NewServerWithSources(source tools.ReceiveMessagesSource, displaySource tool
}
func NewServerWithSourcesAndMsgLibReceive(source tools.ReceiveMessagesSource, displaySource tools.DisplayEntitySource, msglibReceiveSource tools.ReceiveMessagesSource) *mcp.Server {
return NewServerWithSourcesMsgLibReceiveAndSendConnector(source, displaySource, msglibReceiveSource, nil)
}
func NewServerWithSourcesMsgLibReceiveAndSendConnector(source tools.ReceiveMessagesSource, displaySource tools.DisplayEntitySource, msglibReceiveSource tools.ReceiveMessagesSource, sendConnector tools.SendMessageConnector) *mcp.Server {
server := mcp.NewServer(&mcp.Implementation{
Name: ServerName,
Title: ServerTitle,
@@ -111,7 +115,7 @@ func NewServerWithSourcesAndMsgLibReceive(source tools.ReceiveMessagesSource, di
tools.RegisterISphereContactToolsWithDisplayEntities(server, source, displaySource)
tools.RegisterISphereGroupToolsWithDisplayEntities(server, source, displaySource)
tools.RegisterISphereFileTools(server, source)
tools.RegisterISphereSendMessageTool(server, nil)
tools.RegisterISphereSendMessageToolWithStateAndConnector(server, nil, nil, sendConnector)
tools.RegisterISphereSendFileTool(server)
return server
}

View File

@@ -4,7 +4,9 @@ import (
"context"
"crypto/cipher"
"crypto/des"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"os"
"reflect"
@@ -283,6 +285,56 @@ func TestNewServerFromEnvUsesPacketLogDirectory(t *testing.T) {
}
}
func TestNewServerFromEnvWiresUiaRpaSendConnector(t *testing.T) {
clearMsgLibEnvForServerTest(t)
t.Setenv("ISPHERE_SEND_CONNECTOR_MODE", "uia_rpa")
t.Setenv("ISPHERE_SEND_UIA_HELPER_PATH", t.TempDir()+"\\missing-helper.exe")
t.Setenv("ISPHERE_SEND_UIA_HWND", "0x1234")
t.Setenv("ISPHERE_SEND_UIA_EDITOR_AUTOMATION_ID", "rtbSendMessage")
t.Setenv("ISPHERE_SEND_UIA_BUTTON_AUTOMATION_ID", "btnSend")
t.Setenv("ISPHERE_SEND_AUDIT_PATH", t.TempDir()+"\\send-audit.jsonl")
t.Setenv("ISPHERE_SEND_IDEMPOTENCY_PATH", t.TempDir()+"\\send-idempotency.jsonl")
server, err := NewServerFromEnv()
if err != nil {
t.Fatalf("NewServerFromEnv returned error: %v", err)
}
session, cleanup := connectServerTestSession(t, server)
defer cleanup()
content := "uia rpa send through env"
callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
Name: "isphere_send_message",
Arguments: map[string]any{
"target_type": "direct",
"target_id": "alice@imopenfire1-lanzhou",
"content_text": content,
"content_sha256": sha256HexForServerTest(content),
"idempotency_key": "idem-uia-env-1",
"execution_mode": "production",
},
})
if err != nil {
t.Fatalf("call isphere_send_message: %v", err)
}
if callResult.IsError {
t.Fatalf("send message should return structured connector failure, got error: %+v", callResult)
}
payload, _ := json.Marshal(callResult.StructuredContent)
var decoded struct {
OK bool `json:"ok"`
SendStatus string `json:"send_status"`
ConnectorMode string `json:"connector_mode"`
ErrorCode string `json:"error_code"`
}
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("decode payload %s: %v", payload, err)
}
if decoded.OK || decoded.SendStatus != "failed" || decoded.ConnectorMode != "uia-rpa" || decoded.ErrorCode != "uia_rpa_helper_error" {
t.Fatalf("production send did not route to UIA connector: %s", payload)
}
}
type fakeMsgLibClientForServerTest struct {
displayCalls []msglib.DisplayEntitiesOptions
listCalls []msglib.ListMessagesOptions
@@ -376,3 +428,8 @@ func padPacketLogLineForServerTest(data []byte, blockSize int) []byte {
}
return out
}
func sha256HexForServerTest(value string) string {
sum := sha256.Sum256([]byte(value))
return hex.EncodeToString(sum[:])
}

View File

@@ -273,6 +273,7 @@ type normalizedSendMessageArgs struct {
TargetType string
TargetID string
TargetRef string
ContentText string
ContentLength int
ContentSHA256 string
IdempotencyKeySHA256 string
@@ -308,6 +309,7 @@ func normalizeSendMessageArgs(input SendMessageArgs) (normalizedSendMessageArgs,
TargetType: targetType,
TargetID: targetID,
TargetRef: targetRefPrefix + ":" + targetID,
ContentText: contentText,
ContentLength: len(contentText),
ContentSHA256: contentHash,
IdempotencyKeySHA256: sha256Hex(idempotencyKey),
@@ -448,6 +450,12 @@ func applySendMessageConnectorResult(response map[string]any, event *SendMessage
response["send_status"] = normalized.Status
response["connector_mode"] = normalized.ConnectorMode
response["connector_stage"] = normalized.Status
if normalized.ProductionEnabled {
response["production_send_enabled"] = true
}
if len(normalized.SideEffects) > 0 {
response["side_effects"] = normalized.SideEffects
}
if normalized.Accepted {
response["blocked_reason"] = nil
} else if normalized.ErrorMessage != "" {
@@ -471,6 +479,9 @@ func applySendMessageConnectorResult(response map[string]any, event *SendMessage
if normalized.AckRef != "" {
audit["ack_ref"] = normalized.AckRef
}
if normalized.ProductionEnabled {
audit["production_send_enabled"] = true
}
if normalized.ErrorCode != "" {
audit["error_code"] = normalized.ErrorCode
}
@@ -487,6 +498,9 @@ func applySendMessageConnectorResult(response map[string]any, event *SendMessage
event.AckRef = normalized.AckRef
event.ErrorCode = normalized.ErrorCode
event.ErrorMessage = normalized.ErrorMessage
if len(normalized.SideEffects) > 0 {
event.SideEffects = normalized.SideEffects
}
}
}

View File

@@ -385,6 +385,52 @@ func TestISphereSendMessageFakeConnectorFailureAudit(t *testing.T) {
}
}
func TestISphereSendMessageConnectorReceivesContentTextWithoutLeakingIt(t *testing.T) {
audit := &fakeSendMessageAuditSink{}
idempotency := newFakeSendMessageIdempotencyStore()
connector := &fakeSendMessageConnector{
result: SendMessageConnectorResult{
Accepted: true,
Status: "accepted",
AckRef: "fake-content-ack",
ConnectorMode: "fake",
},
}
session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) {
RegisterISphereSendMessageToolWithStateAndConnector(server, audit, idempotency, connector)
})
defer cleanup()
content := "connector needs the raw text to send"
callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
Name: ToolNameSendMessage,
Arguments: map[string]any{
"target_type": "direct",
"target_id": "alice@imopenfire1-lanzhou",
"content_text": content,
"content_sha256": sha256HexForSendMessageTest(content),
"idempotency_key": "idem-connector-content-1",
"execution_mode": "production",
},
})
if err != nil {
t.Fatalf("call %s: %v", ToolNameSendMessage, err)
}
payload, _ := json.Marshal(callResult.StructuredContent)
if connector.lastRequest.ContentText != content {
t.Fatalf("connector ContentText = %q, want raw content", connector.lastRequest.ContentText)
}
if strings.Contains(string(payload), content) {
t.Fatalf("structured response leaked raw content: %s", payload)
}
if len(audit.events) != 1 {
t.Fatalf("audit events = %+v, want one", audit.events)
}
if audit.events[0].ContentText != "" || strings.Contains(mustMarshalStringForSendMessageTest(audit.events[0]), content) {
t.Fatalf("audit leaked raw content: %+v", audit.events[0])
}
}
func TestISphereSendMessageRejectsContentHashMismatch(t *testing.T) {
session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) {
RegisterISphereSendMessageToolWithState(server, &fakeSendMessageAuditSink{}, newFakeSendMessageIdempotencyStore())
@@ -650,13 +696,15 @@ func (f *fakeSendMessageIdempotencyStore) ReserveSendMessageIdempotency(_ contex
}
type fakeSendMessageConnector struct {
result SendMessageConnectorResult
err error
calls int
result SendMessageConnectorResult
err error
calls int
lastRequest SendMessageConnectorRequest
}
func (f *fakeSendMessageConnector) ExecuteSendMessage(_ context.Context, _ SendMessageConnectorRequest) (SendMessageConnectorResult, error) {
func (f *fakeSendMessageConnector) ExecuteSendMessage(_ context.Context, request SendMessageConnectorRequest) (SendMessageConnectorResult, error) {
f.calls++
f.lastRequest = request
return f.result, f.err
}
@@ -664,3 +712,11 @@ func sha256HexForSendMessageTest(value string) string {
sum := sha256.Sum256([]byte(value))
return hex.EncodeToString(sum[:])
}
func mustMarshalStringForSendMessageTest(value any) string {
payload, err := json.Marshal(value)
if err != nil {
panic(err)
}
return string(payload)
}

View File

@@ -13,6 +13,7 @@ type SendMessageConnectorRequest struct {
TargetType string
TargetID string
TargetRef string
ContentText string
ContentSHA256 string
ContentLength int
IdempotencyKeySHA256 string
@@ -20,12 +21,14 @@ type SendMessageConnectorRequest struct {
}
type SendMessageConnectorResult struct {
Accepted bool
Status string
AckRef string
ErrorCode string
ErrorMessage string
ConnectorMode string
Accepted bool
Status string
AckRef string
ErrorCode string
ErrorMessage string
ConnectorMode string
ProductionEnabled bool
SideEffects map[string]any
}
func sendMessageConnectorRequestFromNormalized(input normalizedSendMessageArgs) SendMessageConnectorRequest {
@@ -33,6 +36,7 @@ func sendMessageConnectorRequestFromNormalized(input normalizedSendMessageArgs)
TargetType: input.TargetType,
TargetID: input.TargetID,
TargetRef: input.TargetRef,
ContentText: input.ContentText,
ContentSHA256: input.ContentSHA256,
ContentLength: input.ContentLength,
IdempotencyKeySHA256: input.IdempotencyKeySHA256,

View File

@@ -0,0 +1,239 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"
"isphere-ai-bridge/internal/helperclient"
)
const (
uiaSendMessageOp = "uia_send_message"
EnvSendConnectorMode = "ISPHERE_SEND_CONNECTOR_MODE"
EnvSendUIAHelperPath = "ISPHERE_SEND_UIA_HELPER_PATH"
EnvSendUIAHwnd = "ISPHERE_SEND_UIA_HWND"
EnvSendUIAEditorAutomationID = "ISPHERE_SEND_UIA_EDITOR_AUTOMATION_ID"
EnvSendUIButtonAutomationID = "ISPHERE_SEND_UIA_BUTTON_AUTOMATION_ID"
EnvSendUIATimeoutSeconds = "ISPHERE_SEND_UIA_TIMEOUT_SECONDS"
)
type UiaSendMessageAdapterConfig struct {
HelperPath string
TimeoutSeconds int
Hwnd string
SendEditorAutomationID string
SendButtonAutomationID string
Mode string
}
type UiaHelperCaller interface {
CallUiaHelper(ctx context.Context, op string, args map[string]any) (uiaHelperResponse, error)
}
type uiaHelperResponse struct {
OK bool
Data map[string]any
ErrCode string
ErrText string
}
type helperClientUiaCaller struct {
client helperclient.Client
}
type uiaSendMessageConnector struct {
config UiaSendMessageAdapterConfig
caller UiaHelperCaller
}
func NewSendMessageConnectorFromEnv() SendMessageConnector {
mode := strings.TrimSpace(strings.ToLower(os.Getenv(EnvSendConnectorMode)))
if mode != "uia_rpa" && mode != "uia-rpa" {
return nil
}
timeoutSeconds := 10
if raw := strings.TrimSpace(os.Getenv(EnvSendUIATimeoutSeconds)); raw != "" {
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 {
timeoutSeconds = parsed
}
}
return NewUiaSendMessageConnector(UiaSendMessageAdapterConfig{
Mode: "enabled",
HelperPath: strings.TrimSpace(os.Getenv(EnvSendUIAHelperPath)),
TimeoutSeconds: timeoutSeconds,
Hwnd: strings.TrimSpace(os.Getenv(EnvSendUIAHwnd)),
SendEditorAutomationID: strings.TrimSpace(os.Getenv(EnvSendUIAEditorAutomationID)),
SendButtonAutomationID: strings.TrimSpace(os.Getenv(EnvSendUIButtonAutomationID)),
}, nil)
}
func NewUiaSendMessageConnector(config UiaSendMessageAdapterConfig, caller UiaHelperCaller) SendMessageConnector {
config.Mode = strings.TrimSpace(strings.ToLower(config.Mode))
if config.Mode == "" {
config.Mode = "disabled"
}
if config.SendEditorAutomationID == "" {
config.SendEditorAutomationID = "rtbSendMessage"
}
if config.SendButtonAutomationID == "" {
config.SendButtonAutomationID = "btnSend"
}
if caller == nil {
timeout := 10 * time.Second
if config.TimeoutSeconds > 0 {
timeout = time.Duration(config.TimeoutSeconds) * time.Second
}
caller = helperClientUiaCaller{client: helperclient.Client{HelperPath: config.HelperPath, Timeout: timeout}}
}
return uiaSendMessageConnector{config: config, caller: caller}
}
func (c uiaSendMessageConnector) ExecuteSendMessage(ctx context.Context, request SendMessageConnectorRequest) (SendMessageConnectorResult, error) {
if c.config.Mode != "enabled" && c.config.Mode != "uia_rpa" {
result := SendMessageConnectorResult{
Accepted: false,
Status: "blocked",
ErrorCode: "uia_rpa_mode_blocked",
ErrorMessage: "A-route UIA/RPA send adapter is disabled",
ConnectorMode: "uia-rpa-disabled",
}
return result, fmt.Errorf(result.ErrorMessage)
}
if err := validateUiaSendMessageConfig(c.config); err != nil {
result := SendMessageConnectorResult{
Accepted: false,
Status: "blocked",
ErrorCode: "uia_rpa_config_missing",
ErrorMessage: err.Error(),
ConnectorMode: "uia-rpa",
}
return result, err
}
if err := validateUiaSendMessageRequest(request); err != nil {
return SendMessageConnectorResult{
Accepted: false,
Status: "failed",
ErrorCode: "uia_rpa_invalid_request",
ErrorMessage: err.Error(),
ConnectorMode: "uia-rpa",
}, nil
}
args := map[string]any{
"hwnd": c.config.Hwnd,
"send_editor_automation_id": c.config.SendEditorAutomationID,
"send_button_automation_id": c.config.SendButtonAutomationID,
"target_ref": request.TargetRef,
"content_text": request.ContentText,
"content_sha256": request.ContentSHA256,
"idempotency_key_sha256": request.IdempotencyKeySHA256,
}
response, err := c.caller.CallUiaHelper(ctx, uiaSendMessageOp, args)
if err != nil {
return SendMessageConnectorResult{
Accepted: false,
Status: "failed",
ErrorCode: "uia_rpa_helper_error",
ErrorMessage: err.Error(),
ConnectorMode: "uia-rpa",
}, err
}
if !response.OK {
code := strings.TrimSpace(response.ErrCode)
if code == "" {
code = "uia_rpa_helper_rejected"
}
message := strings.TrimSpace(response.ErrText)
if message == "" {
message = "UIA helper rejected send action"
}
return SendMessageConnectorResult{
Accepted: false,
Status: "failed",
ErrorCode: code,
ErrorMessage: message,
ConnectorMode: "uia-rpa",
}, nil
}
ackRef, _ := response.Data["ack_ref"].(string)
if strings.TrimSpace(ackRef) == "" {
ackRef = "uia:" + c.config.Hwnd + ":" + c.config.SendButtonAutomationID
}
return SendMessageConnectorResult{
Accepted: true,
Status: "accepted",
AckRef: ackRef,
ConnectorMode: "uia-rpa",
ProductionEnabled: true,
SideEffects: map[string]any{
"sent_message": true,
"typed_text": true,
"clicked_ui": true,
"uploaded_file": false,
"sent_file": false,
"captured_network": false,
"attached_hook": false,
"modified_client_data": false,
},
}, nil
}
func validateUiaSendMessageConfig(config UiaSendMessageAdapterConfig) error {
if strings.TrimSpace(config.Hwnd) == "" {
return fmt.Errorf("ISPHERE_SEND_UIA_HWND is required for A-route UIA send")
}
if strings.TrimSpace(config.SendEditorAutomationID) == "" {
return fmt.Errorf("send editor automation id is required")
}
if strings.TrimSpace(config.SendButtonAutomationID) == "" {
return fmt.Errorf("send button automation id is required")
}
return nil
}
func validateUiaSendMessageRequest(request SendMessageConnectorRequest) error {
if strings.TrimSpace(request.TargetRef) == "" {
return fmt.Errorf("target_ref is required")
}
if strings.TrimSpace(request.ContentText) == "" {
return fmt.Errorf("content_text is required")
}
if strings.TrimSpace(request.ContentSHA256) == "" {
return fmt.Errorf("content_sha256 is required")
}
if strings.TrimSpace(request.IdempotencyKeySHA256) == "" {
return fmt.Errorf("idempotency_key_sha256 is required")
}
if request.ExecutionMode != sendMessageProductionMode {
return fmt.Errorf("execution_mode must be production")
}
return nil
}
func (c helperClientUiaCaller) CallUiaHelper(ctx context.Context, op string, args map[string]any) (uiaHelperResponse, error) {
response, err := c.client.Call(ctx, op, args)
if err != nil {
return uiaHelperResponse{}, err
}
out := uiaHelperResponse{OK: response != nil && response.OK}
if response != nil {
out.Data = decodeRawData(response.Data)
if response.Error != nil {
out.ErrCode = response.Error.Code
out.ErrText = response.Error.Message
}
}
if out.Data == nil {
out.Data = map[string]any{}
}
return out, nil
}
func uiaHelperResponseFromRaw(data map[string]json.RawMessage, ok bool) uiaHelperResponse {
return uiaHelperResponse{OK: ok, Data: decodeRawData(data)}
}

View File

@@ -0,0 +1,117 @@
package tools
import (
"context"
"testing"
)
func TestUiaSendMessageConnectorRequiresEnabledModeAndWindowConfig(t *testing.T) {
connector := NewUiaSendMessageConnector(UiaSendMessageAdapterConfig{Mode: "disabled"}, &fakeUiaHelperCaller{})
result, err := connector.ExecuteSendMessage(context.Background(), SendMessageConnectorRequest{
TargetRef: "contact:alice@imopenfire1-lanzhou",
ContentText: "hello",
ContentSHA256: sha256HexForSendMessageTest("hello"),
IdempotencyKeySHA256: sha256HexForSendMessageTest("idem"),
ExecutionMode: sendMessageProductionMode,
})
if err == nil {
t.Fatalf("expected disabled connector error")
}
if result.ErrorCode != "uia_rpa_mode_blocked" || result.ConnectorMode != "uia-rpa-disabled" {
t.Fatalf("unexpected result: %+v", result)
}
connector = NewUiaSendMessageConnector(UiaSendMessageAdapterConfig{Mode: "enabled"}, &fakeUiaHelperCaller{})
result, err = connector.ExecuteSendMessage(context.Background(), SendMessageConnectorRequest{
TargetRef: "contact:alice@imopenfire1-lanzhou",
ContentText: "hello",
ContentSHA256: sha256HexForSendMessageTest("hello"),
IdempotencyKeySHA256: sha256HexForSendMessageTest("idem"),
ExecutionMode: sendMessageProductionMode,
})
if err == nil {
t.Fatalf("expected missing config error")
}
if result.ErrorCode != "uia_rpa_config_missing" {
t.Fatalf("unexpected missing config result: %+v", result)
}
}
func TestUiaSendMessageConnectorCallsHelperAndMapsAck(t *testing.T) {
caller := &fakeUiaHelperCaller{response: uiaHelperResponse{
OK: true,
Data: map[string]any{
"action_mode": "uia_send_message",
"target_ref": "contact:alice@imopenfire1-lanzhou",
"content_sha256": sha256HexForSendMessageTest("hello uia"),
"sent_message": true,
"typed_text": true,
"clicked_ui": true,
"ack_ref": "uia:0x1234:btnSend",
},
}}
connector := NewUiaSendMessageConnector(UiaSendMessageAdapterConfig{
Mode: "enabled",
Hwnd: "0x1234",
SendEditorAutomationID: "rtbSendMessage",
SendButtonAutomationID: "btnSend",
}, caller)
result, err := connector.ExecuteSendMessage(context.Background(), SendMessageConnectorRequest{
TargetRef: "contact:alice@imopenfire1-lanzhou",
ContentText: "hello uia",
ContentSHA256: sha256HexForSendMessageTest("hello uia"),
IdempotencyKeySHA256: sha256HexForSendMessageTest("idem-uia"),
ExecutionMode: sendMessageProductionMode,
})
if err != nil {
t.Fatalf("ExecuteSendMessage: %v", err)
}
if !result.Accepted || result.Status != "accepted" || result.AckRef != "uia:0x1234:btnSend" || result.ConnectorMode != "uia-rpa" || !result.ProductionEnabled {
t.Fatalf("unexpected result: %+v", result)
}
if caller.op != "uia_send_message" {
t.Fatalf("helper op = %q", caller.op)
}
if caller.args["hwnd"] != "0x1234" || caller.args["send_editor_automation_id"] != "rtbSendMessage" || caller.args["send_button_automation_id"] != "btnSend" {
t.Fatalf("unexpected helper args: %#v", caller.args)
}
if caller.args["content_text"] != "hello uia" || caller.args["content_sha256"] != sha256HexForSendMessageTest("hello uia") {
t.Fatalf("helper did not receive send content/hash: %#v", caller.args)
}
}
func TestSendMessageConnectorFromEnvBuildsUiaRPAConnector(t *testing.T) {
t.Setenv(EnvSendConnectorMode, "uia_rpa")
t.Setenv(EnvSendUIAHwnd, "0x1234")
t.Setenv(EnvSendUIAEditorAutomationID, "editorA")
t.Setenv(EnvSendUIButtonAutomationID, "buttonA")
t.Setenv(EnvSendUIAHelperPath, t.TempDir()+"\\missing-helper.exe")
connector := NewSendMessageConnectorFromEnv()
if connector == nil {
t.Fatalf("NewSendMessageConnectorFromEnv returned nil for uia_rpa config")
}
result, err := connector.ExecuteSendMessage(context.Background(), SendMessageConnectorRequest{
TargetRef: "contact:alice@imopenfire1-lanzhou",
ContentText: "hello",
ContentSHA256: sha256HexForSendMessageTest("hello"),
IdempotencyKeySHA256: sha256HexForSendMessageTest("idem"),
ExecutionMode: sendMessageProductionMode,
})
if err == nil || result.ErrorCode != "uia_rpa_helper_error" {
t.Fatalf("expected configured connector to reach helper layer and fail on missing helper, got result=%+v err=%v", result, err)
}
}
type fakeUiaHelperCaller struct {
response uiaHelperResponse
err error
op string
args map[string]any
}
func (f *fakeUiaHelperCaller) CallUiaHelper(ctx context.Context, op string, args map[string]any) (uiaHelperResponse, error) {
f.op = op
f.args = args
return f.response, f.err
}