@sema-agent/core 7.53.0 → 7.54.1
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/CHANGELOG.md +291 -1
- package/LICENSE +3 -1
- package/dist/agents/peer-session-drain.d.ts +0 -1
- package/dist/agents/peer-session-drain.js +0 -2
- package/dist/agents/send-message-tool.js +3 -4
- package/dist/agents/subagent.d.ts +3 -5
- package/dist/agents/subagent.js +18 -11
- package/dist/agents/tool-filter.d.ts +7 -4
- package/dist/agents/tool-filter.js +7 -3
- package/dist/core/a2a.js +4 -2
- package/dist/core/allow-entry.d.ts +1 -0
- package/dist/core/allow-entry.js +1 -0
- package/dist/core/approval-object.d.ts +87 -0
- package/dist/core/approval-object.js +85 -0
- package/dist/core/arg-summary.d.ts +3 -2
- package/dist/core/arg-summary.js +17 -11
- package/dist/core/ask-origin.d.ts +43 -27
- package/dist/core/ask-origin.js +20 -11
- package/dist/core/ask-question.d.ts +0 -1
- package/dist/core/ask-question.js +4 -67
- package/dist/core/ask-unresolvable-notice.d.ts +3 -2
- package/dist/core/auto-compaction.d.ts +49 -50
- package/dist/core/auto-compaction.js +41 -51
- package/dist/core/auto-mode-prompt.d.ts +4 -2
- package/dist/core/auto-mode-prompt.js +4 -3
- package/dist/core/canonical-json.d.ts +69 -0
- package/dist/core/canonical-json.js +130 -8
- package/dist/core/checkpoint-requirements.d.ts +13 -7
- package/dist/core/checkpoint-requirements.js +4 -1
- package/dist/core/checkpoint-store.d.ts +33 -18
- package/dist/core/checkpoint-store.js +5 -4
- package/dist/core/child-clamp-seats.d.ts +45 -6
- package/dist/core/child-clamp-seats.js +56 -4
- package/dist/core/consolidate-scope.d.ts +1 -1
- package/dist/core/cross-boundary-taint.d.ts +213 -0
- package/dist/core/cross-boundary-taint.js +196 -0
- package/dist/core/decision-receipt.d.ts +44 -1
- package/dist/core/decision-receipt.js +16 -0
- package/dist/core/effective-path-target.d.ts +3 -0
- package/dist/core/effective-path-target.js +4 -0
- package/dist/core/engine-notice.d.ts +34 -2
- package/dist/core/engine-notice.js +17 -1
- package/dist/core/engine-storage-backend.d.ts +73 -0
- package/dist/core/engine-storage-backend.js +1 -0
- package/dist/core/file-history-store.d.ts +6 -2
- package/dist/core/file-history-store.js +1 -0
- package/dist/core/gate-fold.js +10 -2
- package/dist/core/gate-lanes.js +30 -21
- package/dist/core/gate-outcome.d.ts +7 -3
- package/dist/core/governance-codes.d.ts +7 -2
- package/dist/core/governance-codes.js +6 -0
- package/dist/core/hooks.d.ts +14 -14
- package/dist/core/hooks.js +1 -2
- package/dist/core/locked-config.d.ts +28 -1
- package/dist/core/locked-config.js +30 -3
- package/dist/core/lsp.d.ts +1 -1
- package/dist/core/mcp-server-spec.d.ts +66 -9
- package/dist/core/mcp-server-spec.js +52 -19
- package/dist/core/mcp.js +5 -4
- package/dist/core/memory-engine/engine.js +2 -1
- package/dist/core/memory-engine/export-bundle.d.ts +1 -1
- package/dist/core/memory-engine/export-bundle.js +1 -1
- package/dist/core/memory-engine/file-backend.d.ts +2 -6
- package/dist/core/memory-engine/file-backend.js +1 -12
- package/dist/core/memory.d.ts +3 -3
- package/dist/core/mode-policy.d.ts +13 -8
- package/dist/core/mode-policy.js +3 -2
- package/dist/core/mods/kernel/receive.d.ts +2 -1
- package/dist/core/mods/kernel/receive.js +7 -2
- package/dist/core/parent-spec-seat.d.ts +10 -13
- package/dist/core/parent-spec-seat.js +14 -6
- package/dist/core/pause-registry.d.ts +35 -1
- package/dist/core/pause-registry.js +10 -0
- package/dist/core/permission-rule-consent.d.ts +18 -5
- package/dist/core/permission-rule-consent.js +13 -8
- package/dist/core/permission-rule-model.d.ts +29 -6
- package/dist/core/permission-rule-model.js +6 -1
- package/dist/core/permission-rule-org.d.ts +37 -0
- package/dist/core/permission-rule-org.js +34 -42
- package/dist/core/permission-rule-session.js +2 -2
- package/dist/core/permission-rule-store.d.ts +8 -0
- package/dist/core/permission-rule-store.js +1 -0
- package/dist/core/permission-rule-sync.js +3 -2
- package/dist/core/read-only-shell.d.ts +1 -1
- package/dist/core/read-only-shell.js +30 -9
- package/dist/core/remote-env.d.ts +41 -1
- package/dist/core/remote-env.js +18 -0
- package/dist/core/resume-startup-cause.d.ts +18 -0
- package/dist/core/resume-startup-cause.js +12 -0
- package/dist/core/runner/approval-feedback.d.ts +46 -0
- package/dist/core/runner/approval-feedback.js +41 -0
- package/dist/core/runner/compaction-call-options.d.ts +3 -3
- package/dist/core/runner/compaction-call-options.js +2 -2
- package/dist/core/runner/contracts.d.ts +33 -41
- package/dist/core/runner/cross-boundary-lane.d.ts +87 -0
- package/dist/core/runner/cross-boundary-lane.js +190 -0
- package/dist/core/runner/decide-continuation.d.ts +4 -4
- package/dist/core/runner/decide-continuation.js +3 -1
- package/dist/core/runner/gate-exit.d.ts +32 -12
- package/dist/core/runner/gate-exit.js +75 -33
- package/dist/core/runner/inherited-ask-grants.js +2 -2
- package/dist/core/runner/person-path-rows.d.ts +2 -2
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +7 -9
- package/dist/core/runner/prepare-acquire-reconcile.js +8 -6
- package/dist/core/runner/prepare-ask-lane.js +3 -3
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +6 -5
- package/dist/core/runner/prepare-caps-and-workflow.js +3 -3
- package/dist/core/runner/prepare-config-doors.d.ts +12 -15
- package/dist/core/runner/prepare-config-doors.js +8 -3
- package/dist/core/runner/prepare-context-lane.js +6 -3
- package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
- package/dist/core/runner/prepare-delegation-surface.js +2 -3
- package/dist/core/runner/prepare-execution-env.d.ts +1 -2
- package/dist/core/runner/prepare-execution-env.js +2 -1
- package/dist/core/runner/prepare-gate-stations.d.ts +2 -0
- package/dist/core/runner/prepare-gate-stations.js +4 -3
- package/dist/core/runner/prepare-hands-readface.d.ts +2 -2
- package/dist/core/runner/prepare-hands-readface.js +1 -1
- package/dist/core/runner/prepare-inherited-gate.d.ts +2 -0
- package/dist/core/runner/prepare-inherited-gate.js +1 -0
- package/dist/core/runner/prepare-park-ask.d.ts +1 -1
- package/dist/core/runner/prepare-park-ask.js +9 -3
- package/dist/core/runner/prepare-policy-chain.js +2 -7
- package/dist/core/runner/prepare-safety-scan.d.ts +7 -2
- package/dist/core/runner/prepare-safety-scan.js +34 -27
- package/dist/core/runner/prepare-suspend-saga.d.ts +2 -2
- package/dist/core/runner/prepare-suspend-saga.js +6 -2
- package/dist/core/runner/prepare-task.js +13 -10
- package/dist/core/runner/prepare-turn-wiring.d.ts +10 -5
- package/dist/core/runner/prepare-turn-wiring.js +14 -49
- package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
- package/dist/core/runner/prepare-wiring-manifest.js +2 -2
- package/dist/core/runner/prepare-workspace-restore.js +2 -2
- package/dist/core/runner/resume-admission.js +9 -9
- package/dist/core/runner/resume-apply.js +4 -4
- package/dist/core/runner/resume-policy-outcome.d.ts +2 -2
- package/dist/core/runner/resume-policy-outcome.js +18 -15
- package/dist/core/runner/resume-preflight.js +2 -2
- package/dist/core/runner/resume-review-outcome.js +3 -3
- package/dist/core/runner/run-leg.js +57 -33
- package/dist/core/runner/run-settle-and-teardown.js +2 -0
- package/dist/core/runner/run-terminal-adoption.js +15 -4
- package/dist/core/runner/session-file-state-replay.d.ts +5 -2
- package/dist/core/runner/session-file-state-replay.js +3 -2
- package/dist/core/runner/stream-steer-verb.js +2 -2
- package/dist/core/runner/tool-face-overlay.d.ts +24 -6
- package/dist/core/runner/tool-face-overlay.js +24 -15
- package/dist/core/runner-deps.d.ts +9 -9
- package/dist/core/session-store.d.ts +35 -16
- package/dist/core/session-store.js +20 -12
- package/dist/core/{shell-lexer.d.ts → shell-run-candidates.d.ts} +36 -12
- package/dist/core/{shell-lexer.js → shell-run-candidates.js} +145 -225
- package/dist/core/shell-wrapper-table.d.ts +5 -3
- package/dist/core/shell-wrapper-table.js +1 -1
- package/dist/core/store-contracts/checkpoint-store-contract.js +2 -2
- package/dist/core/store-contracts/session-store-contract.d.ts +49 -0
- package/dist/core/store-contracts/session-store-contract.js +683 -0
- package/dist/core/store-contracts/session-store-incarnation-contract.d.ts +12 -0
- package/dist/core/store-contracts/session-store-incarnation-contract.js +5 -2
- package/dist/core/task-event.d.ts +6 -4
- package/dist/core/task-registry-agent.js +1 -0
- package/dist/core/task-registry-shared.d.ts +5 -0
- package/dist/core/task-registry.d.ts +7 -0
- package/dist/core/task-registry.js +17 -0
- package/dist/core/task-result.d.ts +1 -1
- package/dist/core/task-spec.d.ts +6 -6
- package/dist/core/tighten-task-spec.d.ts +6 -4
- package/dist/core/tighten-task-spec.js +2 -2
- package/dist/core/tool-catalog-entries.d.ts +1 -1
- package/dist/core/tool-catalog-entries.js +52 -52
- package/dist/core/tool-face-reader.d.ts +49 -0
- package/dist/core/tool-face-reader.js +94 -0
- package/dist/core/tool-face.d.ts +17 -8
- package/dist/core/tool-face.js +1 -0
- package/dist/core/tool-policy.d.ts +64 -23
- package/dist/core/tool-policy.js +62 -56
- package/dist/core/tool-roster.d.ts +46 -28
- package/dist/core/tool-roster.js +91 -14
- package/dist/core/tool-spec-members.js +2 -2
- package/dist/core/tool-spec.d.ts +20 -4
- package/dist/core/tools.js +2 -0
- package/dist/core/trace.d.ts +24 -2
- package/dist/core/types.d.ts +2 -2
- package/dist/core/types.js +1 -1
- package/dist/core/untrusted-text.d.ts +7 -0
- package/dist/core/untrusted-text.js +3 -8
- package/dist/core/user-attachment.d.ts +43 -0
- package/dist/core/user-attachment.js +92 -15
- package/dist/engine/compaction/compaction.d.ts +2 -2
- package/dist/engine/compaction/utils.d.ts +2 -2
- package/dist/engine/harness/types.d.ts +7 -4
- package/dist/engine/llm/types.d.ts +7 -2
- package/dist/engine/llm/types.js +2 -1
- package/dist/engine/session/parent-chain-guard.d.ts +10 -0
- package/dist/engine/session/parent-chain-guard.js +10 -0
- package/dist/engine/session/repo-utils.d.ts +5 -0
- package/dist/engine/session/repo-utils.js +9 -0
- package/dist/engine/session/storage-base.js +5 -2
- package/dist/index.d.ts +23 -9
- package/dist/index.js +19 -8
- package/dist/internal/harness.d.ts +1 -0
- package/dist/internal/harness.js +1 -0
- package/dist/internal/llm.d.ts +1 -1
- package/dist/internal/llm.js +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +3 -0
- package/dist/orchestration/run-workflow-tool.js +1 -0
- package/dist/orchestration/workflow-governance.d.ts +1 -1
- package/dist/orchestration/workflow.d.ts +3 -0
- package/dist/orchestration/workflow.js +2 -1
- package/dist/prompt-assembly/catalog/schema.d.ts +3 -0
- package/dist/prompt-assembly/catalog/schema.js +3 -0
- package/dist/prompt-assembly/overlay.js +2 -2
- package/dist/prompt-assembly/select.js +2 -2
- package/dist/prompts/default.d.ts +1 -1
- package/dist/shell/lex.d.ts +87 -8
- package/dist/shell/lex.js +283 -49
- package/dist/stores/file/index.d.ts +8 -6
- package/dist/tools/artifact/artifact-tool.js +10 -0
- package/dist/tools/fs/bash-program-position.d.ts +3 -15
- package/dist/tools/fs/bash-program-position.js +3 -10
- package/dist/tools/fs/bash-readonly-classifier.js +2 -0
- package/dist/tools/fs/fs-bash.d.ts +3 -3
- package/dist/tools/fs/fs-bash.js +72 -101
- package/dist/tools/fs/fs-read.d.ts +12 -0
- package/dist/tools/fs/fs-read.js +10 -9
- package/dist/tools/fs/fs-shared.d.ts +27 -31
- package/dist/tools/fs/fs-shared.js +12 -15
- package/dist/tools/fs/fs-write.js +1 -1
- package/dist/tools/fs/index.js +2 -2
- package/dist/tools/fs/read-deny-home.d.ts +1 -1
- package/dist/tools/fs/read-deny-home.js +5 -4
- package/dist/tools/fs/read-deny.d.ts +16 -3
- package/dist/tools/fs/read-deny.js +12 -9
- package/dist/tools/fs/safety.d.ts +7 -6
- package/dist/tools/fs/shell-read-bindings.d.ts +5 -3
- package/dist/tools/fs/shell-read-bindings.js +15 -16
- package/dist/tools/fs/shell-read-device.js +1 -1
- package/dist/tools/fs/shell-read-plan.js +36 -47
- package/dist/tools/fs/shell-read-word.d.ts +1 -6
- package/dist/tools/fs/shell-read-word.js +2 -20
- package/dist/tools/fs/shell-reentry-text.js +1 -1
- package/dist/tools/web.d.ts +2 -2
- package/package.json +4 -1
- package/test/export-surface.snapshot.json +5654 -5520
- package/wire-changes/core@7.53.0.json +30 -8
- package/wire-changes/core@7.54.0.json +2870 -0
- package/wire-changes/core@7.54.1.json +37 -0
- package/dist/core/shell-scan.d.ts +0 -60
- package/dist/core/shell-scan.js +0 -183
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { dataKind, perToolEmptyName, perToolEntryRefusal, quotedName, receivePerToolDeclaration } from "./mcp-server-spec.js";
|
|
2
|
+
import { isPlainRecord } from "./mods/kernel/receive.js";
|
|
3
|
+
import { faceProblem } from "./runner/tool-face-overlay.js";
|
|
4
|
+
import { inlineUntrusted } from "./untrusted-text.js";
|
|
5
|
+
const TOOL_FACE_MEMBERS = { family: true, pathTarget: true, renderHints: true, ruleFace: true };
|
|
6
|
+
const PATH_TARGET_KINDS = { param: "string", aliases: "list", access: "string", skillScopeEligible: "boolean", base: "string", absent: "string", patternParam: "string" };
|
|
7
|
+
const PATH_TARGET_REQUIRED = ["param", "access"];
|
|
8
|
+
const RENDER_HINT_KINDS = { userFacingName: "string", activity: "string", summaryParams: "list", resultCards: "list", approvalCard: "string" };
|
|
9
|
+
const RULE_FACE_KINDS = { primaryParams: "list", params: "list" };
|
|
10
|
+
function kindProblem(label, value, kind) {
|
|
11
|
+
if (value === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
if (kind === "list")
|
|
14
|
+
return Array.isArray(value) && value.every((name) => typeof name === "string") ? undefined : `${label} is ${dataKind(value)}, not a list of strings`;
|
|
15
|
+
return typeof value === kind ? undefined : `${label} is ${dataKind(value)}, not a ${kind}`;
|
|
16
|
+
}
|
|
17
|
+
function membersOf(raw, label, names) {
|
|
18
|
+
if (!isPlainRecord(raw))
|
|
19
|
+
return `${label} is ${dataKind(raw)}, not an object`;
|
|
20
|
+
const unknown = Object.keys(raw).find((member) => !names.includes(member));
|
|
21
|
+
return unknown === undefined ? raw : `${label} has the unknown member ${quotedName(unknown)} (the members are ${names.join(", ")})`;
|
|
22
|
+
}
|
|
23
|
+
function ownMember(record, name) {
|
|
24
|
+
return Object.hasOwn(record, name) ? record[name] : undefined;
|
|
25
|
+
}
|
|
26
|
+
function readMembers(raw, label, kinds, required = []) {
|
|
27
|
+
const record = membersOf(raw, label, Object.keys(kinds));
|
|
28
|
+
if (typeof record === "string")
|
|
29
|
+
return record;
|
|
30
|
+
for (const member of required)
|
|
31
|
+
if (ownMember(record, member) === undefined)
|
|
32
|
+
return `${label}.${member} is missing`;
|
|
33
|
+
const entries = [];
|
|
34
|
+
for (const member of Object.keys(record)) {
|
|
35
|
+
const kind = kinds[member];
|
|
36
|
+
const value = record[member];
|
|
37
|
+
const problem = kindProblem(`${label}.${member}`, value, kind);
|
|
38
|
+
if (problem !== undefined)
|
|
39
|
+
return problem;
|
|
40
|
+
if (value === undefined || (kind === "boolean" && value === false))
|
|
41
|
+
continue;
|
|
42
|
+
entries.push([member, kind === "list" ? [...value] : value]);
|
|
43
|
+
}
|
|
44
|
+
return Object.fromEntries(entries);
|
|
45
|
+
}
|
|
46
|
+
function readOneToolFace(raw) {
|
|
47
|
+
const face = membersOf(raw, "the face", Object.keys(TOOL_FACE_MEMBERS));
|
|
48
|
+
if (typeof face === "string")
|
|
49
|
+
return face;
|
|
50
|
+
const records = { pathTarget: [PATH_TARGET_KINDS, PATH_TARGET_REQUIRED], renderHints: [RENDER_HINT_KINDS, []], ruleFace: [RULE_FACE_KINDS, []] };
|
|
51
|
+
const entries = [];
|
|
52
|
+
for (const member of Object.keys(face)) {
|
|
53
|
+
const value = face[member];
|
|
54
|
+
if (value === undefined)
|
|
55
|
+
continue;
|
|
56
|
+
if (member === "family") {
|
|
57
|
+
const problem = kindProblem("family", value, "string");
|
|
58
|
+
if (problem !== undefined)
|
|
59
|
+
return problem;
|
|
60
|
+
entries.push([member, value]);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const [kinds, required] = records[member];
|
|
64
|
+
const record = readMembers(value, member, kinds, required);
|
|
65
|
+
if (typeof record === "string")
|
|
66
|
+
return record;
|
|
67
|
+
entries.push([member, record]);
|
|
68
|
+
}
|
|
69
|
+
const typed = Object.fromEntries(entries);
|
|
70
|
+
return faceProblem(typed, undefined) ?? typed;
|
|
71
|
+
}
|
|
72
|
+
export function readToolFaces(declared) {
|
|
73
|
+
if (declared === undefined)
|
|
74
|
+
return { ok: true };
|
|
75
|
+
const received = receivePerToolDeclaration(declared, "toolFaces", "tool");
|
|
76
|
+
if (!received.ok)
|
|
77
|
+
return { ok: false, problem: received.problem };
|
|
78
|
+
const entries = [];
|
|
79
|
+
for (const [tool, raw] of Object.entries(received.perTool)) {
|
|
80
|
+
if (tool.length === 0)
|
|
81
|
+
return { ok: false, problem: perToolEmptyName("toolFaces", "tool") };
|
|
82
|
+
let face;
|
|
83
|
+
try {
|
|
84
|
+
face = readOneToolFace(raw);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
face = "a member threw while it was read";
|
|
88
|
+
}
|
|
89
|
+
if (typeof face === "string")
|
|
90
|
+
return { ok: false, problem: perToolEntryRefusal("toolFaces", "tool", tool, inlineUntrusted(face, 240)) };
|
|
91
|
+
entries.push([tool, face]);
|
|
92
|
+
}
|
|
93
|
+
return entries.length > 0 ? { ok: true, faces: Object.fromEntries(entries) } : { ok: true };
|
|
94
|
+
}
|
package/dist/core/tool-face.d.ts
CHANGED
|
@@ -198,6 +198,9 @@ export interface ToolRenderHints {
|
|
|
198
198
|
*/
|
|
199
199
|
export interface ToolFaceDeclaration {
|
|
200
200
|
family: ToolFamily;
|
|
201
|
+
/** The legs every call of the definition travels, or `"per_call"` when the code that dispatches it gives them per call. REQUIRED:
|
|
202
|
+
* a definition that states none does not compile — no default, and no fail-closed reading as outside. */
|
|
203
|
+
boundaryLegs: import("./cross-boundary-taint.js").BoundaryLegsDeclaration;
|
|
201
204
|
effect?: import("./tool-spec.js").ToolEffect;
|
|
202
205
|
egress?: true;
|
|
203
206
|
irreversibility?: "never" | "maybe" | "always";
|
|
@@ -232,6 +235,12 @@ export interface ToolFaceDeclaration {
|
|
|
232
235
|
*/
|
|
233
236
|
export declare const TOOL_LEG_KINDS: readonly ["delegated", "teammate", "goal", "observer", "structured"];
|
|
234
237
|
export type ToolLegKind = (typeof TOOL_LEG_KINDS)[number];
|
|
238
|
+
/**
|
|
239
|
+
* The leg kinds that share their spawner's tool surface: a teammate is handed tools its spawner holds in its own right (the team's mailbox,
|
|
240
|
+
* scheduler and task list), so the layers above the leg decide its protocol rows and the leg's own lists do not. A protocol row that no kind
|
|
241
|
+
* of this table names is the leg's own channel — a finishing or hand-back tool its spawner never held — and no layer decides it.
|
|
242
|
+
*/
|
|
243
|
+
export declare const SPAWNER_SURFACE_LEG_KINDS: readonly ToolLegKind[];
|
|
235
244
|
/**
|
|
236
245
|
* One layer of a tool selector, as data: an allowed-tools list (absent = no allowlist on this layer) and a denied-tools list,
|
|
237
246
|
* each a frozen copy of the entries a list was written with. A run's own layer is its `allowTools` and `excludeTools`; the
|
|
@@ -244,8 +253,9 @@ export interface ToolSelectorLayer {
|
|
|
244
253
|
/**
|
|
245
254
|
* The compiled tool selector of one run — the one answer to "is this tool available on this run", read at the roster valve
|
|
246
255
|
* and on every later roster change (an MCP refresh or re-dial, the deferred-search lane), and handed down as the ceiling of
|
|
247
|
-
* every run this one spawns (`layers`). A tool is admitted when it is
|
|
248
|
-
*
|
|
256
|
+
* every run this one spawns (`layers`). A tool is admitted when it is the leg's own protocol channel, or when every layer
|
|
257
|
+
* admits it — the layer's allowlist (if any) reaches it and its denylist does not — or, for a protocol tool of a leg that shares
|
|
258
|
+
* its spawner's tool surface ({@link SPAWNER_SURFACE_LEG_KINDS}), when every layer ABOVE the leg's own admits it.
|
|
249
259
|
*/
|
|
250
260
|
export interface ToolSelector {
|
|
251
261
|
readonly layers: readonly ToolSelectorLayer[];
|
|
@@ -280,13 +290,12 @@ export interface ToolCatalogEntry {
|
|
|
280
290
|
face: ToolFaceDeclaration;
|
|
281
291
|
/** A `modelGate` class the default-bundle assembler stamps on this definition's default arm. */
|
|
282
292
|
modelGate?: string;
|
|
283
|
-
/** The leg kinds whose legs rely on this tool as part of their protocol:
|
|
284
|
-
*
|
|
285
|
-
*
|
|
293
|
+
/** The leg kinds whose legs rely on this tool as part of their protocol: the leg's OWN lists do not decide it (an allowlist need not
|
|
294
|
+
* name it), and a denylist of the leg's own that reaches it is a contradiction the run refuses at prepare. Whether the layers ABOVE
|
|
295
|
+
* the leg decide it follows the row's kinds ({@link SPAWNER_SURFACE_LEG_KINDS}): a row some kind of which shares its spawner's tool
|
|
296
|
+
* surface is held exactly when the spawner may hold it; any other row is the leg's own channel, which no layer decides — an inherited
|
|
297
|
+
* deny that reaches it is the same contradiction. */
|
|
286
298
|
protocol?: readonly ToolLegKind[];
|
|
287
|
-
/** A protocol row the leg shares with the run that spawned it (the team task-list family a teammate shares with its leader): on its
|
|
288
|
-
* leg the leg's OWN lists do not decide it, but the ceiling still does — the leg holds it exactly when its spawner may. */
|
|
289
|
-
protocolUnderCeiling?: true;
|
|
290
299
|
}
|
|
291
300
|
/**
|
|
292
301
|
* The band-membership table over the mount tags: a host derives its hand band as the definitions listed under a
|
package/dist/core/tool-face.js
CHANGED
|
@@ -25,6 +25,7 @@ export const RENDER_HINT_MAX_CHARS = 64;
|
|
|
25
25
|
export const RENDER_HINT_ACTIVITY_MAX_CHARS = 32;
|
|
26
26
|
export const RENDER_HINT_MAX_LIST = 16;
|
|
27
27
|
export const TOOL_LEG_KINDS = Object.freeze(["delegated", "teammate", "goal", "observer", "structured"]);
|
|
28
|
+
export const SPAWNER_SURFACE_LEG_KINDS = Object.freeze(["teammate"]);
|
|
28
29
|
export const MOUNT_TAG_IN_HANDS_BAND = {
|
|
29
30
|
hands: true,
|
|
30
31
|
backgroundTasks: true,
|
|
@@ -2,6 +2,7 @@ import { type AskOrigin, type AssertAllKeysHandled } from "./ask-origin.js";
|
|
|
2
2
|
import { type ReadRootGrantScreen } from "./read-root-grant-screen.js";
|
|
3
3
|
import { type Settlement } from "./gate-outcome.js";
|
|
4
4
|
export { policyCannotAnswer, rowDemandsAPerson } from "./real-approval.js";
|
|
5
|
+
import { type BindingFault } from "./canonical-json.js";
|
|
5
6
|
import { type DecisionReason } from "./decision-receipt.js";
|
|
6
7
|
export { isEngineRefusal } from "./decision-receipt.js";
|
|
7
8
|
import { type RemovalGuardSeat } from "../tools/fs/removal-targets.js";
|
|
@@ -236,7 +237,8 @@ export interface EngineAttestation {
|
|
|
236
237
|
readonly origin?: AskOrigin;
|
|
237
238
|
/** The settled ask's class set (`ASK_CLASSES` order), when the minting station held one. */
|
|
238
239
|
readonly classes?: readonly import("./ask-origin.js").AskClass[];
|
|
239
|
-
/** `
|
|
240
|
+
/** `exactBoundInputHashOf` of the arguments the settlement licenses (an approver's edit, the snapshot shown, or the asked
|
|
241
|
+
* args); absent when they have none (outside the bindable domain) — such a record licenses no bytes. */
|
|
240
242
|
readonly inputDigest?: string;
|
|
241
243
|
/** The FORM of a classifier round that did not rule (a classifier deny's cause). */
|
|
242
244
|
readonly classifierCause?: import("./gate-outcome.js").ClassifierDenyCause;
|
|
@@ -249,6 +251,9 @@ export interface EngineAttestation {
|
|
|
249
251
|
/** Where a path deny row refused the call — the row, the view of the target it met, the face the call is on. The gate's
|
|
250
252
|
* exit mints the refusal's card from it; the judge that found the row is its only writer. */
|
|
251
253
|
readonly pathDenial?: import("../tools/fs/safety.js").PathDenialEvidence;
|
|
254
|
+
/** A seat refused the call's arguments because no approval can be bound to them ({@link bindableArgsOf}): the refusal's
|
|
255
|
+
* sentence, carried by the refusal itself — the gate's deny exit reads it (the remedy it gives) instead of judging again. */
|
|
256
|
+
readonly bindingRefusal?: string;
|
|
252
257
|
}
|
|
253
258
|
/** The members a station attests — everything but the call binding. */
|
|
254
259
|
export type AttestationMembers = Omit<EngineAttestation, "toolCallId" | "toolName">;
|
|
@@ -422,6 +427,11 @@ export type PermissionResult = {
|
|
|
422
427
|
* in the one direction the stamp cannot reach (a caller composing its own ask) the claim is
|
|
423
428
|
* tightening-only: it can make an ask outlive a rule, never turn one into an allow. */
|
|
424
429
|
probeMandated?: true;
|
|
430
|
+
/** This ask stands on the call's CROSS-BOUNDARY obligation — a person's confirmation
|
|
431
|
+
* ({@link import("./cross-boundary-taint.js").crossBoundaryObligation}); the station mints it beside `requiresRealApproval`, the
|
|
432
|
+
* decision receipt adds that bit to a policy's or hook's own. Its one carrier: the class (`cross_boundary`), the rule-silence word (`cross_boundary_taint`) and the
|
|
433
|
+
* real-approval bit are all read off this member. Tightening-only: a policy self-declaring it opts its own ask out of automatic answering. */
|
|
434
|
+
crossBoundaryMandated?: true;
|
|
425
435
|
/** This ask is MANDATED — no persisted allow rule and no STANDING ANSWER (a release remembered from an
|
|
426
436
|
* earlier call, at any scope, a session included) may retire it; only an answer produced for THIS call
|
|
427
437
|
* clears it — a person, a hook, a deployment's blanket `onAsk: "allow"` seat, or the live deciders the
|
|
@@ -592,6 +602,37 @@ export interface AskRuleEvidence {
|
|
|
592
602
|
}
|
|
593
603
|
/** The human/model-readable text of a decision. */
|
|
594
604
|
export declare function decisionText(d: PermissionResult): string | undefined;
|
|
605
|
+
/** Does a license (a record's `inputDigest`) cover the bytes that `runs`? The ONE judge — the gate's ledger keeps a settlement for
|
|
606
|
+
* the bytes it licensed by it, and the unstated question asks whether an answer was given for these bytes by it. A license for
|
|
607
|
+
* bytes covers what the tool reads them as ({@link argsReadFrom}: the one recorded step a reading took to `runs`). Compared by
|
|
608
|
+
* the EXACT digest only: bytes outside the bindable domain have none and are covered by no license. */
|
|
609
|
+
export declare function licenseCovers(licensed: string | undefined, runs: unknown): boolean;
|
|
610
|
+
/**
|
|
611
|
+
* THE judge of whether an approval can be bound to arguments, and the arguments it binds — asked wherever an approval is about to
|
|
612
|
+
* be asked or adopted, and nowhere else: the ask resolver (its no-approver arm too) and the approval policy before anyone is asked,
|
|
613
|
+
* the resolver where it adopts a person's edit, the gate's approved-edit loop (an edit any resolver hands back), and — through the
|
|
614
|
+
* same judge and sentence — the durable decide's capture of one before the claim ({@link capturedJsonOf}). Bindable = a record of
|
|
615
|
+
* plain JSON data nested at most 256 levels deep ({@link bindableValueOf}, exactly the values with an exact digest — the SAME reading every
|
|
616
|
+
* license and mint takes), judged on the value as given with nothing of the caller's run (a getter is never called: a value read
|
|
617
|
+
* through one has no fixed bytes). Ok ⇒ the judge's frozen copy, engine-owned (`args` itself when owned already); else the ONE
|
|
618
|
+
* sentence ({@link bindingRefusalOf}).
|
|
619
|
+
*/
|
|
620
|
+
export declare function bindableArgsOf<T>(toolName: string, args: T): {
|
|
621
|
+
ok: true;
|
|
622
|
+
value: T;
|
|
623
|
+
} | {
|
|
624
|
+
ok: false;
|
|
625
|
+
refusal: string;
|
|
626
|
+
};
|
|
627
|
+
/** THE sentence refusing arguments no approval can be bound to, naming what the judge met and where (`args` is what it was given). */
|
|
628
|
+
export declare function bindingRefusalOf(toolName: string, args: unknown, fault: BindingFault): string;
|
|
629
|
+
/** Was the call ANSWERED for the bytes that run — does `d` carry an engine record ({@link attestationOf}) of a settled wait
|
|
630
|
+
* whose license covers `runs`? A person's or the deployment approver's yes over these arguments is; a layer's allow is not,
|
|
631
|
+
* whatever it says about itself, and neither is a yes given for other bytes (a later layer that rewrote them voids it here). */
|
|
632
|
+
export declare function answeredFor(d: PermissionResult, call: {
|
|
633
|
+
toolCallId: string;
|
|
634
|
+
toolName: string;
|
|
635
|
+
}, runs: unknown): boolean;
|
|
595
636
|
/**
|
|
596
637
|
* Gates tool calls before they execute (the production safety layer for "processing/changing data").
|
|
597
638
|
*
|
|
@@ -878,9 +919,10 @@ export declare function constraintChainEntryOfSuppliedLayer(layer: ConstraintCha
|
|
|
878
919
|
* different frozen deny data now digest differently.
|
|
879
920
|
*/
|
|
880
921
|
export declare function constraintChainDigest(chain: readonly ConstraintChainEntry[]): string;
|
|
881
|
-
/** Every member of the decision union
|
|
882
|
-
* The receipt walks it: a contract member not held as an own enumerable data
|
|
883
|
-
* prototype chain holds one (an `undefined` data value spells absence)
|
|
922
|
+
/** Every member of the decision union, read off the member × arm table ({@link DECISION_MEMBER_ARMS}: a member the union gains
|
|
923
|
+
* and the table lacks is a type error there). The receipt walks it: a contract member not held as an own enumerable data
|
|
924
|
+
* property is refused if a descriptor on the prototype chain holds one (an `undefined` data value spells absence), and a
|
|
925
|
+
* present one must sit on an arm its row holds. Not re-exported from `src/index.ts`. */
|
|
884
926
|
export type DecisionMember = PermissionResult extends infer U ? (U extends unknown ? keyof U : never) : never;
|
|
885
927
|
export declare const DECISION_MEMBERS: readonly DecisionMember[];
|
|
886
928
|
/**
|
|
@@ -949,8 +991,10 @@ export type NamedToolPolicy = ToolPolicy & {
|
|
|
949
991
|
* which simply opts out of the audit). */
|
|
950
992
|
export declare function toolPolicyNameSets(p: ToolPolicy | undefined): readonly ToolPolicyNameSets[];
|
|
951
993
|
/**
|
|
952
|
-
* Name-based allow/deny. `deny` always wins. If `allow` is provided, only those tools
|
|
953
|
-
*
|
|
994
|
+
* Name-based allow/deny. `deny` always wins. If `allow` is provided, only those tools may run (everything else denied) — a
|
|
995
|
+
* NARROWING: a tool the list names gets no objection, never consent, so a `passthrough`-seat tool it names still meets the
|
|
996
|
+
* mode's unstated question; consent is an allow entry (`RunnerDeps.allowedTools` / `TaskSpec.allowedTools`). Without
|
|
997
|
+
* `allow`, everything not in `deny` gets no objection. Each entry is read by the rule
|
|
954
998
|
* syntax (`ruleSpellingOf`): a name, or `Tool()` / `Tool(*)` — the bare `Tool`, as every list reader reads them, once however
|
|
955
999
|
* many spellings write it; any other spelling is not a tool name (`onInvalidName`), and neither is the wildcard `*` (`*`,
|
|
956
1000
|
* `*()`, `*(*)`) in `deny`: it names no tool, and the entry would be kept and never match. In `allow` it is one entry no tool
|
|
@@ -975,13 +1019,6 @@ export declare function createAllowDenyPolicy(opts: {
|
|
|
975
1019
|
list: "allow" | "deny";
|
|
976
1020
|
message: string;
|
|
977
1021
|
}) => void;
|
|
978
|
-
/**
|
|
979
|
-
* What an `allow` list HIT means. `"grant"` (default): the deployment named this tool as permitted — a statement
|
|
980
|
-
* (`decisionReason: "rule"`) that satisfies the fold's unstated rule for a passthrough-seat tool. `"narrowing"`: the
|
|
981
|
-
* list only bounds WHICH tools a lane may see (an agent definition's `allowTools`); a hit is scope, not consent, and
|
|
982
|
-
* answers a bare allow — the mode's unstated question still goes to a person. The deny list is unaffected.
|
|
983
|
-
*/
|
|
984
|
-
scope?: "grant" | "narrowing";
|
|
985
1022
|
}): NamedToolPolicy;
|
|
986
1023
|
/**
|
|
987
1024
|
* What `createApprovalPolicy`'s `approve` may resolve to: a boolean, or the `AskOutcome` OBJECT minus the
|
|
@@ -1030,7 +1067,8 @@ export declare function createApprovalPolicy(opts: {
|
|
|
1030
1067
|
approve: (req: ToolCallRequest, signal?: AbortSignal) => ApprovalSeatOutcome | Promise<ApprovalSeatOutcome>;
|
|
1031
1068
|
/** Always-denied tools. */
|
|
1032
1069
|
deny?: string[];
|
|
1033
|
-
/**
|
|
1070
|
+
/** The exceptions to `denyByDefault` (only meaningful with it): tools that get no objection without an approval. A narrowing,
|
|
1071
|
+
* not consent — a `passthrough`-seat tool named here still meets the mode's unstated question; consent is an allow entry. */
|
|
1034
1072
|
autoAllow?: string[];
|
|
1035
1073
|
/** When true, deny anything not in `requireApproval` or `autoAllow`. Default false. */
|
|
1036
1074
|
denyByDefault?: boolean;
|
|
@@ -1050,12 +1088,11 @@ export declare function createApprovalPolicy(opts: {
|
|
|
1050
1088
|
* short-circuit — scanning continues, because a later policy may still `deny` (which outranks ask).
|
|
1051
1089
|
* With no deny, the first `ask` wins; with neither, `allow`.
|
|
1052
1090
|
*
|
|
1053
|
-
* **
|
|
1054
|
-
*
|
|
1055
|
-
*
|
|
1056
|
-
*
|
|
1057
|
-
*
|
|
1058
|
-
* unstated allow is not a permission — the gate puts the mode's question to it; for every other tool the two forms run alike.
|
|
1091
|
+
* **A layer's allow is no objection, never consent.** What a combined allow carries beyond that is the engine RECORDS its
|
|
1092
|
+
* layers' stations minted (an approver's yes inside a layer, an ancestor classifier's attribution), accumulated per member
|
|
1093
|
+
* group; a yes binds the bytes it was given (its license), so a layer that rewrites the arguments afterwards leaves a yes that
|
|
1094
|
+
* no longer covers what runs. For a tool whose permission seat is `passthrough` the gate puts the mode's question to a
|
|
1095
|
+
* combined allow unless an answer covers the bytes that run ({@link answeredFor}) — the allow layer answers it there.
|
|
1059
1096
|
*
|
|
1060
1097
|
* **Idempotency requirement:** because the fold continues past an `ask` to look for a later `deny`, a
|
|
1061
1098
|
* policy's `check()` can be invoked even after an earlier policy returned `ask`. `check()` MUST be
|
|
@@ -1847,8 +1884,8 @@ export declare function refusalOf<D extends PermissionResult>(deny: D, judged: u
|
|
|
1847
1884
|
/**
|
|
1848
1885
|
* A PreToolUse hook's `ask` folded over a verdict by `deny > ask > allow` — the ONE law for the gate's first screening and for
|
|
1849
1886
|
* an approved edit's re-screen. A deny stands. Over an allow, the hook's own ask, RECONSTRUCTED rather than spread (its message,
|
|
1850
|
-
* its real-approval demand, its rewrite) under the hook provenance the engine stamps. Over an ask, that ask keeps every member,
|
|
1851
|
-
* takes the hook provenance and the hook's real-approval demand, and its message carries the hook's beside its own — a person
|
|
1887
|
+
* its real-approval demand and cross-boundary member, its rewrite) under the hook provenance the engine stamps. Over an ask, that ask keeps every member,
|
|
1888
|
+
* takes the hook provenance and the hook's real-approval demand and cross-boundary member, and its message carries the hook's beside its own — a person
|
|
1852
1889
|
* clearing one ask that stands for two gates sees both reasons.
|
|
1853
1890
|
*/
|
|
1854
1891
|
export declare function foldHookAsk(verdict: Exclude<PermissionResult, {
|
|
@@ -1986,10 +2023,14 @@ export type ResolverArmResult = (Extract<ResolvedAsk, {
|
|
|
1986
2023
|
action: "allow";
|
|
1987
2024
|
}> & {
|
|
1988
2025
|
settlement?: Settlement;
|
|
1989
|
-
|
|
2026
|
+
bindingRefusal?: undefined;
|
|
2027
|
+
})
|
|
2028
|
+
/** `bindingRefusal`: the arm refused arguments no approval can be bound to — the record carries the sentence. */
|
|
2029
|
+
| (Extract<ResolvedAsk, {
|
|
1990
2030
|
action: "deny";
|
|
1991
2031
|
}> & {
|
|
1992
2032
|
settlement: Settlement;
|
|
2033
|
+
bindingRefusal?: string;
|
|
1993
2034
|
});
|
|
1994
2035
|
/**
|
|
1995
2036
|
* The DIRECTIONAL format controls, as a closed set (see {@link AskRequest.hasBidiControls}):
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { withArgsReading } from "./effective-path-target.js";
|
|
3
|
+
import { argsReadFrom, carryArgsReadFrom, withArgsReading } from "./effective-path-target.js";
|
|
4
4
|
import { brandPolicyAskClass } from "./ask-class.js";
|
|
5
5
|
import { decisionAskClassesOf, headlessDenialMessage } from "./ask-origin.js";
|
|
6
6
|
import { ownReadRootCandidate, readRootGrantSeat } from "./read-root-grant-screen.js";
|
|
@@ -12,12 +12,12 @@ import { join, normalize as normalizePath, sep } from "node:path";
|
|
|
12
12
|
import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
|
|
13
13
|
import { protectivePathTargetOf, shellFaceToolNames } from "./tool-registry.js";
|
|
14
14
|
import { heldBy } from "./tool-name-readings.js";
|
|
15
|
-
import { boundInputHashOf } from "./canonical-json.js";
|
|
15
|
+
import { bindableValueOf, boundInputHashOf, describeBindingFault, exactBoundInputHashOf } from "./canonical-json.js";
|
|
16
16
|
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY } from "./untrusted-text.js";
|
|
17
17
|
import { describeRejectedValue } from "./tool-search-setting.js";
|
|
18
18
|
import { egressText, egressThrownText } from "./untrusted-egress.js";
|
|
19
19
|
import { assertNoRetiredKeys, assertNoRetiredToolPolicyKeys, RETIRED_DECISION_MEMBERS, RETIRED_TOOL_POLICY_KEYS } from "./retired-keys.js";
|
|
20
|
-
import { isEngineRefusal, nestedRecordsOf, receiveContract, receiveNestedRecords, refusedOutOfContract, screenContractValues } from "./decision-receipt.js";
|
|
20
|
+
import { DECISION_MEMBER_ARMS, isEngineRefusal, nestedRecordsOf, receiveContract, receiveNestedRecords, refusedOutOfContract, screenContractValues } from "./decision-receipt.js";
|
|
21
21
|
export { isEngineRefusal } from "./decision-receipt.js";
|
|
22
22
|
import { isNamespacedCoveringRuleName, namespacedRuleNameCovers } from "./permission-rules.js";
|
|
23
23
|
import { ruleSpellingOf } from "./permission-rule-syntax.js";
|
|
@@ -45,6 +45,7 @@ const ATTESTATION_GROUP_OF = {
|
|
|
45
45
|
unresolvableCause: "unresolvableCause",
|
|
46
46
|
foldedClasses: "foldedClasses",
|
|
47
47
|
pathDenial: "pathDenial",
|
|
48
|
+
bindingRefusal: "bindingRefusal",
|
|
48
49
|
};
|
|
49
50
|
const ATTESTATION_MEMBERS = Object.keys(ATTESTATION_GROUP_OF);
|
|
50
51
|
const groupLead = (g) => ATTESTATION_MEMBERS.find((k) => ATTESTATION_GROUP_OF[k] === g);
|
|
@@ -146,6 +147,28 @@ export const ASK_EVIDENCE_ABSENCE_VALUES = ["not_wired", "not_adjudicated", "una
|
|
|
146
147
|
export function decisionText(d) {
|
|
147
148
|
return d.message;
|
|
148
149
|
}
|
|
150
|
+
export function licenseCovers(licensed, runs) {
|
|
151
|
+
return licensed !== undefined && (licensed === exactBoundInputHashOf(runs) || licensed === exactBoundInputHashOf(argsReadFrom(runs)));
|
|
152
|
+
}
|
|
153
|
+
export function bindableArgsOf(toolName, args) {
|
|
154
|
+
const bound = bindableValueOf(args);
|
|
155
|
+
if (!bound.ok)
|
|
156
|
+
return { ok: false, refusal: bindingRefusalOf(toolName, args, bound.fault) };
|
|
157
|
+
if (typeof args === "object" && args !== null && OWNED_ARGS.has(args))
|
|
158
|
+
return { ok: true, value: args };
|
|
159
|
+
if (typeof bound.value === "object" && bound.value !== null)
|
|
160
|
+
OWNED_ARGS.add(bound.value);
|
|
161
|
+
return { ok: true, value: bound.value };
|
|
162
|
+
}
|
|
163
|
+
export function bindingRefusalOf(toolName, args, fault) {
|
|
164
|
+
return (`no approval can be bound to the arguments of "${toolName}": they hold ${inlineUntrusted(describeBindingFault(args, fault), 300)} — an approval binds arguments that are ` +
|
|
165
|
+
`a record of plain JSON data (records, dense lists, strings, finite numbers, booleans, null; no getter, hidden member, Proxy, ` +
|
|
166
|
+
`function, symbol, bigint or object of another kind; no cycle or shared object) nested at most 256 levels deep; denied fail-closed`);
|
|
167
|
+
}
|
|
168
|
+
export function answeredFor(d, call, runs) {
|
|
169
|
+
const record = attestationOf(d, call);
|
|
170
|
+
return record?.settlement !== undefined && licenseCovers(record.inputDigest, runs);
|
|
171
|
+
}
|
|
149
172
|
const TOOL_POLICY_CARRIED_MEMBERS = ["projection", "pathRules", "writeGuardRows", "nameSets"];
|
|
150
173
|
export function carriedMembersOf(p) {
|
|
151
174
|
assertNoRetiredKeys(p, RETIRED_TOOL_POLICY_KEYS, "ToolPolicy");
|
|
@@ -259,12 +282,7 @@ export function constraintChainDigest(chain) {
|
|
|
259
282
|
return `cpv1:${createHash("sha256").update(stableJson(chain)).digest("hex")}`;
|
|
260
283
|
}
|
|
261
284
|
const ALLOW = Object.freeze({ action: "allow" });
|
|
262
|
-
const
|
|
263
|
-
export const DECISION_MEMBERS = Object.keys({
|
|
264
|
-
action: 0, updatedInput: 0, message: 0, decisionReason: 0, requiresRealApproval: 0, denialLimitFallback: 0, classifierNoRuling: 0, allowRuleShadowed: 0, probeReason: 0,
|
|
265
|
-
probeCause: 0, probeMandated: 0, mandated: 0, mandate: 0, probeReadRootCandidate: 0, matchedAskRule: 0, ruleEvidence: 0, segmentCoverage: 0,
|
|
266
|
-
origin: 0, askClass: 0, classes: 0, guards: 0, ruleUnreadable: 0, authoredBy: 0, foldedAskClasses: 0, attestation: 0,
|
|
267
|
-
});
|
|
285
|
+
export const DECISION_MEMBERS = Object.keys(DECISION_MEMBER_ARMS);
|
|
268
286
|
const HOOK_RESULT_MEMBERS = [...DECISION_MEMBERS, "additionalContext"];
|
|
269
287
|
const HOOK_SEAM_UNREAD = new Set(["decisionReason"]);
|
|
270
288
|
const HOOK_SEAM_OPTIONAL = new Set(["action"]);
|
|
@@ -315,7 +333,10 @@ export function refuseOutOfContractDecision(d, opts) {
|
|
|
315
333
|
return refusedOutOfContract(`a permission decision's members could not be detached from the layer that returned them (${ownedRewrite.reason}) — ` +
|
|
316
334
|
`the engine keeps its own copy of what it read; denied fail-closed`);
|
|
317
335
|
}
|
|
318
|
-
|
|
336
|
+
carryArgsReadFrom(contract.updatedInput, rewrite);
|
|
337
|
+
const held = detached.value;
|
|
338
|
+
const value = held.crossBoundaryMandated === true && held.requiresRealApproval !== true ? { ...held, requiresRealApproval: true } : held;
|
|
339
|
+
const receipt = Object.freeze(attestation !== undefined ? { ...value, attestation } : value);
|
|
319
340
|
for (const v of Object.values(receipt))
|
|
320
341
|
if (Array.isArray(v))
|
|
321
342
|
Object.freeze(v);
|
|
@@ -355,6 +376,7 @@ export function toolPolicyNameSets(p) {
|
|
|
355
376
|
return Array.isArray(sets) ? sets : [];
|
|
356
377
|
}
|
|
357
378
|
export function createAllowDenyPolicy(opts) {
|
|
379
|
+
assertNoRetiredKeys(opts, RETIRED_ALLOW_DENY_POLICY_OPTIONS, "createAllowDenyPolicy");
|
|
358
380
|
const invalid = [];
|
|
359
381
|
const screen = (entries, list) => {
|
|
360
382
|
if (entries === undefined)
|
|
@@ -444,10 +466,13 @@ export function createAllowDenyPolicy(opts) {
|
|
|
444
466
|
if (allow && !nameListHolds(allow, req) && !namespacedCoveringHit(allowCovering, req)) {
|
|
445
467
|
return { action: "deny", message: `tool "${req.toolName}" is not in the allowlist` };
|
|
446
468
|
}
|
|
447
|
-
return
|
|
469
|
+
return ALLOW;
|
|
448
470
|
},
|
|
449
471
|
};
|
|
450
472
|
}
|
|
473
|
+
const RETIRED_ALLOW_DENY_POLICY_OPTIONS = Object.freeze({
|
|
474
|
+
scope: "an allow list is only ever a narrowing now (a tool it does not name is denied; a tool it names gets no objection, not consent) — drop the option, and put the consent in the allow layer: `RunnerDeps.allowedTools` for the deployment's, `TaskSpec.allowedTools` for a request's",
|
|
475
|
+
});
|
|
451
476
|
export function createApprovalPolicy(opts) {
|
|
452
477
|
if (opts.approvalTimeoutMs !== undefined && !Number.isFinite(opts.approvalTimeoutMs)) {
|
|
453
478
|
throw new Error(`createApprovalPolicy: approvalTimeoutMs must be a finite number of ms (got ${opts.approvalTimeoutMs}) — omit it to wait indefinitely`);
|
|
@@ -481,17 +506,17 @@ export function createApprovalPolicy(opts) {
|
|
|
481
506
|
return { action: "deny", message: `tool "${req.toolName}" is denied by policy` };
|
|
482
507
|
}
|
|
483
508
|
if (nameListHolds(need, req) || namespacedCoveringHit(needCovering, req)) {
|
|
484
|
-
const attestOver = (inputDigest) => (d, settlement) => ({ ...d, attestation: mintAttestation(req, { settlement, origin: "policy", inputDigest }) });
|
|
509
|
+
const attestOver = (inputDigest) => (d, settlement, bindingRefusal) => ({ ...d, attestation: mintAttestation(req, { settlement, origin: "policy", ...(inputDigest !== undefined ? { inputDigest } : {}), ...(bindingRefusal !== undefined ? { bindingRefusal } : {}) }) });
|
|
485
510
|
const unasked = unaskedSeatAnswer(req, signal, { aborted: `approval aborted for "${req.toolName}" (run or turn interrupted)`, unreachable: `approval for "${req.toolName}" was not requested: nobody can be asked about this call` }, "policy");
|
|
486
511
|
if (unasked !== undefined)
|
|
487
512
|
return unasked;
|
|
488
|
-
const
|
|
489
|
-
if (!
|
|
490
|
-
return attestOver(
|
|
491
|
-
}
|
|
492
|
-
const askedDigest =
|
|
513
|
+
const bound = bindableArgsOf(req.toolName, req.args);
|
|
514
|
+
if (!bound.ok)
|
|
515
|
+
return attestOver(undefined)({ action: "deny", message: bound.refusal }, settledByNobody("presentation_failed"), bound.refusal);
|
|
516
|
+
const shown = { value: structuredClone(bound.value) };
|
|
517
|
+
const askedDigest = exactBoundInputHashOf(shown.value);
|
|
493
518
|
const attest = attestOver(askedDigest);
|
|
494
|
-
const heldWhileWaiting = () =>
|
|
519
|
+
const heldWhileWaiting = () => licenseCovers(askedDigest, req.args);
|
|
495
520
|
const changedWhilePending = () => attest({
|
|
496
521
|
action: "deny",
|
|
497
522
|
message: `the arguments of "${req.toolName}" changed while its approval was pending — denied fail-closed ` +
|
|
@@ -544,7 +569,7 @@ export function createApprovalPolicy(opts) {
|
|
|
544
569
|
return attest({ action: "deny", message: `approval denied for "${req.toolName}"` }, settledByDecider("person", "refused", undefined));
|
|
545
570
|
}
|
|
546
571
|
if (nameListHolds(auto, req) || namespacedCoveringHit(autoCovering, req))
|
|
547
|
-
return
|
|
572
|
+
return ALLOW;
|
|
548
573
|
if (opts.denyByDefault) {
|
|
549
574
|
return { action: "deny", message: `tool "${req.toolName}" requires explicit allow` };
|
|
550
575
|
}
|
|
@@ -587,7 +612,6 @@ export function combinePolicies(...policies) {
|
|
|
587
612
|
let asked;
|
|
588
613
|
let current = Object.freeze({ ...req });
|
|
589
614
|
let rewrite;
|
|
590
|
-
let stated;
|
|
591
615
|
let records;
|
|
592
616
|
let ruleAskText;
|
|
593
617
|
let probeMandateSeen = false;
|
|
@@ -614,10 +638,6 @@ export function combinePolicies(...policies) {
|
|
|
614
638
|
if (d.updatedInput !== undefined) {
|
|
615
639
|
current = Object.freeze(await withArgsReading(current, d.updatedInput, signal));
|
|
616
640
|
rewrite = d;
|
|
617
|
-
stated = d.action === "allow" ? d.decisionReason : undefined;
|
|
618
|
-
}
|
|
619
|
-
else if (d.action === "allow" && stated === undefined && d.decisionReason !== undefined) {
|
|
620
|
-
stated = d.decisionReason;
|
|
621
641
|
}
|
|
622
642
|
if (d.action === "allow" && d.attestation !== undefined)
|
|
623
643
|
records = mergeAttestation(req, records, d.attestation);
|
|
@@ -662,8 +682,7 @@ export function combinePolicies(...policies) {
|
|
|
662
682
|
const out = merged !== undefined ? { ...withClasses, updatedInput: merged } : withClasses;
|
|
663
683
|
return folded.size > 0 ? attest(out, req, { foldedClasses: [...folded] }) : out;
|
|
664
684
|
}
|
|
665
|
-
const
|
|
666
|
-
const allowed = stated !== undefined && base.decisionReason !== stated ? { ...base, decisionReason: stated } : base;
|
|
685
|
+
const allowed = rewrite ?? ALLOW;
|
|
667
686
|
return records === undefined ? allowed : { ...allowed, attestation: records };
|
|
668
687
|
},
|
|
669
688
|
};
|
|
@@ -713,7 +732,7 @@ export function createCoarseCommandNamePolicy(opts) {
|
|
|
713
732
|
if (allow && !allow.has(parsed.name)) {
|
|
714
733
|
return fallback(`command "${parsed.name}" is not in the allowlist`);
|
|
715
734
|
}
|
|
716
|
-
return
|
|
735
|
+
return ALLOW;
|
|
717
736
|
},
|
|
718
737
|
}, "sandbox_local");
|
|
719
738
|
}
|
|
@@ -899,7 +918,7 @@ export function refusalOf(deny, judged) {
|
|
|
899
918
|
export function foldHookAsk(verdict, hookAsk) {
|
|
900
919
|
if (verdict.action === "deny")
|
|
901
920
|
return verdict;
|
|
902
|
-
const mandate = hookAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {};
|
|
921
|
+
const mandate = { ...(hookAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {}), ...(hookAsk.crossBoundaryMandated === true ? { crossBoundaryMandated: true } : {}) };
|
|
903
922
|
if (verdict.action === "allow") {
|
|
904
923
|
return { action: "ask", ...(hookAsk.message !== undefined ? { message: hookAsk.message } : {}), decisionReason: "hook", ...mandate, ...(hookAsk.updatedInput !== undefined ? { updatedInput: hookAsk.updatedInput } : {}) };
|
|
905
924
|
}
|
|
@@ -1077,12 +1096,12 @@ export function carriesBidiControls(value, limits) {
|
|
|
1077
1096
|
}
|
|
1078
1097
|
}
|
|
1079
1098
|
export async function resolveAsk(req, onAsk, signal, onLateSettlement) {
|
|
1080
|
-
const askedDigest =
|
|
1081
|
-
const { settlement, ...r } = await resolveAskArms(req, onAsk, signal, onLateSettlement);
|
|
1099
|
+
const askedDigest = exactBoundInputHashOf(req.args);
|
|
1100
|
+
const { settlement, bindingRefusal, ...r } = await resolveAskArms(req, onAsk, signal, onLateSettlement);
|
|
1082
1101
|
if (settlement === undefined)
|
|
1083
1102
|
return r;
|
|
1084
|
-
const licensedDigest = r.action === "allow" && r.updatedInput !== undefined ?
|
|
1085
|
-
return attest(r, req, { settlement, ...(req.origin !== undefined ? { origin: req.origin } : {}), ...(req.classes !== undefined ? { classes: req.classes } : {}), inputDigest: licensedDigest });
|
|
1103
|
+
const licensedDigest = r.action === "allow" && r.updatedInput !== undefined ? exactBoundInputHashOf(r.updatedInput) : r.presentedInput !== undefined ? exactBoundInputHashOf(r.presentedInput) : askedDigest;
|
|
1104
|
+
return attest(r, req, { settlement, ...(req.origin !== undefined ? { origin: req.origin } : {}), ...(req.classes !== undefined ? { classes: req.classes } : {}), ...(licensedDigest !== undefined ? { inputDigest: licensedDigest } : {}), ...(bindingRefusal !== undefined ? { bindingRefusal } : {}) });
|
|
1086
1105
|
}
|
|
1087
1106
|
function raceAskWaitAgainstSignal(wait, signal) {
|
|
1088
1107
|
const settledWait = wait.then((value) => ({ tag: "value", value }), (error) => ({ tag: "threw", error }));
|
|
@@ -1198,10 +1217,9 @@ function readApproverOutcomeObject(ok, toolName, editLane) {
|
|
|
1198
1217
|
`re-adjudication an approver's edit gets; denied fail-closed (approve or refuse what was shown, and route edits ` +
|
|
1199
1218
|
`through the run's approver seat)`, settledByNobody("approver_contract"));
|
|
1200
1219
|
}
|
|
1201
|
-
const edit =
|
|
1202
|
-
if (!edit.ok)
|
|
1203
|
-
return
|
|
1204
|
-
}
|
|
1220
|
+
const edit = bindableArgsOf(toolName, suppliedEdit);
|
|
1221
|
+
if (!edit.ok)
|
|
1222
|
+
return { kind: "refused", message: edit.refusal, settlement: settledByNobody("presentation_failed"), binding: true };
|
|
1205
1223
|
return { kind: "allow", edit: { value: edit.value }, settlement: settledByDecider(decider, "allowed", attribution.approver), grant };
|
|
1206
1224
|
}
|
|
1207
1225
|
async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
|
|
@@ -1211,37 +1229,25 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
|
|
|
1211
1229
|
return { action: "allow", decisionReason: "mode" };
|
|
1212
1230
|
}
|
|
1213
1231
|
if (onAsk === undefined || onAsk === "deny") {
|
|
1232
|
+
const bound = bindableArgsOf(req.toolName, req.args);
|
|
1214
1233
|
return {
|
|
1215
1234
|
action: "deny",
|
|
1216
|
-
message: headlessDenialMessage(req.toolName, req.message),
|
|
1235
|
+
message: bound.ok ? headlessDenialMessage(req.toolName, req.message) : bound.refusal,
|
|
1217
1236
|
decisionReason: "mode",
|
|
1218
1237
|
settlement: settledByNobody("no_approver"),
|
|
1238
|
+
...(bound.ok ? {} : { bindingRefusal: bound.refusal }),
|
|
1219
1239
|
};
|
|
1220
1240
|
}
|
|
1221
1241
|
if (signal?.aborted) {
|
|
1222
1242
|
return { action: "deny", message: `approval aborted for "${req.toolName}" (run or turn interrupted)`, decisionReason: "mode", settlement: settledByNobody("task_aborted") };
|
|
1223
1243
|
}
|
|
1224
|
-
const presented =
|
|
1225
|
-
if (!presented.ok)
|
|
1226
|
-
return {
|
|
1227
|
-
action: "deny",
|
|
1228
|
-
message: `approval for "${req.toolName}" could not present the args safely (${presented.reason}) — denied fail-closed`,
|
|
1229
|
-
decisionReason: "mode",
|
|
1230
|
-
settlement: settledByNobody("presentation_failed"),
|
|
1231
|
-
};
|
|
1232
|
-
}
|
|
1244
|
+
const presented = bindableArgsOf(req.toolName, req.args);
|
|
1245
|
+
if (!presented.ok)
|
|
1246
|
+
return { action: "deny", message: presented.refusal, decisionReason: "mode", settlement: settledByNobody("presentation_failed"), bindingRefusal: presented.refusal };
|
|
1233
1247
|
const offered = ownReadRootCandidate(req.readRootCandidate);
|
|
1234
1248
|
let ok;
|
|
1235
1249
|
try {
|
|
1236
|
-
const approverView =
|
|
1237
|
-
if (!approverView.ok) {
|
|
1238
|
-
return {
|
|
1239
|
-
action: "deny",
|
|
1240
|
-
message: `approval for "${req.toolName}" could not present the args safely (unclonable value: ${approverView.reason}) — denied fail-closed`,
|
|
1241
|
-
decisionReason: "mode",
|
|
1242
|
-
settlement: settledByNobody("presentation_failed"),
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1250
|
+
const approverView = { value: structuredClone(presented.value) };
|
|
1245
1251
|
const bidi = carriesBidiControls(presented.value) || carriesBidiControls(req.preview);
|
|
1246
1252
|
const { hasBidiControls: _carried, ...bare } = req;
|
|
1247
1253
|
const wait = Promise.resolve(onAsk({
|
|
@@ -1295,7 +1301,7 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
|
|
|
1295
1301
|
if (typeof ok === "object" && ok !== null) {
|
|
1296
1302
|
const reading = readApproverOutcomeObject(ok, req.toolName, "readjudicated");
|
|
1297
1303
|
if (reading.kind === "refused")
|
|
1298
|
-
return { action: "deny", message: reading.message, decisionReason: "mode", settlement: reading.settlement };
|
|
1304
|
+
return { action: "deny", message: reading.message, decisionReason: "mode", settlement: reading.settlement, ...(reading.binding === true ? { bindingRefusal: reading.message } : {}) };
|
|
1299
1305
|
if (reading.kind === "allow" && reading.settlement.who.party === "policy" && req.requiresRealApproval === true) {
|
|
1300
1306
|
return { ...unjudgedAllowRefused(req, "policy"), ...readRootGrantSeat(reading.grant, offered, false) };
|
|
1301
1307
|
}
|