feat: centralize send message production gate

This commit is contained in:
zhaoyilun
2026-07-10 20:35:46 +08:00
parent 2a7087218b
commit b08b0e01bd
7 changed files with 187 additions and 22 deletions

View File

@@ -61,6 +61,8 @@ type SendMessageAuditEvent struct {
AckRef string `json:"ack_ref,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
GateReasonCode string `json:"gate_reason_code,omitempty"`
GateReasonMessage string `json:"gate_reason_message,omitempty"`
AuditRef string `json:"audit_ref"`
StartedAt string `json:"started_at"`
FinishedAt string `json:"finished_at"`
@@ -130,7 +132,8 @@ func RegisterISphereSendMessageToolWithStateAndConnector(server *mcp.Server, aud
return nil, nil, err
}
finished := time.Now().UTC()
response, event := sendMessagePreviewResponse(normalized, started, finished, connector != nil)
gatePolicy := currentSendMessageGatePolicy(connector != nil)
response, event := sendMessagePreviewResponse(normalized, started, finished, connector != nil, gatePolicy)
decision, err := idempotencyStore.ReserveSendMessageIdempotency(ctx, sendMessageIdempotencyRecordFromAuditEvent(event))
if err != nil {
return nil, nil, fmt.Errorf("%s idempotency reserve failed: %w", ToolNameSendMessage, err)
@@ -161,6 +164,12 @@ func defaultSendMessageIdempotencyStore() SendMessageIdempotencyStore {
return fileSendMessageIdempotencyStore{path: filepath.Join("runs", "send-audit", "send-message-idempotency.jsonl")}
}
func currentSendMessageGatePolicy(connectorConfigured bool) SendMessageGatePolicy {
return LoadSendMessageGatePolicy(map[string]string{
EnvSendMessageProductionEnabled: os.Getenv(EnvSendMessageProductionEnabled),
}, false, connectorConfigured)
}
func (s fileSendMessageAuditSink) AppendSendMessageAudit(_ context.Context, event SendMessageAuditEvent) error {
if strings.TrimSpace(s.path) == "" {
return nil
@@ -343,12 +352,14 @@ func normalizeSendMessageExecutionMode(value string) (string, error) {
}
}
func sendMessagePreviewResponse(input normalizedSendMessageArgs, started time.Time, finished time.Time, connectorAvailable bool) (map[string]any, SendMessageAuditEvent) {
func sendMessagePreviewResponse(input normalizedSendMessageArgs, started time.Time, finished time.Time, connectorAvailable bool, gatePolicy SendMessageGatePolicy) (map[string]any, SendMessageAuditEvent) {
sideEffects := sendMessageNoSideEffects()
status := "planned"
stage := sendMessagePreviewStage
ok := true
blockedReason := any(nil)
gateReasonCode := ""
gateReasonMessage := ""
if input.ExecutionMode == sendMessageProductionMode {
if connectorAvailable {
stage = sendMessagePreviewStage
@@ -356,7 +367,9 @@ func sendMessagePreviewResponse(input normalizedSendMessageArgs, started time.Ti
ok = false
status = "blocked"
stage = sendMessageBlockedStage
blockedReason = "production send is blocked until dynamic observation, idempotency audit, and one approved sandbox send gate pass"
gateReasonCode = gatePolicy.ReasonCode
gateReasonMessage = gatePolicy.ReasonMessage
blockedReason = gateReasonMessage
}
}
auditRef := sendMessageAuditRef(input.ContentSHA256, input.IdempotencyKeySHA256)
@@ -374,6 +387,8 @@ func sendMessagePreviewResponse(input normalizedSendMessageArgs, started time.Ti
IdempotencyKeySHA256: input.IdempotencyKeySHA256,
SendStatus: status,
Result: status,
GateReasonCode: gateReasonCode,
GateReasonMessage: gateReasonMessage,
AuditRef: auditRef,
StartedAt: started.Format(time.RFC3339Nano),
FinishedAt: finished.Format(time.RFC3339Nano),
@@ -387,6 +402,8 @@ func sendMessagePreviewResponse(input normalizedSendMessageArgs, started time.Ti
"connector": sendMessageConnector,
"connector_mode": sendMessageConnectorMode,
"connector_stage": stage,
"gate_reason_code": gateReasonCode,
"gate_reason_message": gateReasonMessage,
"production_send_enabled": false,
"target": map[string]any{
"target_type": input.TargetType,
@@ -406,18 +423,20 @@ func sendMessagePreviewResponse(input normalizedSendMessageArgs, started time.Ti
},
"side_effects": sideEffects,
"audit": map[string]any{
"tool": ToolNameSendMessage,
"source": sendMessageConnector,
"execution_mode": input.ExecutionMode,
"connector": sendMessageConnector,
"connector_mode": sendMessageConnectorMode,
"connector_stage": stage,
"content_sha256": input.ContentSHA256,
"target_ref": input.TargetRef,
"audit_ref": auditRef,
"started_at": started.Format(time.RFC3339Nano),
"finished_at": finished.Format(time.RFC3339Nano),
"result": status,
"tool": ToolNameSendMessage,
"source": sendMessageConnector,
"execution_mode": input.ExecutionMode,
"connector": sendMessageConnector,
"connector_mode": sendMessageConnectorMode,
"connector_stage": stage,
"gate_reason_code": gateReasonCode,
"gate_reason_message": gateReasonMessage,
"content_sha256": input.ContentSHA256,
"target_ref": input.TargetRef,
"audit_ref": auditRef,
"started_at": started.Format(time.RFC3339Nano),
"finished_at": finished.Format(time.RFC3339Nano),
"result": status,
},
}
return response, event