agent-control-plane-core 0.9.2 → 0.10.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 CHANGED
@@ -125,6 +125,9 @@ NATIVE_ASK_TIER = false;
125
125
  Read `NATIVE_ASK_TIER` instead of keeping a list of which agents to escalate an
126
126
  `ask` to a `deny` on: a guardrail that must not let an ask through escalates it
127
127
  where the flag is `false`, and leaves it alone where the host does the asking.
128
+ `renderEscalated(adapter, verdict, event, { soleGate })` applies that rule: it
129
+ re-renders as a `deny` any non-allow verdict that would render unenforced on a
130
+ vetoable call, except an `ask` on a host whose flag is `true`.
128
131
  It describes the HOST, not the verdict — `ask` renders as `enforced: false`
129
132
  everywhere — and it names the pre-tool gate, since a tool that already ran has
130
133
  nothing left to suspend. Conformance probes your real `render` against it, so a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-control-plane-core",
3
- "version": "0.9.2",
3
+ "version": "0.10.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": {
@@ -159,7 +159,10 @@ export function parse(native) {
159
159
 
160
160
  /**
161
161
  * Amp honours a distinct ask tier: exit 1 is its "ask the user", a code Amp
162
- * treats as neither the allow (0) nor the reject (2). {@link render} emits it
162
+ * treats as neither the allow (0) nor the reject (2). Source: Amp's own notes,
163
+ * "`0` allows the tool call, `1` makes Amp ask, and `2` rejects the tool call"
164
+ * (https://ampcode.com/notes/permissions). No live probe has measured it, and
165
+ * no source says what exit 1 does in a headless run. {@link render} emits it
163
166
  * for every `ask`, so a consumer must NOT escalate an `ask` to a `deny` here.
164
167
  * The table below leans on the same tier for the non-vetoable deny, which is
165
168
  * only honest because the tier is real.
@@ -958,3 +958,34 @@ export function gatedResponse(
958
958
  [row.reason_stream]: blockReason(reason),
959
959
  });
960
960
  }
961
+
962
+ /**
963
+ * Render `verdict` on `adapter`'s host, escalating to a `deny` when the host
964
+ * would otherwise let the call run without the gate it asked for. A non-allow
965
+ * verdict that renders unenforced on a vetoable call is re-rendered as a deny,
966
+ * unless it is an `ask` and the host honours its own ask tier
967
+ * ({@link Adapter} `NATIVE_ASK_TIER`). Nothing changes for an allow, for a call
968
+ * this guardrail cannot veto (a deny there blocks nothing either), or for a
969
+ * render the host already enforces. The deny keeps the verdict's other fields.
970
+ * @param {Adapter} adapter
971
+ * @param {Verdict} verdict
972
+ * @param {ToolCallEvent} event
973
+ * @param {{ soleGate?: boolean }} [options] passed through to `render`
974
+ * @returns {NativeResponse}
975
+ */
976
+ export function renderEscalated(adapter, verdict, event, options = {}) {
977
+ const { decision } = normalizeVerdict(verdict);
978
+ const response = adapter.render(verdict, event, options);
979
+ if (
980
+ response.enforced ||
981
+ decision === Decision.ALLOW ||
982
+ !event.this_call_vetoable ||
983
+ (decision === Decision.ASK && adapter.NATIVE_ASK_TIER)
984
+ )
985
+ return response;
986
+ return adapter.render(
987
+ { ...verdict, decision: Decision.DENY },
988
+ event,
989
+ options,
990
+ );
991
+ }
@@ -98,7 +98,10 @@ export const NATIVE_EVENT_FOR: Record<string, string | undefined>;
98
98
  export const UNRENDERED_FIELDS: Record<string, ReadonlySet<string> | undefined>;
99
99
  /**
100
100
  * Amp honours a distinct ask tier: exit 1 is its "ask the user", a code Amp
101
- * treats as neither the allow (0) nor the reject (2). {@link render} emits it
101
+ * treats as neither the allow (0) nor the reject (2). Source: Amp's own notes,
102
+ * "`0` allows the tool call, `1` makes Amp ask, and `2` rejects the tool call"
103
+ * (https://ampcode.com/notes/permissions). No live probe has measured it, and
104
+ * no source says what exit 1 does in a headless run. {@link render} emits it
102
105
  * for every `ask`, so a consumer must NOT escalate an `ask` to a `deny` here.
103
106
  * The table below leans on the same tier for the non-vetoable deny, which is
104
107
  * only honest because the tier is real.
@@ -288,6 +288,23 @@ export function gatedResponse(agent: string, { transport, enforced, unblocked_ex
288
288
  stdout?: unknown;
289
289
  reason?: string;
290
290
  }): NativeResponse;
291
+ /**
292
+ * Render `verdict` on `adapter`'s host, escalating to a `deny` when the host
293
+ * would otherwise let the call run without the gate it asked for. A non-allow
294
+ * verdict that renders unenforced on a vetoable call is re-rendered as a deny,
295
+ * unless it is an `ask` and the host honours its own ask tier
296
+ * ({@link Adapter} `NATIVE_ASK_TIER`). Nothing changes for an allow, for a call
297
+ * this guardrail cannot veto (a deny there blocks nothing either), or for a
298
+ * render the host already enforces. The deny keeps the verdict's other fields.
299
+ * @param {Adapter} adapter
300
+ * @param {Verdict} verdict
301
+ * @param {ToolCallEvent} event
302
+ * @param {{ soleGate?: boolean }} [options] passed through to `render`
303
+ * @returns {NativeResponse}
304
+ */
305
+ export function renderEscalated(adapter: Adapter, verdict: Verdict, event: ToolCallEvent, options?: {
306
+ soleGate?: boolean;
307
+ }): NativeResponse;
291
308
  /**
292
309
  * The vendor-neutral control-plane contract.
293
310
  *