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 +3 -0
- package/package.json +1 -1
- package/src/adapters/amp.mjs +4 -1
- package/src/control-plane.mjs +31 -0
- package/types/adapters/amp.d.mts +4 -1
- package/types/control-plane.d.mts +17 -0
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.
|
|
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": {
|
package/src/adapters/amp.mjs
CHANGED
|
@@ -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).
|
|
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.
|
package/src/control-plane.mjs
CHANGED
|
@@ -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
|
+
}
|
package/types/adapters/amp.d.mts
CHANGED
|
@@ -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).
|
|
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
|
*
|