agent-control-plane-core 0.8.2 → 0.9.1

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.
@@ -77,7 +77,8 @@ export function assertAliasedInputsCanonical(fixturesList: any[], assert: any):
77
77
  * decision the contract requires every adapter to express.
78
78
  * 4. enforcement honesty: an enforceable deny (`rendered.enforced === true`)
79
79
  * MUST carry a real block signal — a non-zero `exit_code` — not just a JSON
80
- * body the agent is free to ignore. At least one enforced deny must appear,
80
+ * body the agent is free to ignore — plus a non-empty reason on the
81
+ * reason stream its {@link BLOCK_TRANSPORT} row names. At least one enforced deny must appear,
81
82
  * so the honesty check is never vacuous.
82
83
  * 5. vetoability honesty, BOTH directions. When the parsed event's
83
84
  * `this_call_vetoable` is false, EVERY render for that case must be
@@ -254,6 +254,40 @@ export function nativeResponse({ transport, exit_code, enforced, stdout, stderr,
254
254
  stdout?: unknown;
255
255
  stderr?: string;
256
256
  }): NativeResponse;
257
+ /**
258
+ * The {@link BLOCK_TRANSPORT} row for `agent`. Throws for an agent with no row:
259
+ * a missing row would otherwise render an enforced block with no exit code.
260
+ * @param {string} agent
261
+ * @returns {Readonly<{ exit_code: number, reason_stream: "stderr", reason_required: boolean }>}
262
+ */
263
+ export function blockTransportFor(agent: string): Readonly<{
264
+ exit_code: number;
265
+ reason_stream: "stderr";
266
+ reason_required: boolean;
267
+ }>;
268
+ /**
269
+ * `reason` when it holds visible text, else {@link DEFAULT_DENY_REASON}.
270
+ * @param {unknown} reason
271
+ * @returns {string}
272
+ */
273
+ export function blockReason(reason: unknown): string;
274
+ /**
275
+ * Build an adapter's {@link NativeResponse} from its {@link BLOCK_TRANSPORT}
276
+ * row. When `enforced`, the exit code is the row's and the reason (or the
277
+ * default) goes on the row's reason stream. Otherwise `unblocked_exit_code`
278
+ * stands and no reason stream is written: nothing was blocked, so the host
279
+ * reads the exit-0 (or ask) body instead.
280
+ * @param {string} agent
281
+ * @param {{ transport: string, enforced: boolean, unblocked_exit_code: number, stdout?: unknown, reason?: string }} parts
282
+ * @returns {NativeResponse}
283
+ */
284
+ export function gatedResponse(agent: string, { transport, enforced, unblocked_exit_code, stdout, reason }: {
285
+ transport: string;
286
+ enforced: boolean;
287
+ unblocked_exit_code: number;
288
+ stdout?: unknown;
289
+ reason?: string;
290
+ }): NativeResponse;
257
291
  /**
258
292
  * The vendor-neutral control-plane contract.
259
293
  *
@@ -440,7 +474,8 @@ export const CoverageStatus: Readonly<{
440
474
  /**
441
475
  * The result of rendering a {@link Verdict} for a specific agent's transport —
442
476
  * the ATTACH mechanism, not the decision. A caller applies whichever channels
443
- * are present: write `stdout`, exit with `exit_code`, or `throw`.
477
+ * are present: write `stdout`, write `stderr`, then exit with `exit_code`
478
+ * (`emit` in `agent-control-plane-core/runtime` does all three).
444
479
  * @typedef {object} NativeResponse
445
480
  * @property {"external_hook"|"in_process"|"observe_only"} transport
446
481
  * @property {number} exit_code process exit code carrying the decision (0 = proceed)
@@ -485,6 +520,22 @@ export const VERDICT_CONTENT_FIELDS: readonly string[];
485
520
  * ignores the key it was written into.
486
521
  */
487
522
  export const UNRENDERED_ON_UNKNOWN: ReadonlySet<string>;
523
+ /**
524
+ * PROBLEM CLASS — an event marked vetoable on a path where the host can never
525
+ * veto it, so a deny reads downstream as a block that never happened.
526
+ *
527
+ * Each entry names a field (`path` into the {@link ToolCallEvent}) whose `value`
528
+ * makes `this_call_vetoable: true` a false claim. This is the ONE list:
529
+ * {@link makeEvent} refuses an event that breaks an entry, and the JSON-schema
530
+ * generator writes one `if`/`then` rule per entry, so the runtime and the
531
+ * published schema cannot hold different invariants.
532
+ * @type {ReadonlyArray<{ path: readonly string[], value: string, why: string }>}
533
+ */
534
+ export const VETO_IMPOSSIBLE: ReadonlyArray<{
535
+ path: readonly string[];
536
+ value: string;
537
+ why: string;
538
+ }>;
488
539
  /**
489
540
  * The optional {@link EventMeta} string fields a host may carry, and the SSOT
490
541
  * {@link baseMeta} reads off every native payload.
@@ -498,6 +549,33 @@ export const UNRENDERED_ON_UNKNOWN: ReadonlySet<string>;
498
549
  * no adapter carries an omission of its own.
499
550
  */
500
551
  export const STANDARD_META_FIELDS: readonly string[];
552
+ /**
553
+ * The block reason a render writes when the judge gave none (or an empty one).
554
+ * Codex drops an enforced deny whose reason is empty and runs the tool, and on
555
+ * every host the model is refused with no rationale without one.
556
+ */
557
+ export const DEFAULT_DENY_REASON: "blocked by monitor";
558
+ /**
559
+ * PROBLEM CLASS — an enforced block whose reason never reaches the host, or
560
+ * whose exit code is not the one the host treats as a block, because each
561
+ * adapter hand-wired its own stderr and exit code.
562
+ *
563
+ * One row per agent, describing that host's ENFORCED block:
564
+ * - `exit_code`: the exit status the host treats as a block.
565
+ * - `reason_stream`: the stream the host reads the block reason from on that
566
+ * exit. Every host here discards (or never gets) the stdout body on exit 2.
567
+ * - `reason_required`: the host drops a block whose reason is empty and runs
568
+ * the tool, so every body field that carries the reason must be non-empty.
569
+ *
570
+ * {@link gatedResponse} derives each adapter's enforced render from its row,
571
+ * and the conformance harness holds every enforced render to the same row.
572
+ * @type {Readonly<Record<string, Readonly<{ exit_code: number, reason_stream: "stderr", reason_required: boolean }>>>}
573
+ */
574
+ export const BLOCK_TRANSPORT: Readonly<Record<string, Readonly<{
575
+ exit_code: number;
576
+ reason_stream: "stderr";
577
+ reason_required: boolean;
578
+ }>>>;
501
579
  /**
502
580
  * The translator for one agent's protocol. `parse` maps a native event to a
503
581
  * {@link ToolCallEvent} (never throwing on unmodelled input, stamping the
@@ -566,7 +644,8 @@ export type EventMeta = {
566
644
  /**
567
645
  * The result of rendering a {@link Verdict} for a specific agent's transport —
568
646
  * the ATTACH mechanism, not the decision. A caller applies whichever channels
569
- * are present: write `stdout`, exit with `exit_code`, or `throw`.
647
+ * are present: write `stdout`, write `stderr`, then exit with `exit_code`
648
+ * (`emit` in `agent-control-plane-core/runtime` does all three).
570
649
  */
571
650
  export type NativeResponse = {
572
651
  transport: "external_hook" | "in_process" | "observe_only";
package/types/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  export * from "./control-plane.mjs";
2
- export { codexAdapter } from "./adapters/codex.mjs";
3
2
  export { ampAdapter } from "./adapters/amp.mjs";
4
3
  export { claudeAdapter, HookEvent } from "./adapters/claude.mjs";
4
+ export { codexAdapter, createCodexAdapter } from "./adapters/codex.mjs";
5
5
  export { geminiAdapter, GEMINI_TOOL_ALIASES, POST_TOOL_REDACTION_UNSUPPORTED } from "./adapters/gemini.mjs";
6
6
  export { ADAPTERS, AGENT_IDS, adapterFor, assertRegistryConsistent } from "./registry.mjs";
7
7
  export { runAdapterConformance, assertCoverageWellFormed, assertToolAliasesCovered, assertAliasedInputsCanonical } from "./conformance.mjs";
@@ -12,10 +12,15 @@ export function assertRegistryConsistent(adapters: Record<string, Adapter>): voi
12
12
  * selection signal that names no shipped adapter is a misconfiguration, not a
13
13
  * reason to silently fall back to a default agent (which would run the wrong
14
14
  * translator against the payloads and mis-render every verdict).
15
+ *
16
+ * `options` are the launcher's settings for that adapter (Codex:
17
+ * `{ version }`). Passing options to an adapter that takes none throws, so a
18
+ * setting the launcher believes it applied cannot be dropped in silence.
15
19
  * @param {string} id agent id (a {@link AGENT_IDS} member)
20
+ * @param {Record<string, unknown>} [options]
16
21
  * @returns {Adapter}
17
22
  */
18
- export function adapterFor(id: string): Adapter;
23
+ export function adapterFor(id: string, options?: Record<string, unknown>): Adapter;
19
24
  /** @typedef {import("./control-plane.mjs").Adapter} Adapter */
20
25
  /**
21
26
  * Every shipped adapter. Adding an adapter to the package means adding it here —
@@ -13,19 +13,22 @@ export function readStdin(): Promise<string>;
13
13
  export function demoJudge(event: import("./control-plane.mjs").ToolCallEvent): import("./control-plane.mjs").Verdict;
14
14
  /**
15
15
  * Run one host's parse→judge→render pipe over a raw stdin payload. If the payload
16
- * can't be parsed or the pipe throws, return `onFailure` — the host's OWN
16
+ * can't be parsed or the render throws, return `onFailure` — the host's OWN
17
17
  * fail-safe response — so a crashed hook degrades the way THAT host expects
18
18
  * (claude/codex fail OPEN = exit 0; amp fails to ASK = exit 1) instead of a
19
19
  * single shared default. This is the deliberate, necessary recovery the whole
20
20
  * per-host split exists for, not a blanket swallow.
21
21
  *
22
- * Before returning the fail-safe, the error is written to STDERR (never stdout,
23
- * which belongs to the host transport) so a regression that silently disables
24
- * enforcement leaves a greppable trace. Every host tolerates hook stderr:
25
- * claude/codex surface it only in verbose/debug on a 0 exit, amp forwards the
26
- * helper's stderr, and gemini reads stderr as a reason only on exit 2 — none of
27
- * these fail-safe exits is a 2 for a stdout-carrying transport, and amp's ask
28
- * (exit 1) shows the diagnostic alongside the prompt, which is the point.
22
+ * A judge that THROWS is not a malformed payload: the call parsed, and the judge
23
+ * failed to answer. That takes the same path as a judge that answers garbage —
24
+ * the clamp's `ask` (see below) — so a crashing guardrail puts the call in front
25
+ * of a human instead of taking an exit-0 fail-safe that runs it.
26
+ *
27
+ * Every error is written to STDERR (never stdout, which belongs to the host
28
+ * transport) so a regression that silently disables enforcement leaves a
29
+ * greppable trace. Every host tolerates hook stderr: claude/codex surface it
30
+ * only in verbose/debug on a 0 exit, amp forwards the helper's stderr, and
31
+ * gemini reads stderr as a reason only on exit 2.
29
32
  * `judge` is the seam a real deployment fills — {@link demoJudge} is a stand-in,
30
33
  * so the guardrail is supplied here rather than by forking this file. Whatever
31
34
  * it returns is treated as untrusted (see the clamp below).