feat: centralize send message production gate
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user