agent-control-plane-core 0.6.4 → 0.8.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 +69 -1
- package/bin/amp-hook.mjs +1 -1
- package/bin/claude-hook.mjs +1 -1
- package/bin/codex-hook.mjs +1 -1
- package/bin/gemini-hook.mjs +1 -1
- package/package.json +13 -2
- package/schema/control-plane/v1/tool-call-event.schema.json +152 -0
- package/schema/control-plane/v1/tool-input-keys.json +7 -0
- package/schema/control-plane/v1/verdict.schema.json +34 -0
- package/src/adapters/amp.mjs +54 -25
- package/src/adapters/claude.mjs +24 -2
- package/src/adapters/codex.mjs +25 -3
- package/src/adapters/gemini.mjs +11 -0
- package/src/conformance.mjs +156 -21
- package/src/control-plane.mjs +10 -6
- package/src/fixtures/claude.json +4 -2
- package/src/fixtures/codex.json +8 -6
- package/src/json-schema.mjs +331 -0
- package/{bin/hook-runtime.mjs → src/runtime.mjs} +62 -27
- package/types/adapters/amp.d.mts +32 -0
- package/types/adapters/claude.d.mts +11 -1
- package/types/adapters/codex.d.mts +11 -0
- package/types/adapters/gemini.d.mts +9 -0
- package/types/conformance.d.mts +22 -1
- package/types/control-plane.d.mts +14 -6
- package/types/json-schema.d.mts +77 -0
- package/types/runtime.d.mts +79 -0
package/src/conformance.mjs
CHANGED
|
@@ -22,6 +22,22 @@ const UNENFORCEABLE_DENY_PROBE = Object.freeze({
|
|
|
22
22
|
});
|
|
23
23
|
const ABSTAINING_ALLOW_PROBE = Object.freeze({ decision: Decision.ALLOW });
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* The two probe verdicts rule ⑪ renders against the same event, to read whether
|
|
27
|
+
* this host's transport carries an ask tier of its own. They share one `reason`
|
|
28
|
+
* on purpose: the rule compares the two RENDERS, and a reason that differed
|
|
29
|
+
* would make them differ for a reason that is not the decision.
|
|
30
|
+
*/
|
|
31
|
+
const ASK_TIER_REASON = "conformance probe: ask tier";
|
|
32
|
+
const ASK_TIER_ASK_PROBE = Object.freeze({
|
|
33
|
+
decision: Decision.ASK,
|
|
34
|
+
reason: ASK_TIER_REASON,
|
|
35
|
+
});
|
|
36
|
+
const ASK_TIER_DENY_PROBE = Object.freeze({
|
|
37
|
+
decision: Decision.DENY,
|
|
38
|
+
reason: ASK_TIER_REASON,
|
|
39
|
+
});
|
|
40
|
+
|
|
25
41
|
/**
|
|
26
42
|
* The payload rule ⑨ drifts an adapter with. No host emits that event name, so
|
|
27
43
|
* an adapter that maps event names must answer {@link EventKind.UNKNOWN}.
|
|
@@ -202,17 +218,23 @@ const CONTENT_PROBE_VALUES = Object.freeze({
|
|
|
202
218
|
]),
|
|
203
219
|
});
|
|
204
220
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
)
|
|
221
|
+
/**
|
|
222
|
+
* A field added to VERDICT_CONTENT_FIELDS with no probe value would be dropped
|
|
223
|
+
* by normalizeVerdict and then reported as "reaches no native channel" against
|
|
224
|
+
* every adapter, naming the adapter for the harness's own omission. Called at
|
|
225
|
+
* import against the live {@link CONTENT_PROBE_VALUES}; exported so a test can
|
|
226
|
+
* drive both branches.
|
|
227
|
+
* @param {readonly string[]} fields
|
|
228
|
+
* @param {Record<string, readonly unknown[]>} values
|
|
229
|
+
*/
|
|
230
|
+
export function assertContentProbeValuesComplete(fields, values) {
|
|
231
|
+
if (fields.some((field) => (lookup(values, field) ?? []).length < 2))
|
|
232
|
+
throw new Error(
|
|
233
|
+
`conformance: CONTENT_PROBE_VALUES needs two or more values for each of ${fields.join(", ")}`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
assertContentProbeValuesComplete(VERDICT_CONTENT_FIELDS, CONTENT_PROBE_VALUES);
|
|
216
238
|
|
|
217
239
|
/**
|
|
218
240
|
* Rule ⑩'s first half: the adapter declares a row for EVERY {@link EventKind}.
|
|
@@ -454,6 +476,78 @@ function coherentEvent(adapter, byKind, kind) {
|
|
|
454
476
|
};
|
|
455
477
|
}
|
|
456
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Rule ⑪, for one pre-tool event: the adapter's `NATIVE_ASK_TIER` declaration
|
|
481
|
+
* agrees with what `render` does with an `ask`.
|
|
482
|
+
*
|
|
483
|
+
* The declaration is what lets a consumer stop keeping a hand-typed list of
|
|
484
|
+
* which hosts honour an ask. A consumer that must not let an ask through
|
|
485
|
+
* escalates it to a deny on a host with no ask tier — and applying that
|
|
486
|
+
* uniformly turns every ask into a deny on the hosts that DO have one. So the
|
|
487
|
+
* declaration has to be checked against the render rather than believed.
|
|
488
|
+
*
|
|
489
|
+
* Probed on a NON-vetoable variant of the event, for rule ⑧'s reason and one
|
|
490
|
+
* more: it makes the deny comparison below an ADVISORY deny, which is the
|
|
491
|
+
* signal a host with no ask tier has left to spend on an ask.
|
|
492
|
+
*
|
|
493
|
+
* What a render can show, and what it cannot. Whether a host suspends the call
|
|
494
|
+
* for a human lives in the HOST, so no probe reads it off a render. What a probe
|
|
495
|
+
* does read is each direction's tell:
|
|
496
|
+
*
|
|
497
|
+
* - Declared `true`, rendered as the abstaining ALLOW: the consumer is told the
|
|
498
|
+
* human will see this call, and the transport carries nothing that says so —
|
|
499
|
+
* the tool just runs. That is the lie this rule exists to catch.
|
|
500
|
+
* - Declared `false`, rendered as something the adapter's own advisory deny is
|
|
501
|
+
* not: the render invents a third signal on a transport the declaration says
|
|
502
|
+
* has only two, so one of the two is wrong.
|
|
503
|
+
*
|
|
504
|
+
* An OBSERVE_ONLY render is exempt, as in rule ⑧: that transport has no
|
|
505
|
+
* pre-emption channel at all, so its ask, deny and allow coincide by
|
|
506
|
+
* construction and demanding a distinct ask would demand a fiction.
|
|
507
|
+
* @param {import("./control-plane.mjs").Adapter} adapter
|
|
508
|
+
* @param {import("./control-plane.mjs").ToolCallEvent} event a non-vetoable pre-tool event
|
|
509
|
+
* @param {import("./control-plane.mjs").ToolCallEvent} vetoable the fixture's own pre-tool event, whose veto the block half needs
|
|
510
|
+
* @param {string} caseName
|
|
511
|
+
* @param {any} assert
|
|
512
|
+
*/
|
|
513
|
+
function assertAskTier(adapter, event, vetoable, caseName, assert) {
|
|
514
|
+
const asked = adapter.render(ASK_TIER_ASK_PROBE, event);
|
|
515
|
+
assert.equal(
|
|
516
|
+
asked.enforced,
|
|
517
|
+
false,
|
|
518
|
+
`an ask rendered as an enforced block: ${caseName}`,
|
|
519
|
+
);
|
|
520
|
+
if (asked.transport === IntegrationMode.OBSERVE_ONLY) return;
|
|
521
|
+
if (adapter.NATIVE_ASK_TIER) {
|
|
522
|
+
assert.notDeepEqual(
|
|
523
|
+
asked,
|
|
524
|
+
adapter.render(ABSTAINING_ALLOW_PROBE, event),
|
|
525
|
+
`${adapter.AGENT}: NATIVE_ASK_TIER is true, but an ask renders exactly as this host's abstaining allow — nothing in the transport asks anyone: ${caseName}`,
|
|
526
|
+
);
|
|
527
|
+
// The other way to declare an ask tier this host has not got: render the ask
|
|
528
|
+
// as the host's BLOCK. It differs from the allow, so the half above passes,
|
|
529
|
+
// and the consumer it tells to stop escalating then hands a call to a host
|
|
530
|
+
// that denies it outright rather than suspending it for a human. Probed on
|
|
531
|
+
// the vetoable event, because a block is the one signal a non-vetoable
|
|
532
|
+
// render cannot carry.
|
|
533
|
+
if (vetoable.this_call_vetoable) {
|
|
534
|
+
const blocked = adapter.render(ASK_TIER_DENY_PROBE, vetoable);
|
|
535
|
+
if (blocked.enforced)
|
|
536
|
+
assert.notDeepEqual(
|
|
537
|
+
adapter.render(ASK_TIER_ASK_PROBE, vetoable),
|
|
538
|
+
blocked,
|
|
539
|
+
`${adapter.AGENT}: NATIVE_ASK_TIER is true, but an ask renders exactly as this host's enforced deny — the call is blocked, not suspended: ${caseName}`,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
assert.deepEqual(
|
|
545
|
+
asked,
|
|
546
|
+
adapter.render(ASK_TIER_DENY_PROBE, event),
|
|
547
|
+
`${adapter.AGENT}: NATIVE_ASK_TIER is false, but an ask renders differently from this host's advisory deny — the render carries an ask signal the declaration says the host has not got: ${caseName}`,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
457
551
|
/**
|
|
458
552
|
* Rule ⑩, for one parsed event: every {@link VERDICT_CONTENT_FIELDS} entry
|
|
459
553
|
* either reaches this host's wire or is DECLARED unreachable in the adapter's
|
|
@@ -513,8 +607,13 @@ function assertContentChannels(adapter, event, caseName, assert, seen) {
|
|
|
513
607
|
// nowhere to hide.
|
|
514
608
|
/** @param {string} field @param {Record<string, unknown>} held @param {string} shape */
|
|
515
609
|
const carries = (field, held, shape) => {
|
|
516
|
-
|
|
517
|
-
|
|
610
|
+
// `field` is always a VERDICT_CONTENT_FIELDS member (the only caller below
|
|
611
|
+
// iterates it), and assertContentProbeValuesComplete guarantees at import
|
|
612
|
+
// that every such member has two or more entries here — so neither a
|
|
613
|
+
// missing row nor a short one can reach this point.
|
|
614
|
+
const values = /** @type {readonly unknown[]} */ (
|
|
615
|
+
lookup(CONTENT_PROBE_VALUES, field)
|
|
616
|
+
);
|
|
518
617
|
const renders = values.map((value) =>
|
|
519
618
|
adapter.render(
|
|
520
619
|
{ decision: Decision.ALLOW, ...held, [field]: value },
|
|
@@ -592,7 +691,11 @@ function assertContentChannels(adapter, event, caseName, assert, seen) {
|
|
|
592
691
|
const held = others.reduce(
|
|
593
692
|
(combinations, name) =>
|
|
594
693
|
combinations.flatMap((carried) =>
|
|
595
|
-
|
|
694
|
+
// Same guarantee as `carries` above: `name` is a VERDICT_CONTENT_FIELDS
|
|
695
|
+
// member, so the row is always present.
|
|
696
|
+
/** @type {readonly unknown[]} */ (
|
|
697
|
+
lookup(CONTENT_PROBE_VALUES, name)
|
|
698
|
+
).map((value) => ({
|
|
596
699
|
...carried,
|
|
597
700
|
[name]: value,
|
|
598
701
|
})),
|
|
@@ -820,13 +923,23 @@ function assertAliasedInput(agent, caseName, event, assert) {
|
|
|
820
923
|
* that DID reach a wire come back as `contentFieldsSeen`; a suite covering
|
|
821
924
|
* several adapters asserts their union to keep the positive half of the
|
|
822
925
|
* rule non-vacuous, since no single host has a channel for all three.
|
|
926
|
+
* 11. ask-tier honesty: the adapter declares whether this host carries a
|
|
927
|
+
* distinct "ask the human" tier it honours (`NATIVE_ASK_TIER`), and the
|
|
928
|
+
* render is held to that declaration on every pre-tool event. A declared
|
|
929
|
+
* tier whose `ask` renders as the host's abstaining allow fails — the
|
|
930
|
+
* consumer is told a human will see the call while the tool just runs —
|
|
931
|
+
* and so does a declared-absent tier whose `ask` renders as anything but
|
|
932
|
+
* the adapter's own advisory deny. A consumer reads the declaration to
|
|
933
|
+
* decide where an `ask` must be escalated to a `deny`, so a wrong one
|
|
934
|
+
* either lets a call through or denies every asked call on the hosts that
|
|
935
|
+
* do ask. OBSERVE_ONLY renders are exempt, as in rule ⑧.
|
|
823
936
|
*
|
|
824
937
|
* `assert` is injected (node:assert/strict) so the harness stays test-framework
|
|
825
938
|
* neutral; it throws on the first mismatch. Returns a summary the caller can
|
|
826
939
|
* assert further on.
|
|
827
940
|
*
|
|
828
941
|
* @param {{ adapter: import("./control-plane.mjs").Adapter, fixtures: any, assert: any }} args
|
|
829
|
-
* @returns {{ cases: number, renders: number, decisionsSeen: Set<string>, mutationSeen: boolean, contentFieldsSeen: Set<string>, enforcedDenySeen: boolean, vetoableDenySeen: boolean, unknownKindSeen: boolean, coverageClassesChecked: Set<string>, unenforceableDenyChecks: number }}
|
|
942
|
+
* @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 }}
|
|
830
943
|
*/
|
|
831
944
|
export function runAdapterConformance({ adapter, fixtures, assert }) {
|
|
832
945
|
assert.equal(
|
|
@@ -837,6 +950,14 @@ export function runAdapterConformance({ adapter, fixtures, assert }) {
|
|
|
837
950
|
|
|
838
951
|
assertCoverageWellFormed(adapter, assert);
|
|
839
952
|
assertEveryKindHasARow(adapter, assert);
|
|
953
|
+
// Named here rather than left to a `TypeError` inside rule ⑪: a third-party
|
|
954
|
+
// adapter written against the earlier contract has no such member, and its
|
|
955
|
+
// author is who reads this.
|
|
956
|
+
assert.equal(
|
|
957
|
+
typeof adapter.NATIVE_ASK_TIER,
|
|
958
|
+
"boolean",
|
|
959
|
+
`${adapter.AGENT}: adapter declares no NATIVE_ASK_TIER — every adapter needs a boolean saying whether this host honours a distinct "ask the human" tier (false when render collapses ask onto another signal)`,
|
|
960
|
+
);
|
|
840
961
|
|
|
841
962
|
/** @type {Set<string>} */
|
|
842
963
|
const decisionsSeen = new Set();
|
|
@@ -852,6 +973,7 @@ export function runAdapterConformance({ adapter, fixtures, assert }) {
|
|
|
852
973
|
const parsedByKind = {};
|
|
853
974
|
let preToolCases = 0;
|
|
854
975
|
let unenforceableDenyChecks = 0;
|
|
976
|
+
let askTierChecks = 0;
|
|
855
977
|
let renders = 0;
|
|
856
978
|
|
|
857
979
|
for (const testCase of fixtures.cases) {
|
|
@@ -991,12 +1113,17 @@ export function runAdapterConformance({ adapter, fixtures, assert }) {
|
|
|
991
1113
|
// An OBSERVE_ONLY render is exempt from the ≠-allow half: that transport has
|
|
992
1114
|
// no pre-emption channel at all, so its deny and allow are identical by
|
|
993
1115
|
// construction. Demanding a distinct signal there would demand a fiction.
|
|
994
|
-
if (deniedRender.transport
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1116
|
+
if (deniedRender.transport !== IntegrationMode.OBSERVE_ONLY)
|
|
1117
|
+
assert.notDeepEqual(
|
|
1118
|
+
deniedRender,
|
|
1119
|
+
adapter.render(ABSTAINING_ALLOW_PROBE, unenforceable),
|
|
1120
|
+
`unenforceable deny renders identically to an abstaining allow — the objection is lost: ${testCase.name}`,
|
|
1121
|
+
);
|
|
1122
|
+
// Rule ⑪, on the same non-vetoable variant and AFTER rule ⑧: an adapter that
|
|
1123
|
+
// collapses both its deny and its ask onto the host's allow is reported as
|
|
1124
|
+
// the lost objection it is, not as a mis-declared ask tier.
|
|
1125
|
+
assertAskTier(adapter, unenforceable, parsed, testCase.name, assert);
|
|
1126
|
+
askTierChecks += 1;
|
|
1000
1127
|
}
|
|
1001
1128
|
|
|
1002
1129
|
for (const decision of ["allow", "deny", "ask"]) {
|
|
@@ -1055,6 +1182,13 @@ export function runAdapterConformance({ adapter, fixtures, assert }) {
|
|
|
1055
1182
|
preToolCases,
|
|
1056
1183
|
`rule ⑧ (unenforceable deny ≠ allow) ran on ${unenforceableDenyChecks} of ${preToolCases} pre-tool cases`,
|
|
1057
1184
|
);
|
|
1185
|
+
// Same reason as the count above: the rule is in the same loop, so a stray
|
|
1186
|
+
// `continue` would skip it silently on the later cases.
|
|
1187
|
+
assert.equal(
|
|
1188
|
+
askTierChecks,
|
|
1189
|
+
preToolCases,
|
|
1190
|
+
`rule ⑪ (ask-tier declaration) ran on ${askTierChecks} of ${preToolCases} pre-tool cases`,
|
|
1191
|
+
);
|
|
1058
1192
|
|
|
1059
1193
|
return {
|
|
1060
1194
|
cases: fixtures.cases.length,
|
|
@@ -1067,5 +1201,6 @@ export function runAdapterConformance({ adapter, fixtures, assert }) {
|
|
|
1067
1201
|
unknownKindSeen,
|
|
1068
1202
|
coverageClassesChecked,
|
|
1069
1203
|
unenforceableDenyChecks,
|
|
1204
|
+
askTierChecks,
|
|
1070
1205
|
};
|
|
1071
1206
|
}
|
package/src/control-plane.mjs
CHANGED
|
@@ -23,9 +23,11 @@
|
|
|
23
23
|
* Dependency-free on purpose, so a fail-closed hook can import it without
|
|
24
24
|
* dragging in eager config-file reads.
|
|
25
25
|
*
|
|
26
|
-
* VERSIONING — this module IS the frozen contract
|
|
27
|
-
*
|
|
28
|
-
* in
|
|
26
|
+
* VERSIONING — this module IS the frozen contract and its own SSOT. The JSON
|
|
27
|
+
* Schema documents under `schema/` are GENERATED from it by `pnpm gen:schema`
|
|
28
|
+
* and byte-compared in `json-schema.test.mjs`, so they are output rather than a
|
|
29
|
+
* second contract; never hand-edit one. Adapters and guardrail consumers are
|
|
30
|
+
* built against this module in parallel, so its shapes are stable: {@link EventKind}, {@link Decision},
|
|
29
31
|
* {@link MODELED_TOOLS}, and {@link TOOL_ALIASES} are frozen, and
|
|
30
32
|
* {@link SCHEMA_VERSION} / {@link CONTROL_PLANE_SCHEMA} are pinned
|
|
31
33
|
* (control-plane.test.mjs asserts the exact values, so any shape change is a
|
|
@@ -38,9 +40,10 @@
|
|
|
38
40
|
* {@link Adapter} interface an integrator implements. A new REQUIRED adapter
|
|
39
41
|
* member breaks every third-party adapter while every event on the wire stays
|
|
40
42
|
* byte-identical, so it is a package-semver break and leaves
|
|
41
|
-
* {@link CONTROL_PLANE_SCHEMA} alone. `UNRENDERED_FIELDS` is one
|
|
42
|
-
*
|
|
43
|
-
* and the
|
|
43
|
+
* {@link CONTROL_PLANE_SCHEMA} alone. `UNRENDERED_FIELDS` is one and
|
|
44
|
+
* `NATIVE_ASK_TIER` is the second: adding either is a one-line change per
|
|
45
|
+
* adapter, and the conformance harness names the member and the migration
|
|
46
|
+
* rather than throwing from inside itself.
|
|
44
47
|
*/
|
|
45
48
|
|
|
46
49
|
/** Wire identifier for this schema version; bump on a breaking shape change. */
|
|
@@ -501,6 +504,7 @@ export const UNRENDERED_ON_UNKNOWN = readonlySet(VERDICT_CONTENT_FIELDS);
|
|
|
501
504
|
* @property {(native: any) => ToolCallEvent} parse
|
|
502
505
|
* @property {(verdict: Verdict, event: ToolCallEvent, options?: { soleGate?: boolean }) => NativeResponse} render
|
|
503
506
|
* @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`.
|
|
507
|
+
* @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.
|
|
504
508
|
* @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.
|
|
505
509
|
*/
|
|
506
510
|
|
package/src/fixtures/claude.json
CHANGED
|
@@ -67,7 +67,8 @@
|
|
|
67
67
|
"permissionDecision": "deny",
|
|
68
68
|
"permissionDecisionReason": "blocked rm"
|
|
69
69
|
}
|
|
70
|
-
}
|
|
70
|
+
},
|
|
71
|
+
"stderr": "blocked rm"
|
|
71
72
|
}
|
|
72
73
|
},
|
|
73
74
|
"ask": {
|
|
@@ -244,7 +245,8 @@
|
|
|
244
245
|
},
|
|
245
246
|
"decision": "block",
|
|
246
247
|
"reason": "leak"
|
|
247
|
-
}
|
|
248
|
+
},
|
|
249
|
+
"stderr": "leak"
|
|
248
250
|
}
|
|
249
251
|
},
|
|
250
252
|
"ask": {
|
package/src/fixtures/codex.json
CHANGED
|
@@ -109,12 +109,13 @@
|
|
|
109
109
|
},
|
|
110
110
|
"native": {
|
|
111
111
|
"transport": "external_hook",
|
|
112
|
-
"exit_code":
|
|
113
|
-
"enforced":
|
|
112
|
+
"exit_code": 2,
|
|
113
|
+
"enforced": true,
|
|
114
114
|
"stdout": {
|
|
115
115
|
"hookSpecificOutput": {
|
|
116
116
|
"hookEventName": "PreToolUse",
|
|
117
|
-
"permissionDecision": "
|
|
117
|
+
"permissionDecision": "deny",
|
|
118
|
+
"permissionDecisionReason": "blocked by monitor"
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
}
|
|
@@ -315,12 +316,13 @@
|
|
|
315
316
|
},
|
|
316
317
|
"native": {
|
|
317
318
|
"transport": "external_hook",
|
|
318
|
-
"exit_code":
|
|
319
|
-
"enforced":
|
|
319
|
+
"exit_code": 2,
|
|
320
|
+
"enforced": true,
|
|
320
321
|
"stdout": {
|
|
321
322
|
"hookSpecificOutput": {
|
|
322
323
|
"hookEventName": "PreToolUse",
|
|
323
|
-
"permissionDecision": "
|
|
324
|
+
"permissionDecision": "deny",
|
|
325
|
+
"permissionDecisionReason": "blocked by monitor"
|
|
324
326
|
}
|
|
325
327
|
}
|
|
326
328
|
}
|