Files
artex/db/intercept_detail.go
T
dela 0335d572de
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
First Commit
2026-10-09 08:38:16 +08:00

115 lines
4.6 KiB
Go

package db
import (
"database/sql"
"encoding/json"
"time"
)
// InterceptContextEntry is a bounded, recorded session event, not model reasoning.
type InterceptContextEntry struct {
Kind string `json:"kind"`
Tool string `json:"tool,omitempty"`
ToolUseID string `json:"tool_use_id,omitempty"`
Text string `json:"text"`
IsError bool `json:"is_error,omitempty"`
Truncated bool `json:"truncated,omitempty"`
}
// InterceptAudit is captured at review time. It is deliberately excluded from
// polling/list responses; old rows have no audit instead of reconstructed data.
type InterceptAudit struct {
RunID string `json:"run_id,omitempty"`
ToolUseID string `json:"tool_use_id,omitempty"`
Correlation string `json:"correlation"` // exact | ambiguous | unavailable
InputDigest string `json:"input_digest"`
UserMessage string `json:"user_message"`
UserTruncated bool `json:"user_truncated,omitempty"`
Context []InterceptContextEntry `json:"context"`
ContextTruncated bool `json:"context_truncated,omitempty"`
CapturedAt time.Time `json:"captured_at"`
ModelFallback bool `json:"model_fallback,omitempty"`
ModelInput json.RawMessage `json:"model_input,omitempty"`
ModelInputDigest string `json:"model_input_digest,omitempty"`
InitialAction string `json:"initial_action"`
InitialReason string `json:"initial_reason"`
EffectiveAction string `json:"effective_action,omitempty"`
DecisionReason string `json:"decision_reason,omitempty"`
RuleName string `json:"rule_name,omitempty"`
ConfigDigest string `json:"config_digest,omitempty"`
ProfileID int64 `json:"profile_id,omitempty"`
ExecutionStatus string `json:"execution_status"`
Output string `json:"output,omitempty"`
OutputTruncated bool `json:"output_truncated,omitempty"`
ExecutionEndedAt *time.Time `json:"execution_ended_at,omitempty"`
}
type InterceptDetail struct {
InterceptApprovalRow
Audit *InterceptAudit `json:"audit"`
}
func (d *DB) GetInterceptDetail(id int64) (*InterceptDetail, error) {
var out InterceptDetail
var raw []byte
err := d.QueryRow(approvalRowSelectWithAudit+` WHERE ip.id=$1`, id).Scan(
&out.ID, &out.RuleID, &out.ConversationID, &out.TaskID, &out.AgentName,
&out.ToolName, &out.ToolInput, &out.Status, &out.Reason, &out.DecidedAt, &out.CreatedAt,
&out.DecisionSource, &out.ConvTitle, &out.ConvAgentKey, &out.RuleName, &raw,
)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
if len(raw) > 0 {
if err := json.Unmarshal(raw, &out.Audit); err != nil {
return nil, err
}
}
return &out, nil
}
// ResolveIntercept atomically settles a pending request. A timeout cannot
// overwrite a human decision and repeat decisions cannot rewrite history.
func (d *DB) ResolveIntercept(id int64, status, action, reason string) (bool, error) {
execution := "not_executed"
if action == "allow" {
execution = "awaiting_result"
}
patch, err := json.Marshal(map[string]any{
"effective_action": action, "decision_reason": reason, "execution_status": execution,
})
if err != nil {
return false, err
}
r, err := d.Exec(`UPDATE intercept_pending SET status=$2, decided_at=NOW(),
audit=CASE WHEN audit IS NULL THEN NULL ELSE audit || $3::jsonb ||
CASE WHEN $4='allow' AND audit->>'correlation' IS DISTINCT FROM 'exact'
THEN '{"execution_status":"unknown"}'::jsonb ELSE '{}'::jsonb END END
WHERE id=$1 AND status='pending'`, id, status, patch, action)
if err != nil {
return false, err
}
n, err := r.RowsAffected()
return n == 1, err
}
// CompleteIntercept only updates the exact recorded call after it was allowed.
// A blocked tool_result must never be presented as a failed execution.
func (d *DB) CompleteIntercept(id int64, runID, toolUseID, status, output string, truncated bool) error {
patch, err := json.Marshal(map[string]any{
"execution_status": status, "output": output, "output_truncated": truncated,
"execution_ended_at": time.Now().UTC(),
})
if err != nil {
return err
}
_, err = d.Exec(`UPDATE intercept_pending SET audit=audit || $4::jsonb
WHERE id=$1 AND audit->>'run_id'=$2 AND audit->>'tool_use_id'=$3
AND audit->>'effective_action'='allow' AND audit->>'execution_status'='awaiting_result'`,
id, runID, toolUseID, patch)
return err
}