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/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 { geminiAdapter, GEMINI_TOOL_ALIASES } from "./adapters/gemini.mjs";
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,
@@ -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. `reason` has no native channel here, so it is dropped
9
- * (Amp surfaces the helper's own stderr). No `soleGate` option — an allow
10
- * already renders as exit 0 either way, so there's no distinct "real approve"
11
- * signal for this transport to opt into.
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 — structural, uncontested.
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 `hookSpecificOutput`
31
- * body preserving the native event name (`PreToolUse`/`PermissionRequest`) plus
32
- * exit 2 on an enforceable deny. On a pre-v0.135 Codex (`this_call_vetoable`
33
- * false) the body still renders but exit stays 0 and `enforced` is false —
34
- * advisory only.
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. Subagent and resumed
57
- * firing are undocumented ⇒ UNKNOWN (treated as uncovered until an item-⑤ probe
58
- * proves otherwise).
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, held at fail-closed until an item-⑤ probe upgrades them. A guessed ✅
42
- * here would be a silent fail-open.
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} — a consumer told `event.tool` is `Read` reads
62
- * `input.file_path` and finds it, rather than reading `undefined` off a
63
- * forwarded `absolute_path` and allowing. Targets are pinned to
64
- * {@link MODELED_TOOLS} at import, every entry must be witnessed by a gemini
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;
@@ -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;