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.
- package/README.md +20 -12
- package/bin/amp-hook.mjs +0 -0
- package/bin/claude-hook.mjs +0 -0
- package/bin/codex-hook.mjs +11 -7
- package/bin/gemini-hook.mjs +0 -0
- package/package.json +7 -1
- package/src/adapters/amp.mjs +9 -5
- package/src/adapters/claude.mjs +11 -13
- package/src/adapters/codex.mjs +64 -38
- package/src/adapters/gemini.mjs +21 -26
- package/src/conformance.mjs +36 -1
- package/src/control-plane.mjs +141 -11
- package/src/fixtures/codex.json +18 -79
- package/src/index.mjs +1 -1
- package/src/json-schema.mjs +16 -28
- package/src/registry.mjs +23 -3
- package/src/runtime.mjs +46 -17
- package/types/adapters/codex.d.mts +33 -13
- package/types/conformance.d.mts +2 -1
- package/types/control-plane.d.mts +81 -2
- package/types/index.d.mts +1 -1
- package/types/registry.d.mts +6 -1
- package/types/runtime.d.mts +11 -8
package/types/conformance.d.mts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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";
|
package/types/registry.d.mts
CHANGED
|
@@ -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 —
|
package/types/runtime.d.mts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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).
|