agent-control-plane-core 0.11.2 → 0.13.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 +50 -0
- package/package.json +1 -1
- package/schema/control-plane/v1/agents.json +228 -0
- package/src/adapters/amp.mjs +19 -0
- package/src/adapters/claude.mjs +28 -0
- package/src/adapters/codex.mjs +78 -2
- package/src/adapters/gemini.mjs +24 -0
- package/src/agent-manifest.mjs +82 -0
- package/src/conformance.mjs +139 -1
- package/src/control-plane.mjs +180 -4
- package/src/json-schema.mjs +4 -4
- package/src/registry.mjs +35 -11
- package/src/runtime.mjs +30 -10
- package/types/adapters/amp.d.mts +12 -0
- package/types/adapters/claude.d.mts +15 -0
- package/types/adapters/codex.d.mts +39 -2
- package/types/adapters/gemini.d.mts +17 -0
- package/types/agent-manifest.d.mts +8 -0
- package/types/conformance.d.mts +7 -1
- package/types/control-plane.d.mts +98 -4
- package/types/json-schema.d.mts +4 -4
- package/types/registry.d.mts +11 -3
- package/types/runtime.d.mts +23 -9
|
@@ -58,11 +58,12 @@ export function render(verdict: Verdict, event: ToolCallEvent, { soleGate }?: {
|
|
|
58
58
|
* the Codex version the launcher pinned (Codex's hook stdin carries none).
|
|
59
59
|
* Below {@link MIN_ENFORCING_VERSION} every event parses observe-only and a
|
|
60
60
|
* deny renders advisory. With no `version`, `parse` falls back to a payload
|
|
61
|
-
* `version` (see {@link parse}).
|
|
61
|
+
* `version` (see {@link parse}). An option outside {@link OPTION_NAMES} throws,
|
|
62
|
+
* so a misspelled setting is never dropped in silence.
|
|
62
63
|
* @param {{ version?: string }} [options]
|
|
63
64
|
* @returns {import("../control-plane.mjs").Adapter}
|
|
64
65
|
*/
|
|
65
|
-
export function createCodexAdapter(
|
|
66
|
+
export function createCodexAdapter(options?: {
|
|
66
67
|
version?: string;
|
|
67
68
|
}): import("../control-plane.mjs").Adapter;
|
|
68
69
|
/** @typedef {import("../control-plane.mjs").ToolCallEvent} ToolCallEvent */
|
|
@@ -118,6 +119,40 @@ export const COVERAGE: import("../control-plane.mjs").CoverageMap;
|
|
|
118
119
|
* @type {Record<string, ReadonlySet<string>|undefined>}
|
|
119
120
|
*/
|
|
120
121
|
export const UNRENDERED_FIELDS: Record<string, ReadonlySet<string> | undefined>;
|
|
122
|
+
/**
|
|
123
|
+
* How Codex's `PreToolUse` hook files a model tool call ([X7], read from
|
|
124
|
+
* Codex's source at `rust-v0.166.0`). A function tool is filed under its flat
|
|
125
|
+
* name (namespace + name) with its parsed arguments by default; these
|
|
126
|
+
* handlers supply their own payload or none:
|
|
127
|
+
* - `exec_command` → `Bash` with `{command: <cmd>}`; arguments Codex cannot
|
|
128
|
+
* parse as `ExecCommandArgs` file nothing, which this table models only for
|
|
129
|
+
* a missing or null `cmd` or scalar arguments. Not modelled: other type
|
|
130
|
+
* errors, and a positional array, which serde reads (`["ls"]` is `cmd: "ls"`).
|
|
131
|
+
* - `apply_patch` (a freeform tool) → `apply_patch` with its raw patch text as
|
|
132
|
+
* `command`.
|
|
133
|
+
* - `spawn_agent` in the `multi_agent_v1` namespace → `spawn_agent`.
|
|
134
|
+
* - `write_stdin`, code mode's `wait`, and code mode's freeform `exec` → no hook.
|
|
135
|
+
* These rules assume the named tool owns the name; a client-defined tool
|
|
136
|
+
* called `wait` with code mode off takes the default payload.
|
|
137
|
+
* The default payload is function-only, so a freeform extension tool files
|
|
138
|
+
* nothing too — the mini-language's rule for an unlisted freeform call.
|
|
139
|
+
* NOT MODELLED: Codex files an MCP call as `mcp__<namespace>__<tool>`, with
|
|
140
|
+
* `_` trimmed from the namespace's end and the tool's start (`mcp.rs`
|
|
141
|
+
* `hook_tool_name`), which can differ from the flat name — a rule keyed by
|
|
142
|
+
* one name cannot express that.
|
|
143
|
+
* @type {import("../control-plane.mjs").PreToolFiling}
|
|
144
|
+
*/
|
|
145
|
+
export const PRE_TOOL_FILING: import("../control-plane.mjs").PreToolFiling;
|
|
146
|
+
/**
|
|
147
|
+
* Codex's `PreToolUse` output parser ([X8], `rust-v0.166.0`) deserializes
|
|
148
|
+
* `hookSpecificOutput` into a `deny_unknown_fields` struct of exactly these
|
|
149
|
+
* five keys. Any other key fails the whole body: the run is marked failed, no
|
|
150
|
+
* block is set, and the call RUNS. A known key can still void the body by its
|
|
151
|
+
* combination (`updatedInput` without `permissionDecision: "allow"`), which
|
|
152
|
+
* this declaration does not describe.
|
|
153
|
+
* @type {import("../control-plane.mjs").PreToolResponse}
|
|
154
|
+
*/
|
|
155
|
+
export const PRE_TOOL_RESPONSE: import("../control-plane.mjs").PreToolResponse;
|
|
121
156
|
/** Minimum Codex version whose hook can actually veto a tool call. */
|
|
122
157
|
export const MIN_ENFORCING_VERSION: readonly number[];
|
|
123
158
|
/**
|
|
@@ -140,6 +175,8 @@ export const NATIVE_EVENT_FOR: Record<string, string | undefined>;
|
|
|
140
175
|
* naming a key the host discards.
|
|
141
176
|
*/
|
|
142
177
|
export const NATIVE_ASK_TIER: false;
|
|
178
|
+
/** The launcher option names {@link createCodexAdapter} reads. */
|
|
179
|
+
export const OPTION_NAMES: readonly string[];
|
|
143
180
|
/**
|
|
144
181
|
* The Codex adapter with no launcher-pinned version. Real Codex sends no
|
|
145
182
|
* version, so every event it sends parses observe-only here. A deployment that
|
|
@@ -126,6 +126,23 @@ export const GEMINI_TOOL_ALIASES: Readonly<Record<string, string>>;
|
|
|
126
126
|
* here, because an un-escalated ask lets Gemini run the tool.
|
|
127
127
|
*/
|
|
128
128
|
export const NATIVE_ASK_TIER: false;
|
|
129
|
+
/**
|
|
130
|
+
* No rename is modelled. Gemini CLI hands `BeforeTool` the invocation's tool
|
|
131
|
+
* name beside an `originalRequestName`, so an aliased call may be filed under
|
|
132
|
+
* a name the model did not use; that is unverified, so not published.
|
|
133
|
+
* @type {import("../control-plane.mjs").PreToolFiling}
|
|
134
|
+
*/
|
|
135
|
+
export const PRE_TOOL_FILING: import("../control-plane.mjs").PreToolFiling;
|
|
136
|
+
/**
|
|
137
|
+
* Gemini CLI keeps `hookSpecificOutput` as a `Record<string, unknown>` and its
|
|
138
|
+
* `BeforeTool` path reads only `tool_input` from it, by `in` lookup, so any
|
|
139
|
+
* other key is ignored (`packages/core/src/hooks/types.ts`
|
|
140
|
+
* `BeforeToolHookOutput`; both firing paths, `core/coreToolHookTriggers.ts`
|
|
141
|
+
* and `scheduler/hook-utils.ts` `evaluateBeforeToolHook`, read at gemini-cli
|
|
142
|
+
* `62bab5b2`).
|
|
143
|
+
* @type {import("../control-plane.mjs").PreToolResponse}
|
|
144
|
+
*/
|
|
145
|
+
export const PRE_TOOL_RESPONSE: import("../control-plane.mjs").PreToolResponse;
|
|
129
146
|
/** @type {import("../control-plane.mjs").Adapter} */
|
|
130
147
|
export const geminiAdapter: import("../control-plane.mjs").Adapter;
|
|
131
148
|
export type ToolCallEvent = import("../control-plane.mjs").ToolCallEvent;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every generated file, keyed by its package-relative path: the JSON Schema
|
|
3
|
+
* documents and their data maps, plus `agents.json`, keyed by agent id in
|
|
4
|
+
* {@link AGENT_IDS} order. The SSOT the generator writes and the freshness test
|
|
5
|
+
* reads, so neither carries its own list of filenames.
|
|
6
|
+
* @returns {Record<string, Record<string, unknown>>}
|
|
7
|
+
*/
|
|
8
|
+
export function generatedDocuments(): Record<string, Record<string, unknown>>;
|
package/types/conformance.d.mts
CHANGED
|
@@ -132,13 +132,18 @@ export function assertAliasedInputsCanonical(fixturesList: any[], assert: any):
|
|
|
132
132
|
* decide where an `ask` must be escalated to a `deny`, so a wrong one
|
|
133
133
|
* either lets a call through or denies every asked call on the hosts that
|
|
134
134
|
* do ask. OBSERVE_ONLY renders are exempt, as in rule ⑧.
|
|
135
|
+
* 12. pre-tool key honesty: the adapter declares a well-formed
|
|
136
|
+
* `PRE_TOOL_FILING` and `PRE_TOOL_RESPONSE`, and every render on the host's
|
|
137
|
+
* native pre-tool event — fixture, rule ⑧ and rule ⑩ probes alike — puts
|
|
138
|
+
* in `hookSpecificOutput` only keys `PRE_TOOL_RESPONSE` says the host
|
|
139
|
+
* reads. At least one such render must be checked.
|
|
135
140
|
*
|
|
136
141
|
* `assert` is injected (node:assert/strict) so the harness stays test-framework
|
|
137
142
|
* neutral; it throws on the first mismatch. Returns a summary the caller can
|
|
138
143
|
* assert further on.
|
|
139
144
|
*
|
|
140
145
|
* @param {{ adapter: import("./control-plane.mjs").Adapter, fixtures: any, assert: any }} args
|
|
141
|
-
* @returns {{ cases: number, renders: number, decisionsSeen: Set<string>, mutationSeen: boolean, contentFieldsSeen: Set<string>, enforcedDenySeen: boolean, vetoableDenySeen: boolean, unknownKindSeen: boolean, coverageClassesChecked: Set<string>, unenforceableDenyChecks: number, askTierChecks: number }}
|
|
146
|
+
* @returns {{ cases: number, renders: number, decisionsSeen: Set<string>, mutationSeen: boolean, contentFieldsSeen: Set<string>, enforcedDenySeen: boolean, vetoableDenySeen: boolean, unknownKindSeen: boolean, coverageClassesChecked: Set<string>, unenforceableDenyChecks: number, askTierChecks: number, preToolKeyChecks: number }}
|
|
142
147
|
*/
|
|
143
148
|
export function runAdapterConformance({ adapter, fixtures, assert }: {
|
|
144
149
|
adapter: import("./control-plane.mjs").Adapter;
|
|
@@ -156,4 +161,5 @@ export function runAdapterConformance({ adapter, fixtures, assert }: {
|
|
|
156
161
|
coverageClassesChecked: Set<string>;
|
|
157
162
|
unenforceableDenyChecks: number;
|
|
158
163
|
askTierChecks: number;
|
|
164
|
+
preToolKeyChecks: number;
|
|
159
165
|
};
|
|
@@ -124,6 +124,40 @@ export function classifyCallClass(tool: string | null, native?: Record<string, u
|
|
|
124
124
|
* @returns {ReadonlySet<string>}
|
|
125
125
|
*/
|
|
126
126
|
export function readonlySet(values: Iterable<string>): ReadonlySet<string>;
|
|
127
|
+
/**
|
|
128
|
+
* The model tool name a {@link PreToolFiling} is keyed by: `name`, prefixed
|
|
129
|
+
* (no separator) with `namespace` unless that is absent, empty, or Codex's
|
|
130
|
+
* default `functions` namespace (`flat_tool_name`, `is_default_namespace`;
|
|
131
|
+
* [X7] in `docs/hook-coverage-matrix.md`).
|
|
132
|
+
* @param {string | null | undefined} namespace
|
|
133
|
+
* @param {string} name
|
|
134
|
+
* @returns {string}
|
|
135
|
+
*/
|
|
136
|
+
export function flatToolName(namespace: string | null | undefined, name: string): string;
|
|
137
|
+
/**
|
|
138
|
+
* Whether `rule` is a non-null {@link PreToolFiling} rule: a plain object with
|
|
139
|
+
* exactly a non-empty string `tool_name` and a well-formed `input`. The one
|
|
140
|
+
* grammar both {@link fileModelCall} and the conformance harness hold a rule to.
|
|
141
|
+
* @param {unknown} rule
|
|
142
|
+
* @returns {boolean}
|
|
143
|
+
*/
|
|
144
|
+
export function isFilingRule(rule: unknown): boolean;
|
|
145
|
+
/**
|
|
146
|
+
* The call a host's pre-tool hook files for a model call of `kind` to `name`
|
|
147
|
+
* with arguments value `args`, per that host's {@link PreToolFiling}; `null`
|
|
148
|
+
* when the hook files none. The reference reading of the mini-language
|
|
149
|
+
* `agents.json` publishes as `pre_tool_filing`. Throws on a rule outside that
|
|
150
|
+
* grammar or a kind outside {@link MODEL_CALL_KINDS}.
|
|
151
|
+
* @param {PreToolFiling} filing
|
|
152
|
+
* @param {"function" | "freeform"} kind whether the model made a function call or a freeform (custom) call
|
|
153
|
+
* @param {string} name the model tool name, a {@link flatToolName}
|
|
154
|
+
* @param {unknown} args the call's arguments value
|
|
155
|
+
* @returns {{ tool_name: string, tool_input: unknown } | null}
|
|
156
|
+
*/
|
|
157
|
+
export function fileModelCall(filing: PreToolFiling, kind: "function" | "freeform", name: string, args: unknown): {
|
|
158
|
+
tool_name: string;
|
|
159
|
+
tool_input: unknown;
|
|
160
|
+
} | null;
|
|
127
161
|
/**
|
|
128
162
|
* The translator for one agent's protocol. `parse` maps a native event to a
|
|
129
163
|
* {@link ToolCallEvent} (never throwing on unmodelled input, stamping the
|
|
@@ -137,6 +171,8 @@ export function readonlySet(values: Iterable<string>): ReadonlySet<string>;
|
|
|
137
171
|
* @property {(verdict: Verdict, event: ToolCallEvent, options?: { soleGate?: boolean }) => NativeResponse} render
|
|
138
172
|
* @property {Record<string, string|undefined>} [NATIVE_EVENT_FOR] the native event name this host uses for each {@link EventKind}, for a conformance probe of a kind no fixture produced. Optional: a kind absent here (`unknown` always, plus any kind this transport does not carry) is probed with a marker name, which takes the adapter's unrecognized-event branch. Never read at runtime — `parse` stamps the real name on `meta.native_event`.
|
|
139
173
|
* @property {boolean} NATIVE_ASK_TIER whether this host's transport carries a DISTINCT "ask the human" tier that the host honours. `true` ⇒ a rendered `ask` suspends the call in front of a person (Claude Code's `permissionDecision: "ask"`, Amp's exit 1). `false` ⇒ the host has no such tier, so `render` collapses `ask` onto another signal (Gemini CLI writes its advisory `decision: "deny"`) and an un-escalated ask lets the tool run. A consumer that must not let an ask through reads this to decide where to escalate `ask` to `deny`, instead of keeping its own hand-typed list of agent ids. It is about the HOST, not the verdict: `ask` never renders as `enforced` on any adapter, and the tier it names is the PRE-TOOL gate — once a tool has run there is nothing left to suspend, so every host renders a post-tool `ask` as its block. The conformance harness probes the real `render` against it — a `true` whose ask renders as the host's abstaining allow fails, and so does a `false` whose ask differs from its own advisory deny.
|
|
174
|
+
* @property {PreToolFiling} PRE_TOOL_FILING how this host's pre-tool hook files a model tool call, for a consumer matching the model's calls against the hook's. `{}` when no rename is modelled.
|
|
175
|
+
* @property {PreToolResponse} PRE_TOOL_RESPONSE the `hookSpecificOutput` keys this host reads on its pre-tool event, and whether an unread key there runs the call. The conformance harness holds every pre-tool render to the key list.
|
|
140
176
|
* @property {Record<string, ReadonlySet<string>|undefined>} UNRENDERED_FIELDS per-event-kind set of {@link VERDICT_CONTENT_FIELDS} this host has no native channel for, so `render` drops them. EVERY {@link EventKind} carries a row, including one this adapter's `parse` cannot emit — an omission would otherwise read as "every content field reaches a channel here", the reverse of the truth on a transport that carries none. The value type still admits `undefined` so a consumer handles a lookup miss rather than indexing straight into `.has(...)`; the conformance harness refuses a missing row. SCOPE — a row describes the ALLOW path only. An enforceable deny may drop more: Gemini's exit-2 System Block returns no stdout at all, so a deny there carries no `additional_context` whatever the row says. Do not read this map to infer what a deny delivers. The conformance harness fails an adapter whose renders disagree with its declaration either way, so a stale entry cannot survive.
|
|
141
177
|
*/
|
|
142
178
|
/**
|
|
@@ -354,10 +390,10 @@ export function renderEscalated(adapter: Adapter, verdict: Verdict, event: ToolC
|
|
|
354
390
|
* {@link Adapter} interface an integrator implements. A new REQUIRED adapter
|
|
355
391
|
* member breaks every third-party adapter while every event on the wire stays
|
|
356
392
|
* byte-identical, so it is a package-semver break and leaves
|
|
357
|
-
* {@link CONTROL_PLANE_SCHEMA} alone. `UNRENDERED_FIELDS`
|
|
358
|
-
* `
|
|
359
|
-
* adapter, and the conformance harness names the member
|
|
360
|
-
* rather than throwing from inside itself.
|
|
393
|
+
* {@link CONTROL_PLANE_SCHEMA} alone. `UNRENDERED_FIELDS`, `NATIVE_ASK_TIER`,
|
|
394
|
+
* `PRE_TOOL_FILING` and `PRE_TOOL_RESPONSE` are such members: adding one is a
|
|
395
|
+
* one-line change per adapter, and the conformance harness names the member
|
|
396
|
+
* and the migration rather than throwing from inside itself.
|
|
361
397
|
*/
|
|
362
398
|
/** Wire identifier for this schema version; bump on a breaking shape change. */
|
|
363
399
|
export const CONTROL_PLANE_SCHEMA: "control-plane/v1";
|
|
@@ -544,6 +580,14 @@ export const VERDICT_CONTENT_FIELDS: readonly string[];
|
|
|
544
580
|
* ignores the key it was written into.
|
|
545
581
|
*/
|
|
546
582
|
export const UNRENDERED_ON_UNKNOWN: ReadonlySet<string>;
|
|
583
|
+
/** The {@link PreToolFiling} input reference to a call's whole arguments value. */
|
|
584
|
+
export const WHOLE_ARGUMENTS: "$";
|
|
585
|
+
/** The prefix of a {@link PreToolFiling} input reference to one top-level argument. */
|
|
586
|
+
export const ARGUMENT_KEY_PREFIX: "$.";
|
|
587
|
+
/** What a {@link PreToolFiling} rule must be, for error messages. */
|
|
588
|
+
export const FILING_RULE_GRAMMAR: "{tool_name: <non-empty string>, input: \"$\" or an object of \"$\"/\"$.<key>\" references (<key>: non-empty, no \".\")}";
|
|
589
|
+
/** The kinds of model tool call a {@link PreToolFiling} distinguishes. */
|
|
590
|
+
export const MODEL_CALL_KINDS: ReadonlySet<string>;
|
|
547
591
|
/**
|
|
548
592
|
* PROBLEM CLASS — an event marked vetoable on a path where the host can never
|
|
549
593
|
* veto it, so a deny reads downstream as a block that never happened.
|
|
@@ -600,6 +644,19 @@ export const BLOCK_TRANSPORT: Readonly<Record<string, Readonly<{
|
|
|
600
644
|
reason_stream: "stderr";
|
|
601
645
|
reason_required: boolean;
|
|
602
646
|
}>>>;
|
|
647
|
+
/**
|
|
648
|
+
* What a host reads from the `hookSpecificOutput` of an exit-0 answer to its
|
|
649
|
+
* pre-tool event (`PreToolUse`, `BeforeTool`). `hook_specific_output_keys` is
|
|
650
|
+
* every key it reads there, sorted (empty when it reads no body).
|
|
651
|
+
* `unread_key_fails_open`: `true` ⇒ any other key there voids the whole body
|
|
652
|
+
* and the call RUNS; `false` ⇒ other keys are ignored; `null` ⇒ not
|
|
653
|
+
* established from the host's source. Top-level keys are outside this record;
|
|
654
|
+
* Codex parses those `deny_unknown_fields` too ([X8]).
|
|
655
|
+
*/
|
|
656
|
+
export type PreToolResponse = Readonly<{
|
|
657
|
+
hook_specific_output_keys: readonly string[];
|
|
658
|
+
unread_key_fails_open: boolean | null;
|
|
659
|
+
}>;
|
|
603
660
|
/**
|
|
604
661
|
* The translator for one agent's protocol. `parse` maps a native event to a
|
|
605
662
|
* {@link ToolCallEvent} (never throwing on unmodelled input, stamping the
|
|
@@ -625,6 +682,14 @@ export type Adapter = {
|
|
|
625
682
|
* whether this host's transport carries a DISTINCT "ask the human" tier that the host honours. `true` ⇒ a rendered `ask` suspends the call in front of a person (Claude Code's `permissionDecision: "ask"`, Amp's exit 1). `false` ⇒ the host has no such tier, so `render` collapses `ask` onto another signal (Gemini CLI writes its advisory `decision: "deny"`) and an un-escalated ask lets the tool run. A consumer that must not let an ask through reads this to decide where to escalate `ask` to `deny`, instead of keeping its own hand-typed list of agent ids. It is about the HOST, not the verdict: `ask` never renders as `enforced` on any adapter, and the tier it names is the PRE-TOOL gate — once a tool has run there is nothing left to suspend, so every host renders a post-tool `ask` as its block. The conformance harness probes the real `render` against it — a `true` whose ask renders as the host's abstaining allow fails, and so does a `false` whose ask differs from its own advisory deny.
|
|
626
683
|
*/
|
|
627
684
|
NATIVE_ASK_TIER: boolean;
|
|
685
|
+
/**
|
|
686
|
+
* how this host's pre-tool hook files a model tool call, for a consumer matching the model's calls against the hook's. `{}` when no rename is modelled.
|
|
687
|
+
*/
|
|
688
|
+
PRE_TOOL_FILING: PreToolFiling;
|
|
689
|
+
/**
|
|
690
|
+
* the `hookSpecificOutput` keys this host reads on its pre-tool event, and whether an unread key there runs the call. The conformance harness holds every pre-tool render to the key list.
|
|
691
|
+
*/
|
|
692
|
+
PRE_TOOL_RESPONSE: PreToolResponse;
|
|
628
693
|
/**
|
|
629
694
|
* per-event-kind set of {@link VERDICT_CONTENT_FIELDS} this host has no native channel for, so `render` drops them. EVERY {@link EventKind} carries a row, including one this adapter's `parse` cannot emit — an omission would otherwise read as "every content field reaches a channel here", the reverse of the truth on a transport that carries none. The value type still admits `undefined` so a consumer handles a lookup miss rather than indexing straight into `.has(...)`; the conformance harness refuses a missing row. SCOPE — a row describes the ALLOW path only. An enforceable deny may drop more: Gemini's exit-2 System Block returns no stdout at all, so a deny there carries no `additional_context` whatever the row says. Do not read this map to infer what a deny delivers. The conformance harness fails an adapter whose renders disagree with its declaration either way, so a stale entry cannot survive.
|
|
630
695
|
*/
|
|
@@ -739,3 +804,32 @@ export type Verdict = {
|
|
|
739
804
|
*/
|
|
740
805
|
reason?: string | undefined;
|
|
741
806
|
};
|
|
807
|
+
/**
|
|
808
|
+
* How a host's pre-tool hook files a model's tool call, keyed by MODEL tool
|
|
809
|
+
* name, the {@link flatToolName} of the call's `namespace` and `name`. Each
|
|
810
|
+
* value is one of:
|
|
811
|
+
* - `null`: no pre-tool hook fires for this call.
|
|
812
|
+
* - `{ tool_name, input }`: the hook fires with `tool_name`, and `tool_input`
|
|
813
|
+
* built from `input` — either `"$"` (the call's whole arguments value), or
|
|
814
|
+
* an object whose every value is `"$"` or `"$.<key>"`. `<key>` is one
|
|
815
|
+
* literal, non-empty top-level key with no `.` in it. The hook files NOTHING
|
|
816
|
+
* when a `"$.<key>"` does not resolve: the arguments are not an object, lack
|
|
817
|
+
* the key, or hold `null` there.
|
|
818
|
+
* A FUNCTION call the map does not list is filed as the model asked: same
|
|
819
|
+
* name, whole arguments. A FREEFORM (custom) call the map does not list files
|
|
820
|
+
* nothing. Of the shipped hosts only Codex makes freeform calls ([X7] in
|
|
821
|
+
* `docs/hook-coverage-matrix.md`).
|
|
822
|
+
* The map describes a call the host DISPATCHES. Codex files nothing for a call
|
|
823
|
+
* it rejects first: a name it has no tool for, a call kind the tool does not
|
|
824
|
+
* take (each listed rule assumes the tool's own kind), or arguments the
|
|
825
|
+
* handler refuses before its hook (`apply_patch` whose patch does not parse).
|
|
826
|
+
* On Codex the ARGUMENTS VALUE is a function call's `arguments` parsed as JSON
|
|
827
|
+
* (an empty or all-whitespace string reads as `{}`, unparseable text stays the
|
|
828
|
+
* raw string), or a freeform call's raw `input` string.
|
|
829
|
+
* Plain JSON by design: `agents.json` publishes it so a consumer with no JS
|
|
830
|
+
* runtime applies it the way {@link fileModelCall} does.
|
|
831
|
+
*/
|
|
832
|
+
export type PreToolFiling = Readonly<Record<string, null | Readonly<{
|
|
833
|
+
tool_name: string;
|
|
834
|
+
input: string | Readonly<Record<string, string>>;
|
|
835
|
+
}>>>;
|
package/types/json-schema.d.mts
CHANGED
|
@@ -25,10 +25,10 @@ export function toolCallEventSchema(): Record<string, unknown>;
|
|
|
25
25
|
*/
|
|
26
26
|
export function verdictSchema(): Record<string, unknown>;
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* `
|
|
28
|
+
* The contract's generated files, keyed by package-relative path. The two
|
|
29
|
+
* `.schema.json` entries are JSON Schema documents; `tool-input-keys.json` is
|
|
30
|
+
* the data map {@link TOOL_INPUT_KEYS_FILE} describes. `agent-manifest.mjs`'s
|
|
31
|
+
* `generatedDocuments` adds the per-agent manifest to these.
|
|
32
32
|
* @returns {Record<string, Record<string, unknown>>}
|
|
33
33
|
*/
|
|
34
34
|
export function jsonSchemaDocuments(): Record<string, Record<string, unknown>>;
|
package/types/registry.d.mts
CHANGED
|
@@ -13,9 +13,10 @@ export function assertRegistryConsistent(adapters: Record<string, Adapter>): voi
|
|
|
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
15
|
*
|
|
16
|
-
* `options` are the launcher's settings for that adapter
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* `options` are the launcher's settings for that adapter, named by its
|
|
17
|
+
* {@link LAUNCHER_OPTIONS} row. Options for an adapter that takes none throw
|
|
18
|
+
* here, and an option a configurable adapter does not read throws in its
|
|
19
|
+
* factory, so a setting the launcher believes it applied is never dropped.
|
|
19
20
|
* @param {string} id agent id (a {@link AGENT_IDS} member)
|
|
20
21
|
* @param {Record<string, unknown>} [options]
|
|
21
22
|
* @returns {Adapter}
|
|
@@ -37,4 +38,11 @@ export const ADAPTERS: Readonly<Record<string, Adapter>>;
|
|
|
37
38
|
* @type {readonly string[]}
|
|
38
39
|
*/
|
|
39
40
|
export const AGENT_IDS: readonly string[];
|
|
41
|
+
/**
|
|
42
|
+
* The launcher option names each agent's adapter reads, for EVERY agent id (an
|
|
43
|
+
* empty list when it reads none). A launcher builds the options for any agent
|
|
44
|
+
* from this instead of naming the agents that take a version.
|
|
45
|
+
* @type {Readonly<Record<string, readonly string[]>>}
|
|
46
|
+
*/
|
|
47
|
+
export const LAUNCHER_OPTIONS: Readonly<Record<string, readonly string[]>>;
|
|
40
48
|
export type Adapter = import("./control-plane.mjs").Adapter;
|
package/types/runtime.d.mts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A guardrail judge: one normalized event in, its verdict out, synchronously or
|
|
3
|
+
* as a promise. Its answer is untrusted either way (see {@link renderHookResponse}).
|
|
4
|
+
* @typedef {(event: import("./control-plane.mjs").ToolCallEvent) => import("./control-plane.mjs").Verdict | Promise<import("./control-plane.mjs").Verdict>} Judge
|
|
5
|
+
*/
|
|
1
6
|
/**
|
|
2
7
|
* Read all of stdin to a string.
|
|
3
8
|
* @returns {Promise<string>}
|
|
@@ -31,16 +36,18 @@ export function demoJudge(event: import("./control-plane.mjs").ToolCallEvent): i
|
|
|
31
36
|
* greppable trace. Every host tolerates hook stderr: claude/codex surface it
|
|
32
37
|
* only in verbose/debug on a 0 exit, amp forwards the helper's stderr, and
|
|
33
38
|
* gemini reads stderr as a reason only on exit 2.
|
|
34
|
-
* `judge` is the seam a real deployment fills
|
|
35
|
-
*
|
|
36
|
-
*
|
|
39
|
+
* `judge` is the seam a real deployment fills, so the guardrail is supplied here
|
|
40
|
+
* rather than by forking this file ({@link demoJudge} is a stand-in). It may
|
|
41
|
+
* return a promise (a judge that asks a remote monitor); a rejection takes the
|
|
42
|
+
* same path as a throw. Whatever it returns is treated as untrusted (see the
|
|
43
|
+
* clamp below).
|
|
37
44
|
* @param {import("./control-plane.mjs").Adapter} adapter
|
|
38
45
|
* @param {string} rawInput
|
|
39
46
|
* @param {import("./control-plane.mjs").NativeResponse} onFailure
|
|
40
|
-
* @param {
|
|
41
|
-
* @returns {import("./control-plane.mjs").NativeResponse}
|
|
47
|
+
* @param {Judge} [judge]
|
|
48
|
+
* @returns {Promise<import("./control-plane.mjs").NativeResponse>}
|
|
42
49
|
*/
|
|
43
|
-
export function renderHookResponse(adapter: import("./control-plane.mjs").Adapter, rawInput: string, onFailure: import("./control-plane.mjs").NativeResponse, judge?:
|
|
50
|
+
export function renderHookResponse(adapter: import("./control-plane.mjs").Adapter, rawInput: string, onFailure: import("./control-plane.mjs").NativeResponse, judge?: Judge): Promise<import("./control-plane.mjs").NativeResponse>;
|
|
44
51
|
/**
|
|
45
52
|
* The response that BLOCKS the call on `agent`'s host: its
|
|
46
53
|
* {@link BLOCK_TRANSPORT} row's exit code, with `reason` on the row's reason
|
|
@@ -69,12 +76,14 @@ export function failurePosture(agent: string, env?: NodeJS.ProcessEnv): import("
|
|
|
69
76
|
* {@link renderHookResponse} with that host's {@link failurePosture}, and
|
|
70
77
|
* {@link emit} the result. Every `bin/<host>-hook.mjs` is this one call. The
|
|
71
78
|
* failure response also answers a stdin read error and, when
|
|
72
|
-
* {@link hookDeadlineMs} is set, a hook still running at that deadline.
|
|
79
|
+
* {@link hookDeadlineMs} is set, a hook still running at that deadline. It also
|
|
80
|
+
* answers a judge promise that never settles once nothing else keeps node alive,
|
|
81
|
+
* which would otherwise end the process with no response.
|
|
73
82
|
* @param {import("./control-plane.mjs").Adapter} adapter
|
|
74
|
-
* @param {
|
|
83
|
+
* @param {Judge} [judge]
|
|
75
84
|
* @returns {Promise<never>}
|
|
76
85
|
*/
|
|
77
|
-
export function runHookEntry(adapter: import("./control-plane.mjs").Adapter, judge?:
|
|
86
|
+
export function runHookEntry(adapter: import("./control-plane.mjs").Adapter, judge?: Judge): Promise<never>;
|
|
78
87
|
/**
|
|
79
88
|
* `ACP_HOOK_TIMEOUT_MS` as a positive integer, or `undefined` when unset. Set it
|
|
80
89
|
* below the host's own hook timeout, so the hook's failure response lands
|
|
@@ -135,3 +144,8 @@ export function emit(response: import("./control-plane.mjs").NativeResponse): ne
|
|
|
135
144
|
* @type {Readonly<Record<string, import("./control-plane.mjs").NativeResponse>>}
|
|
136
145
|
*/
|
|
137
146
|
export const HOST_FAILURE: Readonly<Record<string, import("./control-plane.mjs").NativeResponse>>;
|
|
147
|
+
/**
|
|
148
|
+
* A guardrail judge: one normalized event in, its verdict out, synchronously or
|
|
149
|
+
* as a promise. Its answer is untrusted either way (see {@link renderHookResponse}).
|
|
150
|
+
*/
|
|
151
|
+
export type Judge = (event: import("./control-plane.mjs").ToolCallEvent) => import("./control-plane.mjs").Verdict | Promise<import("./control-plane.mjs").Verdict>;
|