agent-control-plane-core 0.14.0 → 0.15.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/package.json +1 -1
- package/src/adapters/codex.mjs +7 -8
- package/src/agent-manifest.mjs +7 -1
- package/src/conformance.mjs +15 -38
- package/src/control-plane.mjs +30 -68
- package/types/control-plane.d.mts +22 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-control-plane-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Vendor-neutral control-plane contract for coding agents: a normalized ToolCallEvent/Verdict schema with per-agent adapters (Claude Code, Codex, Amp, Gemini CLI) and a conformance suite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/adapters/codex.mjs
CHANGED
|
@@ -414,7 +414,7 @@ export function render(verdict, event, { soleGate = false } = {}) {
|
|
|
414
414
|
unblocked_exit_code: 0,
|
|
415
415
|
enforced,
|
|
416
416
|
reason: vd.reason,
|
|
417
|
-
stdout: nativeBody(hookEventName, vd, event,
|
|
417
|
+
stdout: nativeBody(hookEventName, vd, event, soleGate),
|
|
418
418
|
});
|
|
419
419
|
}
|
|
420
420
|
|
|
@@ -423,15 +423,14 @@ export function render(verdict, event, { soleGate = false } = {}) {
|
|
|
423
423
|
* @param {string} hookEventName
|
|
424
424
|
* @param {Verdict} vd a normalized verdict
|
|
425
425
|
* @param {ToolCallEvent} event
|
|
426
|
-
* @param {boolean} enforced
|
|
427
426
|
* @param {boolean} soleGate
|
|
428
427
|
* @returns {Record<string, unknown>}
|
|
429
428
|
*/
|
|
430
|
-
function nativeBody(hookEventName, vd, event,
|
|
429
|
+
function nativeBody(hookEventName, vd, event, soleGate) {
|
|
431
430
|
if (event.event === EventKind.POST_TOOL)
|
|
432
431
|
return postToolBody(hookEventName, vd);
|
|
433
432
|
if (hookEventName === HookEvent.PERMISSION_REQUEST)
|
|
434
|
-
return permissionRequestBody(hookEventName, vd,
|
|
433
|
+
return permissionRequestBody(hookEventName, vd, soleGate);
|
|
435
434
|
return preToolBody(hookEventName, vd, event);
|
|
436
435
|
}
|
|
437
436
|
|
|
@@ -475,18 +474,18 @@ function preToolBody(hookEventName, vd, event) {
|
|
|
475
474
|
* `updatedInput` voids the body and there is no context key.
|
|
476
475
|
* @param {string} hookEventName
|
|
477
476
|
* @param {Verdict} vd a normalized verdict
|
|
478
|
-
* @param {boolean} enforced
|
|
479
477
|
* @param {boolean} soleGate
|
|
480
478
|
* @returns {Record<string, unknown>}
|
|
481
479
|
*/
|
|
482
|
-
function permissionRequestBody(hookEventName, vd,
|
|
480
|
+
function permissionRequestBody(hookEventName, vd, soleGate) {
|
|
483
481
|
/** @type {Record<string, unknown>} */
|
|
484
482
|
const hookSpecificOutput = { hookEventName };
|
|
483
|
+
// Codex substitutes its own text for an empty message
|
|
484
|
+
// (`permission_request_decision`, [X8]); ours says who denied it.
|
|
485
485
|
if (vd.decision !== Decision.ALLOW) {
|
|
486
|
-
const message = enforced ? blockReason(vd.reason) : vd.reason;
|
|
487
486
|
hookSpecificOutput.decision = {
|
|
488
487
|
behavior: Decision.DENY,
|
|
489
|
-
|
|
488
|
+
message: blockReason(vd.reason),
|
|
490
489
|
};
|
|
491
490
|
} else if (soleGate) {
|
|
492
491
|
hookSpecificOutput.decision = { behavior: Decision.ALLOW };
|
package/src/agent-manifest.mjs
CHANGED
|
@@ -62,7 +62,13 @@ function manifestRow(id) {
|
|
|
62
62
|
]),
|
|
63
63
|
),
|
|
64
64
|
pre_tool_filing: adapter.PRE_TOOL_FILING,
|
|
65
|
-
pre_tool_response:
|
|
65
|
+
pre_tool_response: {
|
|
66
|
+
...adapter.PRE_TOOL_RESPONSE,
|
|
67
|
+
hook_specific_output_keys: [
|
|
68
|
+
...adapter.PRE_TOOL_RESPONSE.hook_specific_output_keys,
|
|
69
|
+
].sort(),
|
|
70
|
+
top_level_keys: [...adapter.PRE_TOOL_RESPONSE.top_level_keys].sort(),
|
|
71
|
+
},
|
|
66
72
|
};
|
|
67
73
|
}
|
|
68
74
|
|
package/src/conformance.mjs
CHANGED
|
@@ -11,9 +11,9 @@ import {
|
|
|
11
11
|
isCoverageStatus,
|
|
12
12
|
lookup,
|
|
13
13
|
VERDICT_CONTENT_FIELDS,
|
|
14
|
-
WHOLE_ARGUMENTS,
|
|
15
14
|
FILING_RULE_GRAMMAR,
|
|
16
15
|
isFilingRule,
|
|
16
|
+
isPlainObject,
|
|
17
17
|
} from "./control-plane.mjs";
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -808,22 +808,12 @@ function assertPreToolKeysRead(adapter, event, rendered, label, assert) {
|
|
|
808
808
|
return true;
|
|
809
809
|
}
|
|
810
810
|
|
|
811
|
-
/**
|
|
812
|
-
* Whether `value` is an object that is neither `null` nor an array.
|
|
813
|
-
* @param {unknown} value
|
|
814
|
-
* @returns {value is Record<string, unknown>}
|
|
815
|
-
*/
|
|
816
|
-
function isPlainObject(value) {
|
|
817
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
818
|
-
}
|
|
819
|
-
|
|
820
811
|
/**
|
|
821
812
|
* Assert an adapter declares a well-formed `PRE_TOOL_FILING` and
|
|
822
|
-
* `PRE_TOOL_RESPONSE`:
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
* Python consumer cannot apply is refused here, not discovered there.
|
|
813
|
+
* `PRE_TOOL_RESPONSE`: every filing rule `null` or in the mini-language
|
|
814
|
+
* {@link fileModelCall} reads, and each response key list an array of strings
|
|
815
|
+
* beside a boolean-or-null fail-open bit. `agents.json` publishes both, so a
|
|
816
|
+
* shape a Python consumer cannot apply is refused here, not discovered there.
|
|
827
817
|
* @param {import("./control-plane.mjs").Adapter} adapter
|
|
828
818
|
* @param {any} assert
|
|
829
819
|
*/
|
|
@@ -831,37 +821,24 @@ function assertPreToolDeclarationsWellFormed(adapter, assert) {
|
|
|
831
821
|
const agent = adapter.AGENT;
|
|
832
822
|
const filing = adapter.PRE_TOOL_FILING;
|
|
833
823
|
assert.ok(
|
|
834
|
-
|
|
835
|
-
`${agent}: adapter declares no
|
|
824
|
+
isPlainObject(filing),
|
|
825
|
+
`${agent}: adapter declares no PRE_TOOL_FILING — every adapter needs one: {} when its host files every model call as asked`,
|
|
836
826
|
);
|
|
837
|
-
for (const [name, rule] of Object.entries(filing))
|
|
838
|
-
if (rule === null) continue;
|
|
839
|
-
const where = `${agent}: PRE_TOOL_FILING[${JSON.stringify(name)}]`;
|
|
840
|
-
assert.ok(Object.isFrozen(rule), `${where} is not frozen`);
|
|
827
|
+
for (const [name, rule] of Object.entries(filing))
|
|
841
828
|
assert.ok(
|
|
842
|
-
isFilingRule(rule),
|
|
843
|
-
`${
|
|
829
|
+
rule === null || isFilingRule(rule),
|
|
830
|
+
`${agent}: PRE_TOOL_FILING[${JSON.stringify(name)}] is ${JSON.stringify(rule)}, not null or ${FILING_RULE_GRAMMAR}`,
|
|
844
831
|
);
|
|
845
|
-
if (rule.input !== WHOLE_ARGUMENTS)
|
|
846
|
-
assert.ok(Object.isFrozen(rule.input), `${where}.input is not frozen`);
|
|
847
|
-
}
|
|
848
832
|
const response = adapter.PRE_TOOL_RESPONSE;
|
|
849
833
|
assert.ok(
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
Object.isFrozen(response),
|
|
853
|
-
`${agent}: adapter declares no frozen PRE_TOOL_RESPONSE — every adapter needs one naming the top-level and hookSpecificOutput keys its host reads on the pre-tool event (empty lists when it reads no body)`,
|
|
834
|
+
isPlainObject(response),
|
|
835
|
+
`${agent}: adapter declares no PRE_TOOL_RESPONSE — every adapter needs one naming the top-level and hookSpecificOutput keys its host reads on the pre-tool event (empty lists when it reads no body)`,
|
|
854
836
|
);
|
|
855
837
|
for (const level of PRE_TOOL_RESPONSE_LEVELS) {
|
|
856
|
-
const keys =
|
|
838
|
+
const keys = lookup(response, level.keys);
|
|
857
839
|
assert.ok(
|
|
858
|
-
Array.isArray(keys) &&
|
|
859
|
-
`${agent}: PRE_TOOL_RESPONSE.${level.keys} is not
|
|
860
|
-
);
|
|
861
|
-
assert.deepEqual(
|
|
862
|
-
[...keys],
|
|
863
|
-
[...new Set(keys)].sort(),
|
|
864
|
-
`${agent}: PRE_TOOL_RESPONSE.${level.keys} must be sorted and duplicate-free`,
|
|
840
|
+
Array.isArray(keys) && keys.every((key) => typeof key === "string"),
|
|
841
|
+
`${agent}: PRE_TOOL_RESPONSE.${level.keys} is not an array of strings`,
|
|
865
842
|
);
|
|
866
843
|
assert.ok(
|
|
867
844
|
[true, false, null].includes(
|
package/src/control-plane.mjs
CHANGED
|
@@ -496,9 +496,6 @@ export const UNRENDERED_ON_UNKNOWN = readonlySet(VERDICT_CONTENT_FIELDS);
|
|
|
496
496
|
/** The {@link PreToolFiling} input reference to a call's whole arguments value. */
|
|
497
497
|
export const WHOLE_ARGUMENTS = "$";
|
|
498
498
|
|
|
499
|
-
/** The prefix of a {@link PreToolFiling} input reference to one top-level argument. */
|
|
500
|
-
export const ARGUMENT_KEY_PREFIX = "$.";
|
|
501
|
-
|
|
502
499
|
/**
|
|
503
500
|
* How a host's pre-tool hook files a model's tool call, keyed by MODEL tool
|
|
504
501
|
* name, the {@link flatToolName} of the call's `namespace` and `name`. Each
|
|
@@ -545,7 +542,7 @@ export function flatToolName(namespace, name) {
|
|
|
545
542
|
|
|
546
543
|
/**
|
|
547
544
|
* What a host reads from an exit-0 answer to its pre-tool event (`PreToolUse`,
|
|
548
|
-
* `BeforeTool`), as two
|
|
545
|
+
* `BeforeTool`), as two key lists, each empty when the host reads no
|
|
549
546
|
* body: `hook_specific_output_keys` inside `hookSpecificOutput`, and
|
|
550
547
|
* `top_level_keys` at the body's top level. Each list has a fail-open bit,
|
|
551
548
|
* `unread_key_fails_open` and `unread_top_level_key_fails_open`: `true` ⇒ any
|
|
@@ -555,79 +552,41 @@ export function flatToolName(namespace, name) {
|
|
|
555
552
|
*/
|
|
556
553
|
|
|
557
554
|
/**
|
|
558
|
-
* Whether `
|
|
559
|
-
*
|
|
560
|
-
* @
|
|
561
|
-
* @returns {boolean}
|
|
555
|
+
* Whether `value` is a plain object: not null, not an array.
|
|
556
|
+
* @param {unknown} value
|
|
557
|
+
* @returns {value is Record<string, unknown>}
|
|
562
558
|
*/
|
|
563
|
-
function
|
|
564
|
-
|
|
565
|
-
if (typeof ref !== "string" || !ref.startsWith(ARGUMENT_KEY_PREFIX))
|
|
566
|
-
return false;
|
|
567
|
-
const key = ref.slice(ARGUMENT_KEY_PREFIX.length);
|
|
568
|
-
return key !== "" && !key.includes(".");
|
|
559
|
+
export function isPlainObject(value) {
|
|
560
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
569
561
|
}
|
|
570
562
|
|
|
571
|
-
/**
|
|
572
|
-
|
|
573
|
-
* object (not an array) whose every value is `"$"` or `"$.<key>"`.
|
|
574
|
-
* @param {unknown} input
|
|
575
|
-
* @returns {boolean}
|
|
576
|
-
*/
|
|
577
|
-
function isFilingInput(input) {
|
|
578
|
-
if (input === WHOLE_ARGUMENTS) return true;
|
|
579
|
-
return (
|
|
580
|
-
typeof input === "object" &&
|
|
581
|
-
input !== null &&
|
|
582
|
-
!Array.isArray(input) &&
|
|
583
|
-
Object.values(input).every(isFilingRef)
|
|
584
|
-
);
|
|
585
|
-
}
|
|
563
|
+
/** A {@link PreToolFiling} reference to one top-level argument; group 1 is the key. */
|
|
564
|
+
const ARGUMENT_REF = /^\$\.([^.]+)$/u;
|
|
586
565
|
|
|
587
566
|
/** What a {@link PreToolFiling} rule must be, for error messages. */
|
|
588
|
-
export const FILING_RULE_GRAMMAR = `{tool_name: <non-empty string>, input: "${WHOLE_ARGUMENTS}" or an object of "${WHOLE_ARGUMENTS}"/"
|
|
567
|
+
export const FILING_RULE_GRAMMAR = `{tool_name: <non-empty string>, input: "${WHOLE_ARGUMENTS}" or an object of "${WHOLE_ARGUMENTS}"/"$.<key>" references (<key>: non-empty, no ".")}`;
|
|
589
568
|
|
|
590
569
|
/**
|
|
591
|
-
* Whether `rule` is a non-null {@link PreToolFiling} rule: a
|
|
592
|
-
*
|
|
593
|
-
* grammar both {@link fileModelCall} and the
|
|
570
|
+
* Whether `rule` is a non-null {@link PreToolFiling} rule: exactly a non-empty
|
|
571
|
+
* string `tool_name` and an `input` that is `"$"` or an object of `"$"` /
|
|
572
|
+
* `"$.<key>"` references. The one grammar both {@link fileModelCall} and the
|
|
573
|
+
* conformance harness hold a rule to.
|
|
594
574
|
* @param {unknown} rule
|
|
595
575
|
* @returns {boolean}
|
|
596
576
|
*/
|
|
597
577
|
export function isFilingRule(rule) {
|
|
598
|
-
if (
|
|
599
|
-
|
|
600
|
-
const
|
|
601
|
-
|
|
578
|
+
if (!isPlainObject(rule)) return false;
|
|
579
|
+
const { tool_name, input, ...extra } = rule;
|
|
580
|
+
const isRef = (/** @type {unknown} */ ref) =>
|
|
581
|
+
ref === WHOLE_ARGUMENTS ||
|
|
582
|
+
(typeof ref === "string" && ARGUMENT_REF.test(ref));
|
|
602
583
|
return (
|
|
603
|
-
keys.length ===
|
|
604
|
-
keys[0] === "input" &&
|
|
605
|
-
keys[1] === "tool_name" &&
|
|
584
|
+
Object.keys(extra).length === 0 &&
|
|
606
585
|
typeof tool_name === "string" &&
|
|
607
586
|
tool_name !== "" &&
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/** The kinds of model tool call a {@link PreToolFiling} distinguishes. */
|
|
613
|
-
export const MODEL_CALL_KINDS = readonlySet(["function", "freeform"]);
|
|
614
|
-
|
|
615
|
-
/**
|
|
616
|
-
* Resolve one valid {@link PreToolFiling} input reference against `args`;
|
|
617
|
-
* `undefined` when it does not resolve (see {@link PreToolFiling}).
|
|
618
|
-
* @param {string} ref
|
|
619
|
-
* @param {unknown} args
|
|
620
|
-
* @returns {unknown}
|
|
621
|
-
*/
|
|
622
|
-
function resolveArgument(ref, args) {
|
|
623
|
-
if (ref === WHOLE_ARGUMENTS) return args;
|
|
624
|
-
if (typeof args !== "object" || args === null || Array.isArray(args))
|
|
625
|
-
return undefined;
|
|
626
|
-
const value = lookup(
|
|
627
|
-
/** @type {Record<string, unknown>} */ (args),
|
|
628
|
-
ref.slice(ARGUMENT_KEY_PREFIX.length),
|
|
587
|
+
(input === WHOLE_ARGUMENTS ||
|
|
588
|
+
(isPlainObject(input) && Object.values(input).every(isRef)))
|
|
629
589
|
);
|
|
630
|
-
return value === null ? undefined : value;
|
|
631
590
|
}
|
|
632
591
|
|
|
633
592
|
/**
|
|
@@ -635,7 +594,7 @@ function resolveArgument(ref, args) {
|
|
|
635
594
|
* with arguments value `args`, per that host's {@link PreToolFiling}; `null`
|
|
636
595
|
* when the hook files none. The reference reading of the mini-language
|
|
637
596
|
* `agents.json` publishes as `pre_tool_filing`. Throws on a rule outside that
|
|
638
|
-
* grammar or a kind
|
|
597
|
+
* grammar or a kind other than `"function"` or `"freeform"`.
|
|
639
598
|
* @param {PreToolFiling} filing
|
|
640
599
|
* @param {"function" | "freeform"} kind whether the model made a function call or a freeform (custom) call
|
|
641
600
|
* @param {string} name the model tool name, a {@link flatToolName}
|
|
@@ -643,7 +602,7 @@ function resolveArgument(ref, args) {
|
|
|
643
602
|
* @returns {{ tool_name: string, tool_input: unknown } | null}
|
|
644
603
|
*/
|
|
645
604
|
export function fileModelCall(filing, kind, name, args) {
|
|
646
|
-
if (
|
|
605
|
+
if (kind !== "function" && kind !== "freeform")
|
|
647
606
|
throw new Error(
|
|
648
607
|
`control-plane: model call kind ${JSON.stringify(kind)} is neither "function" nor "freeform"`,
|
|
649
608
|
);
|
|
@@ -657,10 +616,13 @@ export function fileModelCall(filing, kind, name, args) {
|
|
|
657
616
|
);
|
|
658
617
|
if (rule.input === WHOLE_ARGUMENTS)
|
|
659
618
|
return { tool_name: rule.tool_name, tool_input: args };
|
|
660
|
-
const entries = Object.entries(rule.input).map(([key, ref]) =>
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
619
|
+
const entries = Object.entries(rule.input).map(([key, ref]) => {
|
|
620
|
+
const argument = ARGUMENT_REF.exec(ref)?.[1];
|
|
621
|
+
if (argument === undefined) return [key, args];
|
|
622
|
+
// A missing key, a null value, or non-object arguments all fail to resolve.
|
|
623
|
+
const value = isPlainObject(args) ? lookup(args, argument) : undefined;
|
|
624
|
+
return [key, value === null ? undefined : value];
|
|
625
|
+
});
|
|
664
626
|
if (entries.some(([, value]) => value === undefined)) return null;
|
|
665
627
|
// fromEntries defines own properties, so an input key `__proto__` stays a
|
|
666
628
|
// key, as it is in a Python consumer's dict, instead of hitting the setter.
|
|
@@ -135,9 +135,26 @@ export function readonlySet(values: Iterable<string>): ReadonlySet<string>;
|
|
|
135
135
|
*/
|
|
136
136
|
export function flatToolName(namespace: string | null | undefined, name: string): string;
|
|
137
137
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
138
|
+
* What a host reads from an exit-0 answer to its pre-tool event (`PreToolUse`,
|
|
139
|
+
* `BeforeTool`), as two key lists, each empty when the host reads no
|
|
140
|
+
* body: `hook_specific_output_keys` inside `hookSpecificOutput`, and
|
|
141
|
+
* `top_level_keys` at the body's top level. Each list has a fail-open bit,
|
|
142
|
+
* `unread_key_fails_open` and `unread_top_level_key_fails_open`: `true` ⇒ any
|
|
143
|
+
* other key there voids the whole body and the call RUNS; `false` ⇒ other keys
|
|
144
|
+
* are ignored; `null` ⇒ not established from the host's source.
|
|
145
|
+
* @typedef {Readonly<{ hook_specific_output_keys: readonly string[], unread_key_fails_open: boolean | null, top_level_keys: readonly string[], unread_top_level_key_fails_open: boolean | null }>} PreToolResponse
|
|
146
|
+
*/
|
|
147
|
+
/**
|
|
148
|
+
* Whether `value` is a plain object: not null, not an array.
|
|
149
|
+
* @param {unknown} value
|
|
150
|
+
* @returns {value is Record<string, unknown>}
|
|
151
|
+
*/
|
|
152
|
+
export function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
153
|
+
/**
|
|
154
|
+
* Whether `rule` is a non-null {@link PreToolFiling} rule: exactly a non-empty
|
|
155
|
+
* string `tool_name` and an `input` that is `"$"` or an object of `"$"` /
|
|
156
|
+
* `"$.<key>"` references. The one grammar both {@link fileModelCall} and the
|
|
157
|
+
* conformance harness hold a rule to.
|
|
141
158
|
* @param {unknown} rule
|
|
142
159
|
* @returns {boolean}
|
|
143
160
|
*/
|
|
@@ -147,7 +164,7 @@ export function isFilingRule(rule: unknown): boolean;
|
|
|
147
164
|
* with arguments value `args`, per that host's {@link PreToolFiling}; `null`
|
|
148
165
|
* when the hook files none. The reference reading of the mini-language
|
|
149
166
|
* `agents.json` publishes as `pre_tool_filing`. Throws on a rule outside that
|
|
150
|
-
* grammar or a kind
|
|
167
|
+
* grammar or a kind other than `"function"` or `"freeform"`.
|
|
151
168
|
* @param {PreToolFiling} filing
|
|
152
169
|
* @param {"function" | "freeform"} kind whether the model made a function call or a freeform (custom) call
|
|
153
170
|
* @param {string} name the model tool name, a {@link flatToolName}
|
|
@@ -582,12 +599,8 @@ export const VERDICT_CONTENT_FIELDS: readonly string[];
|
|
|
582
599
|
export const UNRENDERED_ON_UNKNOWN: ReadonlySet<string>;
|
|
583
600
|
/** The {@link PreToolFiling} input reference to a call's whole arguments value. */
|
|
584
601
|
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
602
|
/** What a {@link PreToolFiling} rule must be, for error messages. */
|
|
588
603
|
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>;
|
|
591
604
|
/**
|
|
592
605
|
* PROBLEM CLASS — an event marked vetoable on a path where the host can never
|
|
593
606
|
* veto it, so a deny reads downstream as a block that never happened.
|
|
@@ -648,7 +661,7 @@ export const BLOCK_TRANSPORT: Readonly<Record<string, Readonly<{
|
|
|
648
661
|
}>>>;
|
|
649
662
|
/**
|
|
650
663
|
* What a host reads from an exit-0 answer to its pre-tool event (`PreToolUse`,
|
|
651
|
-
* `BeforeTool`), as two
|
|
664
|
+
* `BeforeTool`), as two key lists, each empty when the host reads no
|
|
652
665
|
* body: `hook_specific_output_keys` inside `hookSpecificOutput`, and
|
|
653
666
|
* `top_level_keys` at the body's top level. Each list has a fail-open bit,
|
|
654
667
|
* `unread_key_fails_open` and `unread_top_level_key_fails_open`: `true` ⇒ any
|