feat: add fake send connector contract

This commit is contained in:
zhaoyilun
2026-07-10 20:22:02 +08:00
parent 9eb7f382cc
commit dadf42817c
6 changed files with 343 additions and 18 deletions

View File

@@ -51,12 +51,16 @@ type SendMessageAuditEvent struct {
TargetRef string `json:"target_ref"`
ExecutionMode string `json:"execution_mode"`
Connector string `json:"connector"`
ConnectorMode string `json:"connector_mode"`
ConnectorStage string `json:"connector_stage"`
ContentSHA256 string `json:"content_sha256"`
ContentLength int `json:"content_length"`
IdempotencyKeySHA256 string `json:"idempotency_key_sha256"`
SendStatus string `json:"send_status"`
Result string `json:"result"`
AckRef string `json:"ack_ref,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
AuditRef string `json:"audit_ref"`
StartedAt string `json:"started_at"`
FinishedAt string `json:"finished_at"`
@@ -105,6 +109,16 @@ func RegisterISphereSendMessageToolWithState(server *mcp.Server, auditSink SendM
if idempotencyStore == nil {
idempotencyStore = defaultSendMessageIdempotencyStore()
}
RegisterISphereSendMessageToolWithStateAndConnector(server, auditSink, idempotencyStore, nil)
}
func RegisterISphereSendMessageToolWithStateAndConnector(server *mcp.Server, auditSink SendMessageAuditSink, idempotencyStore SendMessageIdempotencyStore, connector SendMessageConnector) {
if auditSink == nil {
auditSink = defaultSendMessageAuditSink()
}
if idempotencyStore == nil {
idempotencyStore = defaultSendMessageIdempotencyStore()
}
mcp.AddTool[SendMessageArgs, map[string]any](server, &mcp.Tool{
Name: ToolNameSendMessage,
@@ -116,12 +130,16 @@ func RegisterISphereSendMessageToolWithState(server *mcp.Server, auditSink SendM
return nil, nil, err
}
finished := time.Now().UTC()
response, event := sendMessagePreviewResponse(normalized, started, finished)
response, event := sendMessagePreviewResponse(normalized, started, finished, connector != nil)
decision, err := idempotencyStore.ReserveSendMessageIdempotency(ctx, sendMessageIdempotencyRecordFromAuditEvent(event))
if err != nil {
return nil, nil, fmt.Errorf("%s idempotency reserve failed: %w", ToolNameSendMessage, err)
}
applySendMessageIdempotencyDecision(response, &event, decision)
if normalized.ExecutionMode == sendMessageProductionMode && connector != nil && !decision.Duplicate && !decision.Conflict {
result, connectorErr := connector.ExecuteSendMessage(ctx, sendMessageConnectorRequestFromNormalized(normalized))
applySendMessageConnectorResult(response, &event, result, connectorErr)
}
if err := auditSink.AppendSendMessageAudit(ctx, event); err != nil {
return nil, nil, fmt.Errorf("%s audit append failed: %w", ToolNameSendMessage, err)
}
@@ -325,17 +343,21 @@ func normalizeSendMessageExecutionMode(value string) (string, error) {
}
}
func sendMessagePreviewResponse(input normalizedSendMessageArgs, started time.Time, finished time.Time) (map[string]any, SendMessageAuditEvent) {
func sendMessagePreviewResponse(input normalizedSendMessageArgs, started time.Time, finished time.Time, connectorAvailable bool) (map[string]any, SendMessageAuditEvent) {
sideEffects := sendMessageNoSideEffects()
status := "planned"
stage := sendMessagePreviewStage
ok := true
blockedReason := any(nil)
if input.ExecutionMode == sendMessageProductionMode {
ok = false
status = "blocked"
stage = sendMessageBlockedStage
blockedReason = "production send is blocked until dynamic observation, idempotency audit, and one approved sandbox send gate pass"
if connectorAvailable {
stage = sendMessagePreviewStage
} else {
ok = false
status = "blocked"
stage = sendMessageBlockedStage
blockedReason = "production send is blocked until dynamic observation, idempotency audit, and one approved sandbox send gate pass"
}
}
auditRef := sendMessageAuditRef(input.ContentSHA256, input.IdempotencyKeySHA256)
event := SendMessageAuditEvent{
@@ -345,6 +367,7 @@ func sendMessagePreviewResponse(input normalizedSendMessageArgs, started time.Ti
TargetRef: input.TargetRef,
ExecutionMode: input.ExecutionMode,
Connector: sendMessageConnector,
ConnectorMode: sendMessageConnectorMode,
ConnectorStage: stage,
ContentSHA256: input.ContentSHA256,
ContentLength: input.ContentLength,
@@ -400,6 +423,54 @@ func sendMessagePreviewResponse(input normalizedSendMessageArgs, started time.Ti
return response, event
}
func applySendMessageConnectorResult(response map[string]any, event *SendMessageAuditEvent, result SendMessageConnectorResult, err error) {
normalized := normalizeSendMessageConnectorResult(result, err)
response["ok"] = normalized.Accepted
response["send_status"] = normalized.Status
response["connector_mode"] = normalized.ConnectorMode
response["connector_stage"] = normalized.Status
if normalized.Accepted {
response["blocked_reason"] = nil
} else if normalized.ErrorMessage != "" {
response["blocked_reason"] = normalized.ErrorMessage
}
if normalized.AckRef != "" {
response["ack_ref"] = normalized.AckRef
}
if normalized.ErrorCode != "" {
response["error_code"] = normalized.ErrorCode
}
if normalized.ErrorMessage != "" {
response["error_message"] = normalized.ErrorMessage
}
audit, _ := response["audit"].(map[string]any)
if audit != nil {
audit["result"] = normalized.Status
audit["connector_mode"] = normalized.ConnectorMode
audit["connector_stage"] = normalized.Status
if normalized.AckRef != "" {
audit["ack_ref"] = normalized.AckRef
}
if normalized.ErrorCode != "" {
audit["error_code"] = normalized.ErrorCode
}
if normalized.ErrorMessage != "" {
audit["error_message"] = normalized.ErrorMessage
}
}
if event != nil {
event.ConnectorMode = normalized.ConnectorMode
event.ConnectorStage = normalized.Status
event.SendStatus = normalized.Status
event.Result = normalized.Status
event.AckRef = normalized.AckRef
event.ErrorCode = normalized.ErrorCode
event.ErrorMessage = normalized.ErrorMessage
}
}
func sendMessageIdempotencyRecordFromAuditEvent(event SendMessageAuditEvent) SendMessageIdempotencyRecord {
return SendMessageIdempotencyRecord{
Tool: event.Tool,