agent-control-plane-core 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/adapters/amp.mjs +13 -2
- package/src/adapters/claude.mjs +17 -2
- package/src/adapters/codex.mjs +17 -2
- package/src/adapters/gemini.mjs +13 -2
- package/src/conformance.mjs +39 -1
- package/src/control-plane.mjs +46 -0
- package/src/fixtures/claude.json +19 -1
- package/types/adapters/amp.d.mts +6 -0
- package/types/adapters/claude.d.mts +8 -0
- package/types/adapters/codex.d.mts +7 -0
- package/types/adapters/gemini.d.mts +6 -0
- package/types/conformance.d.mts +2 -1
- package/types/control-plane.d.mts +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-control-plane-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Vendor-neutral control-plane contract for coding agents: a normalized ToolCallEvent/Verdict schema with per-agent adapters (Claude Code, Codex, Amp, Gemini CLI) and a conformance suite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/adapters/amp.mjs
CHANGED
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
CallClass,
|
|
19
19
|
CoverageStatus,
|
|
20
20
|
classifyCallClass,
|
|
21
|
-
|
|
21
|
+
vetoableFor,
|
|
22
|
+
assertGatedKinds,
|
|
22
23
|
canonicalTool,
|
|
23
24
|
lookup,
|
|
24
25
|
makeEvent,
|
|
@@ -53,6 +54,14 @@ export const COVERAGE = Object.freeze({
|
|
|
53
54
|
[CallClass.RESUMED]: CoverageStatus.UNKNOWN,
|
|
54
55
|
});
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The event kinds this host can actually gate. Amp invokes the delegate for a tool call and nothing else, so parse only ever emits PRE_TOOL.
|
|
59
|
+
* A kind absent here parses non-vetoable, so an unmodelled event never renders as
|
|
60
|
+
* an enforced block the host will not perform.
|
|
61
|
+
*/
|
|
62
|
+
export const GATED_EVENTS = Object.freeze(new Set([EventKind.PRE_TOOL]));
|
|
63
|
+
assertGatedKinds(GATED_EVENTS, AGENT);
|
|
64
|
+
|
|
56
65
|
// Amp invokes the delegate for a tool call; the payload carries the tool name +
|
|
57
66
|
// input and the session context. Pinned by fixtures/amp.json.
|
|
58
67
|
const CONSUMED = new Set(["tool", "input", "session_id", "cwd"]);
|
|
@@ -81,7 +90,9 @@ export function parse(native) {
|
|
|
81
90
|
input: asObject(raw.input),
|
|
82
91
|
response: undefined,
|
|
83
92
|
// Classify on the NATIVE name (MCP detection keys on `mcp__…`).
|
|
84
|
-
this_call_vetoable:
|
|
93
|
+
this_call_vetoable: vetoableFor(
|
|
94
|
+
EventKind.PRE_TOOL,
|
|
95
|
+
GATED_EVENTS,
|
|
85
96
|
COVERAGE[classifyCallClass(nativeTool, raw)],
|
|
86
97
|
),
|
|
87
98
|
meta,
|
package/src/adapters/claude.mjs
CHANGED
|
@@ -19,7 +19,8 @@ import {
|
|
|
19
19
|
CallClass,
|
|
20
20
|
CoverageStatus,
|
|
21
21
|
classifyCallClass,
|
|
22
|
-
|
|
22
|
+
vetoableFor,
|
|
23
|
+
assertGatedKinds,
|
|
23
24
|
canonicalTool,
|
|
24
25
|
lookup,
|
|
25
26
|
makeEvent,
|
|
@@ -57,6 +58,18 @@ export const COVERAGE = Object.freeze({
|
|
|
57
58
|
[CallClass.RESUMED]: CoverageStatus.COVERED,
|
|
58
59
|
});
|
|
59
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The event kinds this host can actually gate. Claude Code honours a deny on these three. SESSION_START is left out: exit 2 does
|
|
63
|
+
* not abort a session start, and the matrix documents no host response for it — an
|
|
64
|
+
* undocumented kind is fail-closed to non-vetoable, the same rule coverage uses.
|
|
65
|
+
* A kind absent here parses non-vetoable, so an unmodelled event never renders as
|
|
66
|
+
* an enforced block the host will not perform.
|
|
67
|
+
*/
|
|
68
|
+
export const GATED_EVENTS = Object.freeze(
|
|
69
|
+
new Set([EventKind.PRE_TOOL, EventKind.POST_TOOL, EventKind.PROMPT_SUBMIT]),
|
|
70
|
+
);
|
|
71
|
+
assertGatedKinds(GATED_EVENTS, AGENT);
|
|
72
|
+
|
|
60
73
|
/** Claude Code native hook event names (the `hook_event_name` field). */
|
|
61
74
|
export const HookEvent = Object.freeze({
|
|
62
75
|
PRE_TOOL_USE: "PreToolUse",
|
|
@@ -162,7 +175,9 @@ export function parse(native) {
|
|
|
162
175
|
response,
|
|
163
176
|
// Classify on the NATIVE name — MCP detection keys on `mcp__…`, which a
|
|
164
177
|
// canonical builtin name would never carry.
|
|
165
|
-
this_call_vetoable:
|
|
178
|
+
this_call_vetoable: vetoableFor(
|
|
179
|
+
kind,
|
|
180
|
+
GATED_EVENTS,
|
|
166
181
|
COVERAGE[classifyCallClass(nativeTool, raw)],
|
|
167
182
|
),
|
|
168
183
|
meta,
|
package/src/adapters/codex.mjs
CHANGED
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
CallClass,
|
|
21
21
|
CoverageStatus,
|
|
22
22
|
classifyCallClass,
|
|
23
|
-
|
|
23
|
+
vetoableFor,
|
|
24
|
+
assertGatedKinds,
|
|
24
25
|
canonicalTool,
|
|
25
26
|
makeEvent,
|
|
26
27
|
normalizeVerdict,
|
|
@@ -54,6 +55,15 @@ export const COVERAGE = Object.freeze({
|
|
|
54
55
|
[CallClass.RESUMED]: CoverageStatus.UNKNOWN,
|
|
55
56
|
});
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* The event kinds this host can actually gate. `PreToolUse` is the one event Codex can veto. Every other native event parses as
|
|
60
|
+
* UNKNOWN today, so without this set each of them reported an enforced block.
|
|
61
|
+
* A kind absent here parses non-vetoable, so an unmodelled event never renders as
|
|
62
|
+
* an enforced block the host will not perform.
|
|
63
|
+
*/
|
|
64
|
+
export const GATED_EVENTS = Object.freeze(new Set([EventKind.PRE_TOOL]));
|
|
65
|
+
assertGatedKinds(GATED_EVENTS, AGENT);
|
|
66
|
+
|
|
57
67
|
/** Minimum Codex version whose hook can actually veto a tool call. */
|
|
58
68
|
export const MIN_ENFORCING_VERSION = Object.freeze([0, 135]);
|
|
59
69
|
|
|
@@ -140,7 +150,12 @@ export function parse(native) {
|
|
|
140
150
|
const nativeTool = asStringOrNull(raw.tool_name);
|
|
141
151
|
if (nativeTool !== null) meta.native_tool = nativeTool;
|
|
142
152
|
const vetoable =
|
|
143
|
-
enforce &&
|
|
153
|
+
enforce &&
|
|
154
|
+
vetoableFor(
|
|
155
|
+
kind,
|
|
156
|
+
GATED_EVENTS,
|
|
157
|
+
COVERAGE[classifyCallClass(nativeTool, raw)],
|
|
158
|
+
);
|
|
144
159
|
|
|
145
160
|
return makeEvent({
|
|
146
161
|
event: kind,
|
package/src/adapters/gemini.mjs
CHANGED
|
@@ -35,7 +35,8 @@ import {
|
|
|
35
35
|
CallClass,
|
|
36
36
|
CoverageStatus,
|
|
37
37
|
classifyCallClass,
|
|
38
|
-
|
|
38
|
+
vetoableFor,
|
|
39
|
+
assertGatedKinds,
|
|
39
40
|
canonicalTool,
|
|
40
41
|
lookup,
|
|
41
42
|
assertAliasTargetsModeled,
|
|
@@ -72,6 +73,16 @@ export const COVERAGE = Object.freeze({
|
|
|
72
73
|
[CallClass.RESUMED]: CoverageStatus.UNKNOWN,
|
|
73
74
|
});
|
|
74
75
|
|
|
76
|
+
/**
|
|
77
|
+
* The event kinds this host can actually gate. Gemini CLI honours a deny on each of the three events NATIVE_TO_KIND maps.
|
|
78
|
+
* A kind absent here parses non-vetoable, so an unmodelled event never renders as
|
|
79
|
+
* an enforced block the host will not perform.
|
|
80
|
+
*/
|
|
81
|
+
export const GATED_EVENTS = Object.freeze(
|
|
82
|
+
new Set([EventKind.PRE_TOOL, EventKind.POST_TOOL, EventKind.PROMPT_SUBMIT]),
|
|
83
|
+
);
|
|
84
|
+
assertGatedKinds(GATED_EVENTS, AGENT);
|
|
85
|
+
|
|
75
86
|
/** Gemini CLI native hook event names (the `hook_event_name` field). */
|
|
76
87
|
export const HookEvent = Object.freeze({
|
|
77
88
|
BEFORE_TOOL: "BeforeTool",
|
|
@@ -236,7 +247,7 @@ export function parse(native) {
|
|
|
236
247
|
tool: geminiCanonicalTool(nativeTool, callClass),
|
|
237
248
|
input: geminiInput(kind, raw, nativeTool, callClass),
|
|
238
249
|
response,
|
|
239
|
-
this_call_vetoable:
|
|
250
|
+
this_call_vetoable: vetoableFor(kind, GATED_EVENTS, COVERAGE[callClass]),
|
|
240
251
|
meta,
|
|
241
252
|
});
|
|
242
253
|
}
|
package/src/conformance.mjs
CHANGED
|
@@ -21,6 +21,12 @@ const UNENFORCEABLE_DENY_PROBE = Object.freeze({
|
|
|
21
21
|
});
|
|
22
22
|
const ABSTAINING_ALLOW_PROBE = Object.freeze({ decision: Decision.ALLOW });
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The native event name rule ⑨ drifts an adapter with. No host emits it, so an
|
|
26
|
+
* adapter that maps event names must answer {@link EventKind.UNKNOWN} for it.
|
|
27
|
+
*/
|
|
28
|
+
const UNMODELLED_EVENT_NAME = "ConformanceProbeUnmodelledEvent";
|
|
29
|
+
|
|
24
30
|
/**
|
|
25
31
|
* Assert an adapter's {@link import("./control-plane.mjs").Adapter.COVERAGE}
|
|
26
32
|
* hook-coverage matrix is well-formed: it classifies EXACTLY the canonical
|
|
@@ -225,7 +231,7 @@ function assertAliasedInput(agent, caseName, event, assert) {
|
|
|
225
231
|
* assert further on.
|
|
226
232
|
*
|
|
227
233
|
* @param {{ adapter: import("./control-plane.mjs").Adapter, fixtures: any, assert: any }} args
|
|
228
|
-
* @returns {{ cases: number, renders: number, decisionsSeen: Set<string>, mutationSeen: boolean, enforcedDenySeen: boolean, vetoableDenySeen: boolean, coverageClassesChecked: Set<string>, unenforceableDenyChecks: number }}
|
|
234
|
+
* @returns {{ cases: number, renders: number, decisionsSeen: Set<string>, mutationSeen: boolean, enforcedDenySeen: boolean, vetoableDenySeen: boolean, unknownKindSeen: boolean, coverageClassesChecked: Set<string>, unenforceableDenyChecks: number }}
|
|
229
235
|
*/
|
|
230
236
|
export function runAdapterConformance({ adapter, fixtures, assert }) {
|
|
231
237
|
assert.equal(
|
|
@@ -243,6 +249,7 @@ export function runAdapterConformance({ adapter, fixtures, assert }) {
|
|
|
243
249
|
let mutationSeen = false;
|
|
244
250
|
let enforcedDenySeen = false;
|
|
245
251
|
let vetoableDenySeen = false;
|
|
252
|
+
let unknownKindSeen = false;
|
|
246
253
|
let preToolCases = 0;
|
|
247
254
|
let unenforceableDenyChecks = 0;
|
|
248
255
|
let renders = 0;
|
|
@@ -260,6 +267,18 @@ export function runAdapterConformance({ adapter, fixtures, assert }) {
|
|
|
260
267
|
// a rename-without-its-input land by editing the expectation.
|
|
261
268
|
assertAliasedInput(adapter.AGENT, testCase.name, parsed, assert);
|
|
262
269
|
|
|
270
|
+
// An event the adapter could not name is one whose host response nobody has
|
|
271
|
+
// established, so claiming a veto for it is the one error that lies to a
|
|
272
|
+
// guardrail: the transcript shows a block and the host runs the tool anyway.
|
|
273
|
+
if (parsed.event === EventKind.UNKNOWN) {
|
|
274
|
+
assert.equal(
|
|
275
|
+
parsed.this_call_vetoable,
|
|
276
|
+
false,
|
|
277
|
+
`unmodelled event parsed as vetoable: ${testCase.name}`,
|
|
278
|
+
);
|
|
279
|
+
unknownKindSeen = true;
|
|
280
|
+
}
|
|
281
|
+
|
|
263
282
|
if (testCase.call_class !== undefined) {
|
|
264
283
|
assert.ok(
|
|
265
284
|
CALL_CLASSES.includes(testCase.call_class),
|
|
@@ -385,6 +404,24 @@ export function runAdapterConformance({ adapter, fixtures, assert }) {
|
|
|
385
404
|
enforcedDenySeen,
|
|
386
405
|
"no enforced deny rendered — enforcement honesty is untested",
|
|
387
406
|
);
|
|
407
|
+
// Rule ⑨, probed on a synthesized payload rather than a fixture flag: an
|
|
408
|
+
// adapter whose parse can never emit UNKNOWN — amp routes every payload to
|
|
409
|
+
// pre_tool — has no such fixture to write, so a fixture-driven check would sit
|
|
410
|
+
// vacuous on exactly the adapters this rule covers.
|
|
411
|
+
const drifted = adapter.parse({ hook_event_name: UNMODELLED_EVENT_NAME });
|
|
412
|
+
if (drifted.event === EventKind.UNKNOWN) {
|
|
413
|
+
assert.equal(
|
|
414
|
+
drifted.this_call_vetoable,
|
|
415
|
+
false,
|
|
416
|
+
"an unmodelled event parsed as vetoable",
|
|
417
|
+
);
|
|
418
|
+
assert.equal(
|
|
419
|
+
adapter.render(UNENFORCEABLE_DENY_PROBE, drifted).enforced,
|
|
420
|
+
false,
|
|
421
|
+
"a deny on an unmodelled event rendered as an enforced block",
|
|
422
|
+
);
|
|
423
|
+
unknownKindSeen = true;
|
|
424
|
+
}
|
|
388
425
|
assert.ok(renders > 0, "conformance fixtures render nothing");
|
|
389
426
|
// EVERY pre-tool case, not merely one: the count is what catches rule ⑧ being
|
|
390
427
|
// skipped by a stray `continue` rather than merely present somewhere. A suite
|
|
@@ -403,6 +440,7 @@ export function runAdapterConformance({ adapter, fixtures, assert }) {
|
|
|
403
440
|
mutationSeen,
|
|
404
441
|
enforcedDenySeen,
|
|
405
442
|
vetoableDenySeen,
|
|
443
|
+
unknownKindSeen,
|
|
406
444
|
coverageClassesChecked,
|
|
407
445
|
unenforceableDenyChecks,
|
|
408
446
|
};
|
package/src/control-plane.mjs
CHANGED
|
@@ -258,6 +258,52 @@ export function coverageAllowsVeto(status) {
|
|
|
258
258
|
return status === CoverageStatus.COVERED || status === CoverageStatus.PARTIAL;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
/**
|
|
262
|
+
* The event kinds a host can gate at all. {@link EventKind.UNKNOWN} is absent by
|
|
263
|
+
* construction, and that absence is the point: an event this package could not
|
|
264
|
+
* name is one whose host response nobody has established.
|
|
265
|
+
*/
|
|
266
|
+
/** @type {Set<string>} */
|
|
267
|
+
const GATEABLE_KINDS = new Set([
|
|
268
|
+
EventKind.PRE_TOOL,
|
|
269
|
+
EventKind.POST_TOOL,
|
|
270
|
+
EventKind.PROMPT_SUBMIT,
|
|
271
|
+
EventKind.SESSION_START,
|
|
272
|
+
]);
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Whether an event may be marked `this_call_vetoable`.
|
|
276
|
+
*
|
|
277
|
+
* INVARIANT: an event whose kind the adapter's `GATED_EVENTS` does not name is
|
|
278
|
+
* never vetoable, whatever its coverage says. Coverage answers whether the
|
|
279
|
+
* host's hook fires for a CLASS of call and says nothing about a kind the
|
|
280
|
+
* adapter could not model — so keying on coverage alone reported an enforced
|
|
281
|
+
* block for every unmodelled event, which is the one direction that lies to a
|
|
282
|
+
* guardrail: the transcript shows a block and the host runs the tool.
|
|
283
|
+
* @param {string} kind an {@link EventKind} value
|
|
284
|
+
* @param {Set<string>} gatedKinds the adapter's own gated set
|
|
285
|
+
* @param {string|undefined} coverage a {@link CoverageStatus} value
|
|
286
|
+
* @returns {boolean}
|
|
287
|
+
*/
|
|
288
|
+
export function vetoableFor(kind, gatedKinds, coverage) {
|
|
289
|
+
if (!gatedKinds.has(kind)) return false;
|
|
290
|
+
return coverageAllowsVeto(coverage);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Refuse a gated-kind set naming a kind no host can gate. Called at module load
|
|
295
|
+
* by each adapter, so `EventKind.UNKNOWN` cannot be added to one by hand.
|
|
296
|
+
* @param {Set<string>} kinds an adapter's `GATED_EVENTS`
|
|
297
|
+
* @param {string} agent the adapter's agent id, for the message
|
|
298
|
+
*/
|
|
299
|
+
export function assertGatedKinds(kinds, agent) {
|
|
300
|
+
for (const kind of kinds)
|
|
301
|
+
if (!GATEABLE_KINDS.has(kind))
|
|
302
|
+
throw new Error(
|
|
303
|
+
`${agent} adapter: ${JSON.stringify(kind)} is not a gateable event kind`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
261
307
|
/**
|
|
262
308
|
* True when `status` is a recognized {@link CoverageStatus} value.
|
|
263
309
|
* @param {unknown} status
|
package/src/fixtures/claude.json
CHANGED
|
@@ -318,7 +318,7 @@
|
|
|
318
318
|
"event": "session_start",
|
|
319
319
|
"tool": null,
|
|
320
320
|
"input": {},
|
|
321
|
-
"this_call_vetoable":
|
|
321
|
+
"this_call_vetoable": false,
|
|
322
322
|
"meta": {
|
|
323
323
|
"agent": "claude",
|
|
324
324
|
"native_event": "SessionStart",
|
|
@@ -344,6 +344,24 @@
|
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
}
|
|
347
|
+
},
|
|
348
|
+
"deny": {
|
|
349
|
+
"verdict": {
|
|
350
|
+
"decision": "deny",
|
|
351
|
+
"reason": "not a gateable event"
|
|
352
|
+
},
|
|
353
|
+
"native": {
|
|
354
|
+
"transport": "external_hook",
|
|
355
|
+
"exit_code": 0,
|
|
356
|
+
"enforced": false,
|
|
357
|
+
"stdout": {
|
|
358
|
+
"hookSpecificOutput": {
|
|
359
|
+
"hookEventName": "SessionStart"
|
|
360
|
+
},
|
|
361
|
+
"decision": "block",
|
|
362
|
+
"reason": "not a gateable event"
|
|
363
|
+
}
|
|
364
|
+
}
|
|
347
365
|
}
|
|
348
366
|
}
|
|
349
367
|
}
|
package/types/adapters/amp.d.mts
CHANGED
|
@@ -30,6 +30,12 @@ export const INTEGRATION_MODE: "external_hook";
|
|
|
30
30
|
*/
|
|
31
31
|
/** @type {import("../control-plane.mjs").CoverageMap} */
|
|
32
32
|
export const COVERAGE: import("../control-plane.mjs").CoverageMap;
|
|
33
|
+
/**
|
|
34
|
+
* The event kinds this host can actually gate. Amp invokes the delegate for a tool call and nothing else, so parse only ever emits PRE_TOOL.
|
|
35
|
+
* A kind absent here parses non-vetoable, so an unmodelled event never renders as
|
|
36
|
+
* an enforced block the host will not perform.
|
|
37
|
+
*/
|
|
38
|
+
export const GATED_EVENTS: Readonly<Set<"pre_tool">>;
|
|
33
39
|
/** @type {import("../control-plane.mjs").Adapter} */
|
|
34
40
|
export const ampAdapter: import("../control-plane.mjs").Adapter;
|
|
35
41
|
export type ToolCallEvent = import("../control-plane.mjs").ToolCallEvent;
|
|
@@ -41,6 +41,14 @@ export const INTEGRATION_MODE: "external_hook";
|
|
|
41
41
|
*/
|
|
42
42
|
/** @type {import("../control-plane.mjs").CoverageMap} */
|
|
43
43
|
export const COVERAGE: import("../control-plane.mjs").CoverageMap;
|
|
44
|
+
/**
|
|
45
|
+
* The event kinds this host can actually gate. Claude Code honours a deny on these three. SESSION_START is left out: exit 2 does
|
|
46
|
+
* not abort a session start, and the matrix documents no host response for it — an
|
|
47
|
+
* undocumented kind is fail-closed to non-vetoable, the same rule coverage uses.
|
|
48
|
+
* A kind absent here parses non-vetoable, so an unmodelled event never renders as
|
|
49
|
+
* an enforced block the host will not perform.
|
|
50
|
+
*/
|
|
51
|
+
export const GATED_EVENTS: Readonly<Set<"pre_tool" | "post_tool" | "prompt_submit">>;
|
|
44
52
|
/** Claude Code native hook event names (the `hook_event_name` field). */
|
|
45
53
|
export const HookEvent: Readonly<{
|
|
46
54
|
PRE_TOOL_USE: "PreToolUse";
|
|
@@ -59,6 +59,13 @@ export const INTEGRATION_MODE: "external_hook";
|
|
|
59
59
|
*/
|
|
60
60
|
/** @type {import("../control-plane.mjs").CoverageMap} */
|
|
61
61
|
export const COVERAGE: import("../control-plane.mjs").CoverageMap;
|
|
62
|
+
/**
|
|
63
|
+
* The event kinds this host can actually gate. `PreToolUse` is the one event Codex can veto. Every other native event parses as
|
|
64
|
+
* UNKNOWN today, so without this set each of them reported an enforced block.
|
|
65
|
+
* A kind absent here parses non-vetoable, so an unmodelled event never renders as
|
|
66
|
+
* an enforced block the host will not perform.
|
|
67
|
+
*/
|
|
68
|
+
export const GATED_EVENTS: Readonly<Set<"pre_tool">>;
|
|
62
69
|
/** Minimum Codex version whose hook can actually veto a tool call. */
|
|
63
70
|
export const MIN_ENFORCING_VERSION: readonly number[];
|
|
64
71
|
export const DEFAULT_DENY_REASON: "blocked by monitor";
|
|
@@ -43,6 +43,12 @@ export const INTEGRATION_MODE: "external_hook";
|
|
|
43
43
|
*/
|
|
44
44
|
/** @type {import("../control-plane.mjs").CoverageMap} */
|
|
45
45
|
export const COVERAGE: import("../control-plane.mjs").CoverageMap;
|
|
46
|
+
/**
|
|
47
|
+
* The event kinds this host can actually gate. Gemini CLI honours a deny on each of the three events NATIVE_TO_KIND maps.
|
|
48
|
+
* A kind absent here parses non-vetoable, so an unmodelled event never renders as
|
|
49
|
+
* an enforced block the host will not perform.
|
|
50
|
+
*/
|
|
51
|
+
export const GATED_EVENTS: Readonly<Set<"pre_tool" | "post_tool" | "prompt_submit">>;
|
|
46
52
|
/** Gemini CLI native hook event names (the `hook_event_name` field). */
|
|
47
53
|
export const HookEvent: Readonly<{
|
|
48
54
|
BEFORE_TOOL: "BeforeTool";
|
package/types/conformance.d.mts
CHANGED
|
@@ -103,7 +103,7 @@ export function assertAliasedInputsCanonical(fixturesList: any[], assert: any):
|
|
|
103
103
|
* assert further on.
|
|
104
104
|
*
|
|
105
105
|
* @param {{ adapter: import("./control-plane.mjs").Adapter, fixtures: any, assert: any }} args
|
|
106
|
-
* @returns {{ cases: number, renders: number, decisionsSeen: Set<string>, mutationSeen: boolean, enforcedDenySeen: boolean, vetoableDenySeen: boolean, coverageClassesChecked: Set<string>, unenforceableDenyChecks: number }}
|
|
106
|
+
* @returns {{ cases: number, renders: number, decisionsSeen: Set<string>, mutationSeen: boolean, enforcedDenySeen: boolean, vetoableDenySeen: boolean, unknownKindSeen: boolean, coverageClassesChecked: Set<string>, unenforceableDenyChecks: number }}
|
|
107
107
|
*/
|
|
108
108
|
export function runAdapterConformance({ adapter, fixtures, assert }: {
|
|
109
109
|
adapter: import("./control-plane.mjs").Adapter;
|
|
@@ -116,6 +116,7 @@ export function runAdapterConformance({ adapter, fixtures, assert }: {
|
|
|
116
116
|
mutationSeen: boolean;
|
|
117
117
|
enforcedDenySeen: boolean;
|
|
118
118
|
vetoableDenySeen: boolean;
|
|
119
|
+
unknownKindSeen: boolean;
|
|
119
120
|
coverageClassesChecked: Set<string>;
|
|
120
121
|
unenforceableDenyChecks: number;
|
|
121
122
|
};
|
|
@@ -46,6 +46,28 @@ export function lookup<T>(map: Record<string, T>, key: string): T | undefined;
|
|
|
46
46
|
* @returns {boolean}
|
|
47
47
|
*/
|
|
48
48
|
export function coverageAllowsVeto(status: string | undefined): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Whether an event may be marked `this_call_vetoable`.
|
|
51
|
+
*
|
|
52
|
+
* INVARIANT: an event whose kind the adapter's `GATED_EVENTS` does not name is
|
|
53
|
+
* never vetoable, whatever its coverage says. Coverage answers whether the
|
|
54
|
+
* host's hook fires for a CLASS of call and says nothing about a kind the
|
|
55
|
+
* adapter could not model — so keying on coverage alone reported an enforced
|
|
56
|
+
* block for every unmodelled event, which is the one direction that lies to a
|
|
57
|
+
* guardrail: the transcript shows a block and the host runs the tool.
|
|
58
|
+
* @param {string} kind an {@link EventKind} value
|
|
59
|
+
* @param {Set<string>} gatedKinds the adapter's own gated set
|
|
60
|
+
* @param {string|undefined} coverage a {@link CoverageStatus} value
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
export function vetoableFor(kind: string, gatedKinds: Set<string>, coverage: string | undefined): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Refuse a gated-kind set naming a kind no host can gate. Called at module load
|
|
66
|
+
* by each adapter, so `EventKind.UNKNOWN` cannot be added to one by hand.
|
|
67
|
+
* @param {Set<string>} kinds an adapter's `GATED_EVENTS`
|
|
68
|
+
* @param {string} agent the adapter's agent id, for the message
|
|
69
|
+
*/
|
|
70
|
+
export function assertGatedKinds(kinds: Set<string>, agent: string): void;
|
|
49
71
|
/**
|
|
50
72
|
* True when `status` is a recognized {@link CoverageStatus} value.
|
|
51
73
|
* @param {unknown} status
|