agent-control-plane-core 0.5.2 → 0.6.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/README.md +35 -6
- package/bin/codex-hook.mjs +3 -2
- package/package.json +1 -1
- package/src/adapters/amp.mjs +89 -17
- package/src/adapters/claude.mjs +75 -36
- package/src/adapters/codex.mjs +234 -61
- package/src/adapters/gemini.mjs +196 -78
- package/src/conformance.mjs +614 -1
- package/src/control-plane.mjs +173 -10
- package/src/fixtures/amp.json +8 -3
- package/src/fixtures/codex.json +277 -0
- package/src/fixtures/gemini.json +99 -2
- package/src/index.mjs +5 -1
- package/types/adapters/amp.d.mts +48 -6
- package/types/adapters/claude.d.mts +34 -3
- package/types/adapters/codex.d.mts +63 -11
- package/types/adapters/gemini.d.mts +58 -8
- package/types/conformance.d.mts +16 -1
- package/types/control-plane.d.mts +178 -75
- package/types/index.d.mts +1 -1
package/src/index.mjs
CHANGED
|
@@ -21,7 +21,11 @@ export * from "./control-plane.mjs";
|
|
|
21
21
|
export { claudeAdapter, HookEvent } from "./adapters/claude.mjs";
|
|
22
22
|
export { codexAdapter } from "./adapters/codex.mjs";
|
|
23
23
|
export { ampAdapter } from "./adapters/amp.mjs";
|
|
24
|
-
export {
|
|
24
|
+
export {
|
|
25
|
+
geminiAdapter,
|
|
26
|
+
GEMINI_TOOL_ALIASES,
|
|
27
|
+
POST_TOOL_REDACTION_UNSUPPORTED,
|
|
28
|
+
} from "./adapters/gemini.mjs";
|
|
25
29
|
export {
|
|
26
30
|
ADAPTERS,
|
|
27
31
|
AGENT_IDS,
|
package/types/adapters/amp.d.mts
CHANGED
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
export function parse(native: any): ToolCallEvent;
|
|
6
6
|
/**
|
|
7
7
|
* Render into Amp's pure exit-code transport: the decision is the exit code,
|
|
8
|
-
* with no stdout body.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* with no stdout body. An ENFORCED deny also carries its `reason` on
|
|
9
|
+
* `NativeResponse.stderr`, which `emit` writes to fd 2: the delegate is a PATH
|
|
10
|
+
* helper whose stderr Amp surfaces, and this render is what that helper writes,
|
|
11
|
+
* so a block that reached the user with no rationale was the adapter throwing
|
|
12
|
+
* the reason away. Only the enforced path — a deny this call cannot veto and an
|
|
13
|
+
* ask have blocked nothing, and `stderr` is the contract's block-reason channel.
|
|
14
|
+
* No `soleGate` option — an allow already renders as exit 0 either way, so
|
|
15
|
+
* there's no distinct "real approve" signal for this transport to opt into.
|
|
12
16
|
* @param {Verdict} verdict
|
|
13
17
|
* @param {ToolCallEvent} event
|
|
14
18
|
* @returns {NativeResponse}
|
|
@@ -16,8 +20,8 @@ export function parse(native: any): ToolCallEvent;
|
|
|
16
20
|
export function render(verdict: Verdict, event: ToolCallEvent): NativeResponse;
|
|
17
21
|
/** @typedef {import("../control-plane.mjs").ToolCallEvent} ToolCallEvent */
|
|
18
22
|
/** @typedef {import("../control-plane.mjs").Verdict} Verdict */
|
|
19
|
-
/** @typedef {import("../control-plane.mjs").EventMeta} EventMeta */
|
|
20
23
|
/** @typedef {import("../control-plane.mjs").NativeResponse} NativeResponse */
|
|
24
|
+
/** @typedef {import("../control-plane.mjs").EventMeta} EventMeta */
|
|
21
25
|
export const AGENT: "amp";
|
|
22
26
|
export const INTEGRATION_MODE: "external_hook";
|
|
23
27
|
/**
|
|
@@ -27,12 +31,50 @@ export const INTEGRATION_MODE: "external_hook";
|
|
|
27
31
|
* gates calls inside subagents — so builtin, MCP, and subagent are all COVERED.
|
|
28
32
|
* Resumed-thread firing is a strong-but-uncited structural argument, so it is
|
|
29
33
|
* held at UNKNOWN until an item-⑤ probe confirms it.
|
|
34
|
+
*
|
|
35
|
+
* That UNKNOWN is this adapter's own fail-closed reading, NOT a transcription of
|
|
36
|
+
* the matrix cell, which reads `✅ struct. [A4]` — COVERED, on evidence the
|
|
37
|
+
* `struct.` suffix marks as uncited. A cell is a CEILING: an adapter may never
|
|
38
|
+
* claim more than it, but declaring LESS is always safe, since every step down
|
|
39
|
+
* degrades a deny toward notify. This row takes that conservative option until a
|
|
40
|
+
* probe cites it; Claude's takes the cell at face value on the same class of
|
|
41
|
+
* argument. The difference is free today, for the reason below.
|
|
42
|
+
*
|
|
43
|
+
* The row is DECLARATIVE ONLY either way: no host marks a lone tool event as
|
|
44
|
+
* belonging to a resumed session, so {@link classifyCallClass} never returns
|
|
45
|
+
* RESUMED and `parse` never reads the entry — a call in a resumed thread is
|
|
46
|
+
* classified by its TOOL, BUILTIN or MCP, and both are COVERED, so it parses
|
|
47
|
+
* vetoable. (The SUBAGENT row is reachable — the classifier answers it for a
|
|
48
|
+
* payload carrying `agent_type` — but Amp's subagent coverage is COVERED too,
|
|
49
|
+
* so it changes nothing here either.) The entry is there for a consumer reading
|
|
50
|
+
* COVERAGE directly, and becomes load-bearing only once a signal exists.
|
|
30
51
|
*/
|
|
31
52
|
/** @type {import("../control-plane.mjs").CoverageMap} */
|
|
32
53
|
export const COVERAGE: import("../control-plane.mjs").CoverageMap;
|
|
54
|
+
/**
|
|
55
|
+
* The native event a conformance probe should carry for each kind — this
|
|
56
|
+
* adapter's own answer, so an every-kind probe exercises the branch that kind
|
|
57
|
+
* really takes. Amp's observer names every event `delegate` and reaches no
|
|
58
|
+
* other kind.
|
|
59
|
+
* @type {Record<string, string|undefined>}
|
|
60
|
+
*/
|
|
61
|
+
export const NATIVE_EVENT_FOR: Record<string, string | undefined>;
|
|
62
|
+
/**
|
|
63
|
+
* Which {@link VERDICT_CONTENT_FIELDS} have no native channel, so `render`
|
|
64
|
+
* drops them. Amp's transport is the exit code and nothing else — there is no
|
|
65
|
+
* stdout body to carry a replacement input, a replacement output or extra
|
|
66
|
+
* context, so ALL THREE are dropped on every kind. A guardrail that needs any of
|
|
67
|
+
* them cannot use Amp as its only integration. `reason` is not one of them and
|
|
68
|
+
* is NOT dropped: an enforced deny writes it to the helper's stderr, which Amp
|
|
69
|
+
* surfaces (see {@link render}). Built over every
|
|
70
|
+
* {@link EventKind} rather than the one `parse` emits: a row this adapter cannot
|
|
71
|
+
* reach is still the honest answer for a caller that asks.
|
|
72
|
+
* @type {Record<string, ReadonlySet<string>|undefined>}
|
|
73
|
+
*/
|
|
74
|
+
export const UNRENDERED_FIELDS: Record<string, ReadonlySet<string> | undefined>;
|
|
33
75
|
/** @type {import("../control-plane.mjs").Adapter} */
|
|
34
76
|
export const ampAdapter: import("../control-plane.mjs").Adapter;
|
|
35
77
|
export type ToolCallEvent = import("../control-plane.mjs").ToolCallEvent;
|
|
36
78
|
export type Verdict = import("../control-plane.mjs").Verdict;
|
|
37
|
-
export type EventMeta = import("../control-plane.mjs").EventMeta;
|
|
38
79
|
export type NativeResponse = import("../control-plane.mjs").NativeResponse;
|
|
80
|
+
export type EventMeta = import("../control-plane.mjs").EventMeta;
|
|
@@ -26,8 +26,8 @@ export function render(verdict: Verdict, event: ToolCallEvent, { soleGate }?: {
|
|
|
26
26
|
}): NativeResponse;
|
|
27
27
|
/** @typedef {import("../control-plane.mjs").ToolCallEvent} ToolCallEvent */
|
|
28
28
|
/** @typedef {import("../control-plane.mjs").Verdict} Verdict */
|
|
29
|
-
/** @typedef {import("../control-plane.mjs").EventMeta} EventMeta */
|
|
30
29
|
/** @typedef {import("../control-plane.mjs").NativeResponse} NativeResponse */
|
|
30
|
+
/** @typedef {import("../control-plane.mjs").EventMeta} EventMeta */
|
|
31
31
|
/** Producing-agent id stamped onto every event this adapter parses. */
|
|
32
32
|
export const AGENT: "claude";
|
|
33
33
|
/** How this adapter attaches to the agent. */
|
|
@@ -37,10 +37,34 @@ export const INTEGRATION_MODE: "external_hook";
|
|
|
37
37
|
* for builtins, for MCP tools (surfaced as `mcp__<server>__<tool>` through the
|
|
38
38
|
* same hook), and for every tool a subagent uses (subagents do not inherit the
|
|
39
39
|
* parent's permissions, so the hook is often their only gate). Resumed sessions
|
|
40
|
-
* re-read hooks from settings and fire per new call —
|
|
40
|
+
* re-read hooks from settings and fire per new call — `✅ struct. [C4]`, where
|
|
41
|
+
* the `struct.` suffix marks the status as uncited: no source addresses resumed
|
|
42
|
+
* sessions directly, and COVERED is an inference from the reload mechanism. This
|
|
43
|
+
* row takes the cell at face value. Amp holds the equivalent row at UNKNOWN,
|
|
44
|
+
* which the matrix permits — a cell is a ceiling, and declaring less coverage
|
|
45
|
+
* than it is always the safe direction. Both are free today, since
|
|
46
|
+
* `classifyCallClass` never returns RESUMED and no call is judged by this entry.
|
|
41
47
|
*/
|
|
42
48
|
/** @type {import("../control-plane.mjs").CoverageMap} */
|
|
43
49
|
export const COVERAGE: import("../control-plane.mjs").CoverageMap;
|
|
50
|
+
/**
|
|
51
|
+
* Which {@link VERDICT_CONTENT_FIELDS} have no native channel on each event
|
|
52
|
+
* kind, so `render` drops them. Claude Code splits its content channels by
|
|
53
|
+
* event: `updatedInput` exists only on PreToolUse (the call has not run yet),
|
|
54
|
+
* and `updatedToolOutput` only on PostToolUse (there is no tool output to
|
|
55
|
+
* replace anywhere else). `additionalContext` is the one channel every kind
|
|
56
|
+
* carries. The conformance harness holds this to what `render` actually emits,
|
|
57
|
+
* in both directions.
|
|
58
|
+
*
|
|
59
|
+
* `additionalContext` on PreToolUse is inherited behaviour, not a checked claim:
|
|
60
|
+
* Claude Code documents the field for UserPromptSubmit, SessionStart and
|
|
61
|
+
* PostToolUse, and this adapter has emitted it on PreToolUse since before the
|
|
62
|
+
* declaration existed. Rule ⑩ can see that the value reaches the wire, never
|
|
63
|
+
* that the host reads the key — confirm it against the hook reference before
|
|
64
|
+
* relying on it.
|
|
65
|
+
* @type {Record<string, ReadonlySet<string>|undefined>}
|
|
66
|
+
*/
|
|
67
|
+
export const UNRENDERED_FIELDS: Record<string, ReadonlySet<string> | undefined>;
|
|
44
68
|
/** Claude Code native hook event names (the `hook_event_name` field). */
|
|
45
69
|
export const HookEvent: Readonly<{
|
|
46
70
|
PRE_TOOL_USE: "PreToolUse";
|
|
@@ -48,9 +72,16 @@ export const HookEvent: Readonly<{
|
|
|
48
72
|
USER_PROMPT_SUBMIT: "UserPromptSubmit";
|
|
49
73
|
SESSION_START: "SessionStart";
|
|
50
74
|
}>;
|
|
75
|
+
/**
|
|
76
|
+
* The native event a conformance probe should carry for each kind — this
|
|
77
|
+
* adapter's own answer, so an every-kind probe exercises the branch that kind
|
|
78
|
+
* really takes. `unknown` has no native name by definition and is absent.
|
|
79
|
+
* @type {Record<string, string|undefined>}
|
|
80
|
+
*/
|
|
81
|
+
export const NATIVE_EVENT_FOR: Record<string, string | undefined>;
|
|
51
82
|
/** @type {import("../control-plane.mjs").Adapter} */
|
|
52
83
|
export const claudeAdapter: import("../control-plane.mjs").Adapter;
|
|
53
84
|
export type ToolCallEvent = import("../control-plane.mjs").ToolCallEvent;
|
|
54
85
|
export type Verdict = import("../control-plane.mjs").Verdict;
|
|
55
|
-
export type EventMeta = import("../control-plane.mjs").EventMeta;
|
|
56
86
|
export type NativeResponse = import("../control-plane.mjs").NativeResponse;
|
|
87
|
+
export type EventMeta = import("../control-plane.mjs").EventMeta;
|
|
@@ -27,15 +27,20 @@ export function canEnforce(version: unknown): boolean;
|
|
|
27
27
|
*/
|
|
28
28
|
export function parse(native: any): ToolCallEvent;
|
|
29
29
|
/**
|
|
30
|
-
* Render into Codex's native external-hook transport: a
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
30
|
+
* Render into Codex's native external-hook transport: a JSON body preserving the
|
|
31
|
+
* native event name (`PreToolUse`/`PermissionRequest`/`PostToolUse`) plus exit 2
|
|
32
|
+
* on an enforceable deny. On a pre-v0.135 Codex (`this_call_vetoable` false) the
|
|
33
|
+
* body still renders but exit stays 0 and `enforced` is false — advisory only.
|
|
34
|
+
*
|
|
35
|
+
* The two kinds use DIFFERENT native shapes, because Codex does: a pre-tool
|
|
36
|
+
* decision rides `hookSpecificOutput.permissionDecision`, a post-tool one the
|
|
37
|
+
* top-level `decision`/`reason` pair (`permissionDecision` is accepted but inert
|
|
38
|
+
* on PostToolUse, so emitting it there would read as a block that never bites).
|
|
35
39
|
*
|
|
36
40
|
* `soleGate` (default `false`) is the same dangerous, explicit opt-in as the
|
|
37
41
|
* Claude adapter: when `true` AND the verdict is `allow`, the render emits the
|
|
38
|
-
* real `permissionDecision: "allow"` instead of abstaining.
|
|
42
|
+
* real `permissionDecision: "allow"` instead of abstaining. It has no meaning
|
|
43
|
+
* post-tool — there is no approval left to grant — so it is ignored there.
|
|
39
44
|
* @param {Verdict} verdict
|
|
40
45
|
* @param {ToolCallEvent} event
|
|
41
46
|
* @param {{ soleGate?: boolean }} [options]
|
|
@@ -46,25 +51,72 @@ export function render(verdict: Verdict, event: ToolCallEvent, { soleGate }?: {
|
|
|
46
51
|
}): NativeResponse;
|
|
47
52
|
/** @typedef {import("../control-plane.mjs").ToolCallEvent} ToolCallEvent */
|
|
48
53
|
/** @typedef {import("../control-plane.mjs").Verdict} Verdict */
|
|
49
|
-
/** @typedef {import("../control-plane.mjs").EventMeta} EventMeta */
|
|
50
54
|
/** @typedef {import("../control-plane.mjs").NativeResponse} NativeResponse */
|
|
55
|
+
/** @typedef {import("../control-plane.mjs").EventMeta} EventMeta */
|
|
51
56
|
export const AGENT: "codex";
|
|
52
57
|
export const INTEGRATION_MODE: "external_hook";
|
|
53
58
|
/**
|
|
54
59
|
* Hook-coverage matrix row (`docs/hook-coverage-matrix.md`). `PreToolUse`
|
|
55
60
|
* intercepts the shell (Bash) tool ONLY — other builtins and MCP tools never
|
|
56
|
-
* reach it — so builtin is PARTIAL and MCP is UNCOVERED.
|
|
57
|
-
*
|
|
58
|
-
*
|
|
61
|
+
* reach it — so builtin is PARTIAL and MCP is UNCOVERED. A live probe
|
|
62
|
+
* ([X3]/[X4]) fired `PreToolUse` for a SUBAGENT's call and for a call in a
|
|
63
|
+
* RESUMED session, so both are PARTIAL too: covered, under the same Bash-only
|
|
64
|
+
* limit as [X1], rather than the ❓ the matrix carried before the probe ran.
|
|
65
|
+
*
|
|
66
|
+
* These rows describe PRE-TOOL routing, which is the only thing the matrix's
|
|
67
|
+
* [X1]/[X2] sources speak about. `parse` applies them to a pre-tool event only
|
|
68
|
+
* (see {@link vetoableCall}); Codex's `PostToolUse` fires for tools `PreToolUse`
|
|
69
|
+
* never sees, so a post-tool event judged by the MCP row would drop a block
|
|
70
|
+
* Codex documents it honours.
|
|
71
|
+
*
|
|
72
|
+
* The SUBAGENT row is the one the classifier now reaches on this host: the same
|
|
73
|
+
* probe found `agent_id`/`agent_type` present on a subagent's `PreToolUse`
|
|
74
|
+
* payload and ABSENT on the main thread's, so {@link classifyCallClass} answers
|
|
75
|
+
* SUBAGENT from that payload alone. It is PARTIAL, so such a call stays vetoable
|
|
76
|
+
* — the row's job here is to be honest, not to degrade. An MCP-named call takes
|
|
77
|
+
* the UNCOVERED MCP row either way, subagent or not.
|
|
78
|
+
*
|
|
79
|
+
* The RESUMED row is DECLARATIVE ONLY, for a reason no longer about coverage: a
|
|
80
|
+
* resumed session announces itself on `SessionStart` (`source: "resume"`), not on
|
|
81
|
+
* its tool payloads, and this package keeps no cross-event state — so the
|
|
82
|
+
* classifier cannot answer RESUMED from a lone pre-tool event and such a call is
|
|
83
|
+
* judged by the row its TOOL selects. Both rows are PARTIAL, so that reads the
|
|
84
|
+
* same either way.
|
|
59
85
|
*/
|
|
60
86
|
/** @type {import("../control-plane.mjs").CoverageMap} */
|
|
61
87
|
export const COVERAGE: import("../control-plane.mjs").CoverageMap;
|
|
88
|
+
/**
|
|
89
|
+
* Which {@link VERDICT_CONTENT_FIELDS} have no native channel on each kind, so
|
|
90
|
+
* `render` drops them. Codex splits its two content channels by event:
|
|
91
|
+
* `updatedInput` exists only on PreToolUse (the call has not run yet), and
|
|
92
|
+
* `hookSpecificOutput.additionalContext` only on PostToolUse — Codex documents
|
|
93
|
+
* no context field on PreToolUse, where `updatedInput` is the whole surface.
|
|
94
|
+
*
|
|
95
|
+
* NEITHER kind can rewrite tool output: PostToolUse "can't undo side effects
|
|
96
|
+
* from a tool that already ran", and Codex rejects `updatedMCPToolOutput`, so
|
|
97
|
+
* `mutated_output` is dropped everywhere. A redaction verdict therefore does NOT
|
|
98
|
+
* reach the model here: the unredacted output stands, and a guardrail that must
|
|
99
|
+
* redact has to deny (post-tool, that blocks the turn rather than un-showing the
|
|
100
|
+
* output). Inventing a native key would be worse than the visible gap, because
|
|
101
|
+
* the host ignores it and the caller reads the render as a redaction applied.
|
|
102
|
+
* @type {Record<string, ReadonlySet<string>|undefined>}
|
|
103
|
+
*/
|
|
104
|
+
export const UNRENDERED_FIELDS: Record<string, ReadonlySet<string> | undefined>;
|
|
62
105
|
/** Minimum Codex version whose hook can actually veto a tool call. */
|
|
63
106
|
export const MIN_ENFORCING_VERSION: readonly number[];
|
|
107
|
+
/**
|
|
108
|
+
* The native event a conformance probe should carry for each kind — this
|
|
109
|
+
* adapter's own answer, so an every-kind probe exercises the branch that kind
|
|
110
|
+
* really takes. `pre_tool` names `PreToolUse` rather than `PermissionRequest`
|
|
111
|
+
* because that is the branch a synthesized probe should take; `unknown` has no
|
|
112
|
+
* native name by definition and is absent.
|
|
113
|
+
* @type {Record<string, string|undefined>}
|
|
114
|
+
*/
|
|
115
|
+
export const NATIVE_EVENT_FOR: Record<string, string | undefined>;
|
|
64
116
|
export const DEFAULT_DENY_REASON: "blocked by monitor";
|
|
65
117
|
/** @type {import("../control-plane.mjs").Adapter} */
|
|
66
118
|
export const codexAdapter: import("../control-plane.mjs").Adapter;
|
|
67
119
|
export type ToolCallEvent = import("../control-plane.mjs").ToolCallEvent;
|
|
68
120
|
export type Verdict = import("../control-plane.mjs").Verdict;
|
|
69
|
-
export type EventMeta = import("../control-plane.mjs").EventMeta;
|
|
70
121
|
export type NativeResponse = import("../control-plane.mjs").NativeResponse;
|
|
122
|
+
export type EventMeta = import("../control-plane.mjs").EventMeta;
|
|
@@ -29,8 +29,8 @@ export function render(verdict: Verdict, event: ToolCallEvent, { soleGate }?: {
|
|
|
29
29
|
}): NativeResponse;
|
|
30
30
|
/** @typedef {import("../control-plane.mjs").ToolCallEvent} ToolCallEvent */
|
|
31
31
|
/** @typedef {import("../control-plane.mjs").Verdict} Verdict */
|
|
32
|
-
/** @typedef {import("../control-plane.mjs").EventMeta} EventMeta */
|
|
33
32
|
/** @typedef {import("../control-plane.mjs").NativeResponse} NativeResponse */
|
|
33
|
+
/** @typedef {import("../control-plane.mjs").EventMeta} EventMeta */
|
|
34
34
|
export const AGENT: "gemini";
|
|
35
35
|
export const INTEGRATION_MODE: "external_hook";
|
|
36
36
|
/**
|
|
@@ -38,17 +38,66 @@ export const INTEGRATION_MODE: "external_hook";
|
|
|
38
38
|
* builtins on v0.26+ (COVERED). MCP routing through the same matcher is only
|
|
39
39
|
* MEDIUM-confidence and unproven, subagent firing for a loaded agent is
|
|
40
40
|
* undocumented, and resumed-session behavior has no source — all three are
|
|
41
|
-
* UNKNOWN
|
|
42
|
-
*
|
|
41
|
+
* UNKNOWN. A guessed ✅ here would be a silent fail-open.
|
|
42
|
+
*
|
|
43
|
+
* Only the MCP row is fail-closed in practice: `parse` reads it, because
|
|
44
|
+
* {@link classifyCallClass} detects MCP from the tool name or `mcp_context`.
|
|
45
|
+
* The SUBAGENT row answers only for a payload carrying a non-empty `agent_type`
|
|
46
|
+
* ({@link classifyCallClass}); Gemini CLI documents no such field, so a
|
|
47
|
+
* subagent's call is judged today by the row its TOOL selects — BUILTIN
|
|
48
|
+
* (COVERED, i.e. vetoable), or the MCP row above when the name or `mcp_context`
|
|
49
|
+
* says so. The RESUMED row has no signal at all and is DECLARATIVE ONLY. Both
|
|
50
|
+
* record the matrix verdict for a consumer reading COVERAGE directly; the
|
|
51
|
+
* subagent one starts biting the moment a payload names an agent, which is what
|
|
52
|
+
* an item-⑤ probe of [G3] would establish.
|
|
43
53
|
*/
|
|
44
54
|
/** @type {import("../control-plane.mjs").CoverageMap} */
|
|
45
55
|
export const COVERAGE: import("../control-plane.mjs").CoverageMap;
|
|
56
|
+
/**
|
|
57
|
+
* Which {@link VERDICT_CONTENT_FIELDS} have no native channel on each event
|
|
58
|
+
* kind, so `render` drops them. Gemini CLI documents
|
|
59
|
+
* `hookSpecificOutput.tool_input` on BeforeTool, `systemMessage` on the tool
|
|
60
|
+
* events and `hookSpecificOutput.additionalContext` on BeforeAgent. It documents
|
|
61
|
+
* NO AfterTool output-rewrite field, so `mutated_output` is dropped on every
|
|
62
|
+
* kind. `mutated_input` is dropped everywhere
|
|
63
|
+
* but BeforeTool: the tool has already run by AfterTool, and BeforeAgent has no
|
|
64
|
+
* tool input at all, so emitting `tool_input` there names a channel the host
|
|
65
|
+
* ignores while reading to the caller as a mutation applied.
|
|
66
|
+
*
|
|
67
|
+
* ALLOW path only. The enforced-deny branch of `render` returns no stdout at
|
|
68
|
+
* all, so a deny drops `additional_context` too, on every kind — these rows do
|
|
69
|
+
* not describe it.
|
|
70
|
+
*
|
|
71
|
+
* AfterTool is GATED here, so a redaction verdict does reach this adapter and
|
|
72
|
+
* cannot be honoured. It renders {@link POST_TOOL_REDACTION_UNSUPPORTED} on
|
|
73
|
+
* `systemMessage` instead, so the model is told the output above it is
|
|
74
|
+
* unredacted rather than left to read it as vetted. The raw output still reaches
|
|
75
|
+
* the model; a guardrail that must actually redact has to deny.
|
|
76
|
+
* @type {Record<string, ReadonlySet<string>|undefined>}
|
|
77
|
+
*/
|
|
78
|
+
export const UNRENDERED_FIELDS: Record<string, ReadonlySet<string> | undefined>;
|
|
79
|
+
/**
|
|
80
|
+
* What the model is told when a verdict redacts an AfterTool output Gemini has
|
|
81
|
+
* no channel to replace. Exported so a caller composing its own `systemMessage`
|
|
82
|
+
* can recognize it; recognize it with `endsWith`, never equality, because a
|
|
83
|
+
* verdict carrying `additional_context` too puts that context first (see
|
|
84
|
+
* {@link decisionBody}).
|
|
85
|
+
*/
|
|
86
|
+
export const POST_TOOL_REDACTION_UNSUPPORTED: string;
|
|
46
87
|
/** Gemini CLI native hook event names (the `hook_event_name` field). */
|
|
47
88
|
export const HookEvent: Readonly<{
|
|
48
89
|
BEFORE_TOOL: "BeforeTool";
|
|
49
90
|
AFTER_TOOL: "AfterTool";
|
|
50
91
|
BEFORE_AGENT: "BeforeAgent";
|
|
51
92
|
}>;
|
|
93
|
+
/**
|
|
94
|
+
* The native event a conformance probe should carry for each kind — this
|
|
95
|
+
* adapter's own answer, so an every-kind probe exercises the branch that kind
|
|
96
|
+
* really takes. `session_start` and `unknown` have no Gemini event and are
|
|
97
|
+
* absent.
|
|
98
|
+
* @type {Record<string, string|undefined>}
|
|
99
|
+
*/
|
|
100
|
+
export const NATIVE_EVENT_FOR: Record<string, string | undefined>;
|
|
52
101
|
/**
|
|
53
102
|
* Adapter-scoped native-builtin → canonical tool aliases, applied ONLY when a
|
|
54
103
|
* call classifies as BUILTIN. These names are too generic for the global
|
|
@@ -58,10 +107,11 @@ export const HookEvent: Readonly<{
|
|
|
58
107
|
* `mcp_{server}_{tool}` name (gemini-cli docs/tools/mcp-server.md), so a bare
|
|
59
108
|
* builtin name in `tool_name` can only be the builtin. Renaming the tool also
|
|
60
109
|
* renames its INPUT to the schema that name advertises, via
|
|
61
|
-
* {@link GEMINI_INPUT_ALIASES}
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
110
|
+
* {@link GEMINI_INPUT_ALIASES}, and the rename must actually produce that key or
|
|
111
|
+
* the call keeps its native name (see {@link geminiCall}) — a consumer told
|
|
112
|
+
* `event.tool` is `Read` reads `input.file_path` and finds it, rather than
|
|
113
|
+
* reading `undefined` off a forwarded `absolute_path` and allowing. Targets are
|
|
114
|
+
* pinned to {@link MODELED_TOOLS} at import, every entry must be witnessed by a gemini
|
|
65
115
|
* conformance fixture (`assertToolAliasesCovered`), and every aliased case must
|
|
66
116
|
* carry the canonical input key (`assertAliasedInputsCanonical`).
|
|
67
117
|
* @type {Readonly<Record<string, string>>}
|
|
@@ -71,5 +121,5 @@ export const GEMINI_TOOL_ALIASES: Readonly<Record<string, string>>;
|
|
|
71
121
|
export const geminiAdapter: import("../control-plane.mjs").Adapter;
|
|
72
122
|
export type ToolCallEvent = import("../control-plane.mjs").ToolCallEvent;
|
|
73
123
|
export type Verdict = import("../control-plane.mjs").Verdict;
|
|
74
|
-
export type EventMeta = import("../control-plane.mjs").EventMeta;
|
|
75
124
|
export type NativeResponse = import("../control-plane.mjs").NativeResponse;
|
|
125
|
+
export type EventMeta = import("../control-plane.mjs").EventMeta;
|
package/types/conformance.d.mts
CHANGED
|
@@ -97,13 +97,27 @@ export function assertAliasedInputsCanonical(fixturesList: any[], assert: any):
|
|
|
97
97
|
* throwing away the objection entirely. The strongest honest signal a
|
|
98
98
|
* transport has left — an ask, an advisory body — must survive. An
|
|
99
99
|
* OBSERVE_ONLY render is exempt: it has no pre-emption channel to differ in.
|
|
100
|
+
* 9. drift honesty: `parse` must ANSWER an event name no host emits with
|
|
101
|
+
* EventKind.UNKNOWN rather than throw, and a deny rendered on that answer
|
|
102
|
+
* must not claim a block. Probed on a synthesized payload, since an adapter
|
|
103
|
+
* whose parse can never emit UNKNOWN has no such fixture to write.
|
|
104
|
+
* 10. content-channel honesty: for every parsed event, each Verdict content
|
|
105
|
+
* field (mutated_input, mutated_output, additional_context) either reaches
|
|
106
|
+
* the host's wire or is DECLARED unreachable in the adapter's
|
|
107
|
+
* UNRENDERED_FIELDS — and the render must agree with the declaration in
|
|
108
|
+
* both directions. Without it an adapter with no channel for a field just
|
|
109
|
+
* ignored it, so a redaction verdict rendered a bare allow and the
|
|
110
|
+
* unredacted output reached the model with nothing saying so. The fields
|
|
111
|
+
* that DID reach a wire come back as `contentFieldsSeen`; a suite covering
|
|
112
|
+
* several adapters asserts their union to keep the positive half of the
|
|
113
|
+
* rule non-vacuous, since no single host has a channel for all three.
|
|
100
114
|
*
|
|
101
115
|
* `assert` is injected (node:assert/strict) so the harness stays test-framework
|
|
102
116
|
* neutral; it throws on the first mismatch. Returns a summary the caller can
|
|
103
117
|
* assert further on.
|
|
104
118
|
*
|
|
105
119
|
* @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, unknownKindSeen: boolean, coverageClassesChecked: Set<string>, unenforceableDenyChecks: number }}
|
|
120
|
+
* @returns {{ cases: number, renders: number, decisionsSeen: Set<string>, mutationSeen: boolean, contentFieldsSeen: Set<string>, enforcedDenySeen: boolean, vetoableDenySeen: boolean, unknownKindSeen: boolean, coverageClassesChecked: Set<string>, unenforceableDenyChecks: number }}
|
|
107
121
|
*/
|
|
108
122
|
export function runAdapterConformance({ adapter, fixtures, assert }: {
|
|
109
123
|
adapter: import("./control-plane.mjs").Adapter;
|
|
@@ -114,6 +128,7 @@ export function runAdapterConformance({ adapter, fixtures, assert }: {
|
|
|
114
128
|
renders: number;
|
|
115
129
|
decisionsSeen: Set<string>;
|
|
116
130
|
mutationSeen: boolean;
|
|
131
|
+
contentFieldsSeen: Set<string>;
|
|
117
132
|
enforcedDenySeen: boolean;
|
|
118
133
|
vetoableDenySeen: boolean;
|
|
119
134
|
unknownKindSeen: boolean;
|