agent-control-plane-core 0.5.0 → 0.5.2
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/amp.mjs +4 -2
- package/src/adapters/claude.mjs +4 -2
- package/src/adapters/codex.mjs +4 -2
- package/src/adapters/gemini.mjs +4 -2
- package/src/conformance.mjs +24 -13
- package/src/control-plane.mjs +10 -0
- package/types/adapters/amp.d.mts +0 -6
- package/types/adapters/claude.d.mts +0 -8
- package/types/adapters/codex.d.mts +0 -7
- package/types/adapters/gemini.d.mts +0 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-control-plane-core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
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/amp.mjs
CHANGED
|
@@ -57,9 +57,11 @@ export const COVERAGE = Object.freeze({
|
|
|
57
57
|
/**
|
|
58
58
|
* The event kinds this host can actually gate. Amp invokes the delegate for a tool call and nothing else, so parse only ever emits PRE_TOOL.
|
|
59
59
|
* A kind absent here parses non-vetoable, so an unmodelled event never renders as
|
|
60
|
-
* an enforced block the host will not perform.
|
|
60
|
+
* an enforced block the host will not perform. Module-private: `Object.freeze` does
|
|
61
|
+
* not stop `Set.add`, so an exported set would let a consumer add UNKNOWN back
|
|
62
|
+
* after `assertGatedKinds` has already run.
|
|
61
63
|
*/
|
|
62
|
-
|
|
64
|
+
const GATED_EVENTS = Object.freeze(new Set([EventKind.PRE_TOOL]));
|
|
63
65
|
assertGatedKinds(GATED_EVENTS, AGENT);
|
|
64
66
|
|
|
65
67
|
// Amp invokes the delegate for a tool call; the payload carries the tool name +
|
package/src/adapters/claude.mjs
CHANGED
|
@@ -63,9 +63,11 @@ export const COVERAGE = Object.freeze({
|
|
|
63
63
|
* not abort a session start, and the matrix documents no host response for it — an
|
|
64
64
|
* undocumented kind is fail-closed to non-vetoable, the same rule coverage uses.
|
|
65
65
|
* A kind absent here parses non-vetoable, so an unmodelled event never renders as
|
|
66
|
-
* an enforced block the host will not perform.
|
|
66
|
+
* an enforced block the host will not perform. Module-private: `Object.freeze` does
|
|
67
|
+
* not stop `Set.add`, so an exported set would let a consumer add UNKNOWN back after
|
|
68
|
+
* `assertGatedKinds` has already run.
|
|
67
69
|
*/
|
|
68
|
-
|
|
70
|
+
const GATED_EVENTS = Object.freeze(
|
|
69
71
|
new Set([EventKind.PRE_TOOL, EventKind.POST_TOOL, EventKind.PROMPT_SUBMIT]),
|
|
70
72
|
);
|
|
71
73
|
assertGatedKinds(GATED_EVENTS, AGENT);
|
package/src/adapters/codex.mjs
CHANGED
|
@@ -59,9 +59,11 @@ export const COVERAGE = Object.freeze({
|
|
|
59
59
|
* The event kinds this host can actually gate. `PreToolUse` is the one event Codex can veto. Every other native event parses as
|
|
60
60
|
* UNKNOWN today, so without this set each of them reported an enforced block.
|
|
61
61
|
* A kind absent here parses non-vetoable, so an unmodelled event never renders as
|
|
62
|
-
* an enforced block the host will not perform.
|
|
62
|
+
* an enforced block the host will not perform. Module-private: `Object.freeze` does
|
|
63
|
+
* not stop `Set.add`, so an exported set would let a consumer add UNKNOWN back
|
|
64
|
+
* after `assertGatedKinds` has already run.
|
|
63
65
|
*/
|
|
64
|
-
|
|
66
|
+
const GATED_EVENTS = Object.freeze(new Set([EventKind.PRE_TOOL]));
|
|
65
67
|
assertGatedKinds(GATED_EVENTS, AGENT);
|
|
66
68
|
|
|
67
69
|
/** Minimum Codex version whose hook can actually veto a tool call. */
|
package/src/adapters/gemini.mjs
CHANGED
|
@@ -76,9 +76,11 @@ export const COVERAGE = Object.freeze({
|
|
|
76
76
|
/**
|
|
77
77
|
* The event kinds this host can actually gate. Gemini CLI honours a deny on each of the three events NATIVE_TO_KIND maps.
|
|
78
78
|
* A kind absent here parses non-vetoable, so an unmodelled event never renders as
|
|
79
|
-
* an enforced block the host will not perform.
|
|
79
|
+
* an enforced block the host will not perform. Module-private: `Object.freeze` does
|
|
80
|
+
* not stop `Set.add`, so an exported set would let a consumer add UNKNOWN back
|
|
81
|
+
* after `assertGatedKinds` has already run.
|
|
80
82
|
*/
|
|
81
|
-
|
|
83
|
+
const GATED_EVENTS = Object.freeze(
|
|
82
84
|
new Set([EventKind.PRE_TOOL, EventKind.POST_TOOL, EventKind.PROMPT_SUBMIT]),
|
|
83
85
|
);
|
|
84
86
|
assertGatedKinds(GATED_EVENTS, AGENT);
|
package/src/conformance.mjs
CHANGED
|
@@ -22,10 +22,19 @@ const UNENFORCEABLE_DENY_PROBE = Object.freeze({
|
|
|
22
22
|
const ABSTAINING_ALLOW_PROBE = Object.freeze({ decision: Decision.ALLOW });
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* The
|
|
26
|
-
* adapter that maps event names must answer {@link EventKind.UNKNOWN}
|
|
25
|
+
* The payload rule ⑨ drifts an adapter with. No host emits that event name, so
|
|
26
|
+
* an adapter that maps event names must answer {@link EventKind.UNKNOWN}.
|
|
27
|
+
*
|
|
28
|
+
* `version` carries a release far past any adapter's enforcement floor. Without
|
|
29
|
+
* it Codex's own version gate answers non-vetoable for an unrelated reason, and
|
|
30
|
+
* the rule then passes on the adapter that needs it most — Codex routes EVERY
|
|
31
|
+
* non-PreToolUse event into UNKNOWN, so a probe it cannot bite on certifies
|
|
32
|
+
* nothing.
|
|
27
33
|
*/
|
|
28
|
-
const
|
|
34
|
+
const DRIFT_PROBE_NATIVE = Object.freeze({
|
|
35
|
+
hook_event_name: "ConformanceProbeUnmodelledEvent",
|
|
36
|
+
version: "9999.0.0",
|
|
37
|
+
});
|
|
29
38
|
|
|
30
39
|
/**
|
|
31
40
|
* Assert an adapter's {@link import("./control-plane.mjs").Adapter.COVERAGE}
|
|
@@ -404,17 +413,19 @@ export function runAdapterConformance({ adapter, fixtures, assert }) {
|
|
|
404
413
|
enforcedDenySeen,
|
|
405
414
|
"no enforced deny rendered — enforcement honesty is untested",
|
|
406
415
|
);
|
|
407
|
-
// Rule
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
|
|
416
|
+
// Rule ⑨: parse must ANSWER for an event it cannot name rather than throw, and
|
|
417
|
+
// the render of a deny on that answer must not claim a block. Probed on a
|
|
418
|
+
// synthesized payload rather than a fixture flag, because an adapter whose
|
|
419
|
+
// parse can never emit UNKNOWN — amp routes every payload to pre_tool — has no
|
|
420
|
+
// such fixture to write, and a fixture-driven check would then sit vacuous on
|
|
421
|
+
// exactly the adapters this rule covers.
|
|
422
|
+
const drifted = adapter.parse({ ...DRIFT_PROBE_NATIVE });
|
|
412
423
|
if (drifted.event === EventKind.UNKNOWN) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
424
|
+
// Not that the event is non-vetoable — `makeEvent` refuses to construct a
|
|
425
|
+
// vetoable UNKNOWN at all, so asserting it here would police a state the
|
|
426
|
+
// constructor already makes unreachable. What only a probe can reach is the
|
|
427
|
+
// RENDER: an adapter is free to derive `enforced` from something other than
|
|
428
|
+
// the flag, and then the false block returns by another door.
|
|
418
429
|
assert.equal(
|
|
419
430
|
adapter.render(UNENFORCEABLE_DENY_PROBE, drifted).enforced,
|
|
420
431
|
false,
|
package/src/control-plane.mjs
CHANGED
|
@@ -429,6 +429,16 @@ export function makeEvent({
|
|
|
429
429
|
throw new TypeError(
|
|
430
430
|
`control-plane: makeEvent this_call_vetoable must be a boolean, got ${typeof this_call_vetoable}`,
|
|
431
431
|
);
|
|
432
|
+
// The same fail-open seam, one step further: an event the adapter could not
|
|
433
|
+
// name is one whose host response nobody established, so a veto reported for
|
|
434
|
+
// it is a block the render claims and the host never performs. Refused HERE
|
|
435
|
+
// because every adapter builds its event through this call, so an adapter that
|
|
436
|
+
// computes the flag without `vetoableFor` cannot reach the wire either.
|
|
437
|
+
if (event === EventKind.UNKNOWN && this_call_vetoable)
|
|
438
|
+
throw new Error(
|
|
439
|
+
"control-plane: makeEvent got a vetoable UNKNOWN event — an unmodelled " +
|
|
440
|
+
"event has no host response to veto, so reporting one is a false block",
|
|
441
|
+
);
|
|
432
442
|
/** @type {ToolCallEvent} */
|
|
433
443
|
const evt = {
|
|
434
444
|
schema_version: SCHEMA_VERSION,
|
package/types/adapters/amp.d.mts
CHANGED
|
@@ -30,12 +30,6 @@ export const INTEGRATION_MODE: "external_hook";
|
|
|
30
30
|
*/
|
|
31
31
|
/** @type {import("../control-plane.mjs").CoverageMap} */
|
|
32
32
|
export const COVERAGE: import("../control-plane.mjs").CoverageMap;
|
|
33
|
-
/**
|
|
34
|
-
* The event kinds this host can actually gate. Amp invokes the delegate for a tool call and nothing else, so parse only ever emits PRE_TOOL.
|
|
35
|
-
* A kind absent here parses non-vetoable, so an unmodelled event never renders as
|
|
36
|
-
* an enforced block the host will not perform.
|
|
37
|
-
*/
|
|
38
|
-
export const GATED_EVENTS: Readonly<Set<"pre_tool">>;
|
|
39
33
|
/** @type {import("../control-plane.mjs").Adapter} */
|
|
40
34
|
export const ampAdapter: import("../control-plane.mjs").Adapter;
|
|
41
35
|
export type ToolCallEvent = import("../control-plane.mjs").ToolCallEvent;
|
|
@@ -41,14 +41,6 @@ export const INTEGRATION_MODE: "external_hook";
|
|
|
41
41
|
*/
|
|
42
42
|
/** @type {import("../control-plane.mjs").CoverageMap} */
|
|
43
43
|
export const COVERAGE: import("../control-plane.mjs").CoverageMap;
|
|
44
|
-
/**
|
|
45
|
-
* The event kinds this host can actually gate. Claude Code honours a deny on these three. SESSION_START is left out: exit 2 does
|
|
46
|
-
* not abort a session start, and the matrix documents no host response for it — an
|
|
47
|
-
* undocumented kind is fail-closed to non-vetoable, the same rule coverage uses.
|
|
48
|
-
* A kind absent here parses non-vetoable, so an unmodelled event never renders as
|
|
49
|
-
* an enforced block the host will not perform.
|
|
50
|
-
*/
|
|
51
|
-
export const GATED_EVENTS: Readonly<Set<"pre_tool" | "post_tool" | "prompt_submit">>;
|
|
52
44
|
/** Claude Code native hook event names (the `hook_event_name` field). */
|
|
53
45
|
export const HookEvent: Readonly<{
|
|
54
46
|
PRE_TOOL_USE: "PreToolUse";
|
|
@@ -59,13 +59,6 @@ export const INTEGRATION_MODE: "external_hook";
|
|
|
59
59
|
*/
|
|
60
60
|
/** @type {import("../control-plane.mjs").CoverageMap} */
|
|
61
61
|
export const COVERAGE: import("../control-plane.mjs").CoverageMap;
|
|
62
|
-
/**
|
|
63
|
-
* The event kinds this host can actually gate. `PreToolUse` is the one event Codex can veto. Every other native event parses as
|
|
64
|
-
* UNKNOWN today, so without this set each of them reported an enforced block.
|
|
65
|
-
* A kind absent here parses non-vetoable, so an unmodelled event never renders as
|
|
66
|
-
* an enforced block the host will not perform.
|
|
67
|
-
*/
|
|
68
|
-
export const GATED_EVENTS: Readonly<Set<"pre_tool">>;
|
|
69
62
|
/** Minimum Codex version whose hook can actually veto a tool call. */
|
|
70
63
|
export const MIN_ENFORCING_VERSION: readonly number[];
|
|
71
64
|
export const DEFAULT_DENY_REASON: "blocked by monitor";
|
|
@@ -43,12 +43,6 @@ export const INTEGRATION_MODE: "external_hook";
|
|
|
43
43
|
*/
|
|
44
44
|
/** @type {import("../control-plane.mjs").CoverageMap} */
|
|
45
45
|
export const COVERAGE: import("../control-plane.mjs").CoverageMap;
|
|
46
|
-
/**
|
|
47
|
-
* The event kinds this host can actually gate. Gemini CLI honours a deny on each of the three events NATIVE_TO_KIND maps.
|
|
48
|
-
* A kind absent here parses non-vetoable, so an unmodelled event never renders as
|
|
49
|
-
* an enforced block the host will not perform.
|
|
50
|
-
*/
|
|
51
|
-
export const GATED_EVENTS: Readonly<Set<"pre_tool" | "post_tool" | "prompt_submit">>;
|
|
52
46
|
/** Gemini CLI native hook event names (the `hook_event_name` field). */
|
|
53
47
|
export const HookEvent: Readonly<{
|
|
54
48
|
BEFORE_TOOL: "BeforeTool";
|