-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcapability.go
More file actions
151 lines (142 loc) · 6.35 KB
/
Copy pathcapability.go
File metadata and controls
151 lines (142 loc) · 6.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package agenthooks
// Capability names one thing a decision can express. The matrix below encodes
// which providers honor it on which events, so degradation is explicit and
// typed instead of silent.
type Capability string
const (
CapDeny Capability = "deny"
CapAsk Capability = "ask"
CapAllow Capability = "allow"
CapUpdateInput Capability = "update-input"
CapAddContext Capability = "add-context"
CapReplaceOutput Capability = "replace-output"
CapContinueAgent Capability = "continue-agent"
CapStopAgent Capability = "stop-agent"
CapSystemMessage Capability = "system-message"
)
// CapSet is the set of capabilities available on one provider+event pair.
type CapSet map[Capability]bool
func (s CapSet) Has(c Capability) bool { return s[c] }
func caps(cs ...Capability) CapSet {
s := make(CapSet, len(cs))
for _, c := range cs {
s[c] = true
}
return s
}
// capMatrix is the normative capability table (DESIGN.md §4.1). Fire-and-forget
// events get an empty set: pretending to block on them is worse than honesty.
var capMatrix = map[Provider]map[EventKind]CapSet{
ProviderClaudeCode: {
KindToolPre: caps(CapDeny, CapAsk, CapAllow, CapUpdateInput, CapAddContext, CapSystemMessage, CapStopAgent),
KindPermission: caps(CapDeny, CapAllow, CapUpdateInput, CapSystemMessage, CapStopAgent),
KindPromptSubmitted: caps(CapDeny, CapAddContext, CapSystemMessage, CapStopAgent),
KindToolPost: caps(CapAddContext, CapReplaceOutput, CapSystemMessage, CapStopAgent),
KindToolError: caps(CapAddContext, CapSystemMessage, CapStopAgent),
KindStop: caps(CapContinueAgent, CapSystemMessage),
KindSubagentStop: caps(CapContinueAgent, CapSystemMessage),
KindSessionStart: caps(CapAddContext, CapSystemMessage),
KindCompactPre: caps(CapSystemMessage),
},
ProviderCodex: {
KindToolPre: caps(CapDeny, CapAllow, CapUpdateInput, CapAddContext, CapSystemMessage, CapStopAgent),
KindPermission: caps(CapDeny, CapAllow, CapSystemMessage, CapStopAgent),
KindPromptSubmitted: caps(CapDeny, CapAddContext, CapSystemMessage, CapStopAgent),
KindToolPost: caps(CapAddContext, CapSystemMessage),
KindStop: caps(CapContinueAgent, CapSystemMessage),
KindSubagentStop: caps(CapContinueAgent, CapSystemMessage),
KindSessionStart: caps(CapAddContext),
},
ProviderCursor: {
// Ask is enforced only on shell/MCP events, ignored on preToolUse,
// and beforeReadFile is allow/deny only — the runner refines this
// per native event on top of the kind-level matrix.
KindToolPre: caps(CapDeny, CapAsk, CapAllow, CapUpdateInput, CapAddContext, CapSystemMessage),
KindPromptSubmitted: caps(CapDeny),
KindToolPost: caps(CapReplaceOutput), // updated_mcp_tool_output, MCP only
KindStop: caps(CapContinueAgent), // followup_message
KindSubagentStop: caps(CapContinueAgent),
},
ProviderGemini: {
KindToolPre: caps(CapDeny, CapAsk, CapAllow, CapUpdateInput, CapAddContext, CapStopAgent),
KindPromptSubmitted: caps(CapDeny, CapAddContext, CapStopAgent),
KindToolPost: caps(CapAddContext, CapReplaceOutput),
KindToolError: caps(CapAddContext),
KindStop: caps(CapContinueAgent, CapStopAgent),
KindSessionStart: caps(CapAddContext),
},
ProviderOpenCode: {
KindToolPre: caps(CapDeny, CapUpdateInput),
KindPromptSubmitted: caps(CapDeny, CapAddContext),
KindToolPost: caps(CapReplaceOutput),
KindToolError: caps(CapReplaceOutput),
KindPermission: caps(CapAllow, CapDeny), // via HTTP reply (§8)
// session.idle cannot continue the agent: empty set on KindStop.
},
ProviderOpenClaw: {
// before_tool_call returns {block, blockReason, requireApproval,
// params}; requireApproval resolves headless via timeoutBehavior
// (verified 2026.6.34), so Ask is honest. before_agent_run is the
// blockable prompt gate ({outcome: "block"}); its result carries no
// context channel. Everything else is observe-only — after_tool_call,
// agent_end, session_*, llm_* returns are ignored by the Gateway.
KindToolPre: caps(CapDeny, CapAsk, CapUpdateInput),
KindPromptSubmitted: caps(CapDeny),
},
ProviderCopilot: {
// preToolUse and permissionRequest are the only decision-capable
// events: deny was observed enforced end to end, and it fires even
// under --allow-all/--yolo. prompt.submitted is deliberately an empty
// set — Copilot DROPS command-hook output for userPromptSubmitted
// (modifiedPrompt is SDK-programmatic only), so pretending to block
// there would silently allow. Everything else is observation-only.
KindToolPre: caps(CapDeny, CapAsk, CapAllow, CapUpdateInput),
KindPermission: caps(CapDeny, CapAllow),
KindStop: caps(CapContinueAgent),
KindSubagentStop: caps(CapContinueAgent),
KindSessionStart: caps(CapAddContext),
},
ProviderKimi: {
// Only UserPromptSubmit, PreToolUse and Stop are blockable; JSON
// output understands deny|allow only — no ask, no updatedInput, no
// additionalContext (quirk #22). Prompt context rides plain exit-0
// stdout; prompt/stop blocking is exit 2 + stderr (quirk #23).
// PermissionRequest, PostToolUse and the rest are observation-only:
// empty sets, so pretend-blocking degrades honestly.
KindToolPre: caps(CapDeny, CapAllow),
KindPromptSubmitted: caps(CapDeny, CapAddContext),
KindStop: caps(CapContinueAgent),
},
}
// Capabilities reports what a decision can express for the given
// provider/variant/event combination.
func Capabilities(p Provider, v Variant, k EventKind) CapSet {
byKind, ok := capMatrix[p]
if !ok {
return CapSet{}
}
s, ok := byKind[k]
if !ok {
return CapSet{}
}
return s
}
// Can reports whether the event's provider honors the capability. Backfilled
// events are reporting-only: the provider never sent them, so nothing can be
// gated or mutated.
func (e *Event) Can(c Capability) bool {
if e.Backfilled {
return false
}
return Capabilities(e.Provider, e.Variant, e.Kind).Has(c)
}
// cursorAskSupported refines CapAsk per native Cursor event: enforced only on
// shell/MCP confirmations; ignored on preToolUse; beforeReadFile is
// allow/deny only; treated as deny on subagentStart.
func cursorAskSupported(nativeName string) bool {
switch nativeName {
case "beforeShellExecution", "beforeMCPExecution":
return true
}
return false
}