@sema-agent/core 7.50.2 → 7.51.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 +313 -0
- package/dist/agents/child-failure.d.ts +5 -16
- package/dist/agents/child-failure.js +4 -24
- package/dist/agents/subagent-steps.js +2 -2
- package/dist/agents/subagent.d.ts +4 -13
- package/dist/agents/subagent.js +94 -204
- package/dist/agents/tool-filter.d.ts +2 -2
- package/dist/brain/anthropic.d.ts +1 -1
- package/dist/brain/anthropic.js +7 -5
- package/dist/brain/open-responses.d.ts +1 -1
- package/dist/brain/open-responses.js +3 -2
- package/dist/brain/openai.d.ts +1 -1
- package/dist/brain/openai.js +3 -2
- package/dist/brain/request-route.d.ts +2 -2
- package/dist/brain/route-adjudicator.d.ts +16 -7
- package/dist/brain/route-adjudicator.js +23 -7
- package/dist/brain/route-conformance.js +23 -5
- package/dist/brain/stream-engine.js +9 -3
- package/dist/core/a2a.js +6 -8
- package/dist/core/allow-entry.d.ts +45 -26
- package/dist/core/allow-entry.js +48 -24
- package/dist/core/arg-summary.d.ts +8 -85
- package/dist/core/arg-summary.js +4 -386
- package/dist/core/ask-origin.d.ts +35 -33
- package/dist/core/ask-origin.js +9 -11
- package/dist/core/checkpoint-requirements.d.ts +6 -0
- package/dist/core/checkpoint-requirements.js +1 -0
- package/dist/core/checkpoint-store.d.ts +15 -34
- package/dist/core/checkpoint-store.js +5 -4
- package/dist/core/classifier-unavailable-arm.d.ts +12 -8
- package/dist/core/classifier-unavailable-arm.js +7 -2
- package/dist/core/credential-vocabulary.d.ts +3 -2
- package/dist/core/credential-vocabulary.js +27 -18
- package/dist/core/delegation-frames.d.ts +40 -15
- package/dist/core/delegation-frames.js +34 -0
- package/dist/core/engine-capabilities.d.ts +15 -0
- package/dist/core/engine-capabilities.js +1 -0
- package/dist/core/engine-notice.d.ts +41 -14
- package/dist/core/gate-fold.js +9 -7
- package/dist/core/gate-lanes.js +6 -4
- package/dist/core/gate-outcome.d.ts +81 -15
- package/dist/core/gate-outcome.js +168 -67
- package/dist/core/governance-codes.d.ts +7 -1
- package/dist/core/governance-codes.js +9 -0
- package/dist/core/hooks.d.ts +4 -4
- package/dist/core/hooks.js +4 -3
- package/dist/core/mcp-connection-pool.d.ts +2 -0
- package/dist/core/mcp-connection-pool.js +2 -1
- package/dist/core/mcp-failure.js +2 -1
- package/dist/core/mcp-server-spec.d.ts +27 -5
- package/dist/core/mcp.d.ts +13 -38
- package/dist/core/mcp.js +28 -23
- package/dist/core/media-byte-cap.d.ts +25 -0
- package/dist/core/media-byte-cap.js +18 -2
- package/dist/core/memory-engine/consolidation-driver.js +1 -1
- package/dist/core/mode-policy.d.ts +25 -1
- package/dist/core/mode-policy.js +28 -13
- package/dist/core/own-property-receipt.d.ts +18 -0
- package/dist/core/own-property-receipt.js +39 -0
- package/dist/core/permission-rule-model.d.ts +32 -11
- package/dist/core/permission-rule-model.js +12 -7
- package/dist/core/persisted-rule-arms.d.ts +3 -3
- package/dist/core/prompt-section-seat.d.ts +54 -0
- package/dist/core/prompt-section-seat.js +49 -0
- package/dist/core/protocol-http-auth.d.ts +44 -0
- package/dist/core/protocol-http-auth.js +45 -0
- package/dist/core/read-face-seats.d.ts +30 -0
- package/dist/core/read-face-seats.js +38 -0
- package/dist/core/redaction-passes.d.ts +82 -0
- package/dist/core/redaction-passes.js +378 -0
- package/dist/core/remote-env.d.ts +4 -3
- package/dist/core/resume-startup-cause.d.ts +56 -0
- package/dist/core/resume-startup-cause.js +43 -0
- package/dist/core/rule-denial-sentence.d.ts +2 -2
- package/dist/core/runner/announce-once-ledger.js +2 -2
- package/dist/core/runner/classifier-ladder.d.ts +17 -13
- package/dist/core/runner/classifier-ladder.js +12 -10
- package/dist/core/runner/construction-doors.js +9 -20
- package/dist/core/runner/contracts.d.ts +4 -4
- package/dist/core/runner/gate-exit.js +2 -2
- package/dist/core/runner/memory-consolidation-write-judge.d.ts +53 -9
- package/dist/core/runner/memory-consolidation-write-judge.js +39 -1
- package/dist/core/runner/permission-rule-lanes.d.ts +8 -13
- package/dist/core/runner/person-path-rows.d.ts +4 -3
- package/dist/core/runner/policy-throw.js +4 -4
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -3
- package/dist/core/runner/prepare-caps-and-workflow.js +8 -6
- package/dist/core/runner/prepare-gate-stations.js +4 -4
- package/dist/core/runner/prepare-hands-readface.js +1 -19
- package/dist/core/runner/prepare-inherited-gate.js +10 -9
- package/dist/core/runner/prepare-policy-chain.d.ts +3 -5
- package/dist/core/runner/prepare-policy-chain.js +10 -13
- package/dist/core/runner/prepare-project-context.d.ts +4 -4
- package/dist/core/runner/prepare-project-context.js +2 -1
- package/dist/core/runner/prepare-prompt-assembly.js +1 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -1
- package/dist/core/runner/prepare-prompt-inputs.js +4 -4
- package/dist/core/runner/prepare-protocol-tools.d.ts +4 -3
- package/dist/core/runner/prepare-protocol-tools.js +3 -2
- package/dist/core/runner/prepare-task.js +6 -4
- package/dist/core/runner/prepare-turn-wiring.d.ts +16 -13
- package/dist/core/runner/prepare-turn-wiring.js +8 -7
- package/dist/core/runner/run-harness-handlers.js +2 -2
- package/dist/core/runner/run-leg.js +10 -9
- package/dist/core/runner/run-settle-and-teardown.js +5 -5
- package/dist/core/runner/run-turn-boundary.js +3 -2
- package/dist/core/runner/runtask.d.ts +1 -2
- package/dist/core/runner/runtask.js +8 -12
- package/dist/core/runner/session-rule-policy.d.ts +15 -11
- package/dist/core/runner/session-rule-policy.js +43 -33
- package/dist/core/runner/stream-lifecycle-verbs.js +2 -2
- package/dist/core/runner/stream-settle-backstop.js +2 -1
- package/dist/core/runner/sub-agent-deny-lists.d.ts +12 -8
- package/dist/core/runner/sub-agent-deny-lists.js +23 -12
- package/dist/core/runner/terminal-account.js +3 -3
- package/dist/core/runner/tool-output-projection.d.ts +5 -0
- package/dist/core/runner/tool-output-projection.js +6 -11
- package/dist/core/runner/tool-result-media-seat.d.ts +19 -0
- package/dist/core/runner/tool-result-media-seat.js +13 -0
- package/dist/core/runner/turn-attachments.d.ts +13 -12
- package/dist/core/runner/turn-attachments.js +11 -7
- package/dist/core/runner-deps.d.ts +4 -3
- package/dist/core/safety-merge-corpus.js +21 -21
- package/dist/core/secret-env.d.ts +41 -15
- package/dist/core/secret-env.js +85 -19
- package/dist/core/session-policy-store.d.ts +89 -72
- package/dist/core/session-policy-store.js +100 -93
- package/dist/core/shell-read-silence.d.ts +38 -4
- package/dist/core/shell-read-silence.js +102 -0
- package/dist/core/side-query.d.ts +7 -8
- package/dist/core/skill-grant-binding.d.ts +32 -11
- package/dist/core/skill-grant-binding.js +11 -3
- package/dist/core/skill-grant.d.ts +4 -3
- package/dist/core/skill-grant.js +5 -4
- package/dist/core/skills-directory.d.ts +12 -1
- package/dist/core/skills-directory.js +17 -8
- package/dist/core/spec-contract.d.ts +5 -3
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +3 -0
- package/dist/core/swappable-deps.d.ts +15 -1
- package/dist/core/swappable-deps.js +25 -1
- package/dist/core/synthetic-read-verdict.d.ts +16 -14
- package/dist/core/synthetic-read-verdict.js +28 -8
- package/dist/core/task-notification.d.ts +13 -3
- package/dist/core/task-notification.js +12 -1
- package/dist/core/task-spec.d.ts +14 -15
- package/dist/core/tool-name-readings.d.ts +0 -4
- package/dist/core/tool-name-readings.js +0 -1
- package/dist/core/tool-policy.d.ts +40 -9
- package/dist/core/tool-policy.js +25 -7
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.js +1 -1
- package/dist/core/untrusted-egress.d.ts +60 -11
- package/dist/core/untrusted-egress.js +120 -9
- package/dist/core/untrusted-text.js +16 -0
- package/dist/core/wiring-manifest.d.ts +22 -8
- package/dist/core/wiring-manifest.js +12 -4
- package/dist/engine/execution-env/kill-tree.js +4 -4
- package/dist/engine/execution-env/node-execution-env.d.ts +11 -4
- package/dist/engine/execution-env/node-execution-env.js +9 -4
- package/dist/engine/harness/agent-harness.d.ts +2 -1
- package/dist/engine/harness/agent-harness.js +4 -1
- package/dist/engine/harness/types.d.ts +3 -0
- package/dist/engine/llm/types.d.ts +7 -9
- package/dist/engine/loop/agent-loop.d.ts +8 -3
- package/dist/engine/loop/agent-loop.js +26 -9
- package/dist/engine/loop/types.d.ts +10 -0
- package/dist/engine/lsp/node-lsp-manager.d.ts +2 -1
- package/dist/engine/lsp/node-lsp-manager.js +2 -2
- package/dist/index.d.ts +22 -5
- package/dist/index.js +18 -5
- package/dist/orchestration/run-workflow-tool.js +8 -8
- package/dist/orchestration/workflow.js +2 -2
- package/dist/prompt-assembly/assemble.d.ts +3 -1
- package/dist/prompt-assembly/assemble.js +6 -0
- package/dist/prompts/cowork.d.ts +42 -0
- package/dist/prompts/cowork.js +529 -0
- package/dist/prompts/default.d.ts +14 -1
- package/dist/prompts/default.js +3 -0
- package/dist/stores/file/fs-atomic.js +2 -2
- package/dist/stores/file/session-policy-store.d.ts +6 -3
- package/dist/stores/file/session-policy-store.js +5 -16
- package/dist/tools/artifact/artifact-tool.js +2 -0
- package/dist/tools/fs/image-admission.d.ts +57 -4
- package/dist/tools/fs/image-admission.js +85 -0
- package/dist/tools/fs/path-containment.d.ts +2 -0
- package/dist/tools/fs/path-containment.js +5 -0
- package/dist/tools/fs/shell-read-check.d.ts +31 -6
- package/dist/tools/fs/shell-read-check.js +2 -4
- package/dist/tools/fs/shell-read-environment.d.ts +10 -25
- package/dist/tools/fs/shell-read-environment.js +6 -18
- package/dist/tools/fs/shell-read-establish.d.ts +3 -2
- package/dist/tools/fs/shell-read-establish.js +5 -5
- package/dist/tools/fs/shell-read-judge.d.ts +38 -8
- package/dist/tools/fs/shell-read-judge.js +12 -10
- package/dist/tools/web.js +2 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +112 -10
- package/wire-changes/core@7.51.0.json +2421 -0
- package/wire-changes/core@7.51.1.json +259 -0
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { posix } from "node:path";
|
|
2
2
|
import { isWinFormPath } from "../tools/fs/safety.js";
|
|
3
3
|
import { NAMESPACED_NAME_SHAPES, carriesNamespaceSeparator } from "./protocol-table.js";
|
|
4
|
-
import {
|
|
4
|
+
import { compileAllowList, inertEntryPhrase, readRuleEntry, renderListEntry } from "./allow-entry.js";
|
|
5
|
+
import { valueTypeLabel } from "./tool-name-list.js";
|
|
6
|
+
const SESSION_RULE_BUCKETS = { toolAllow: "allow", toolDeny: "deny", allowDirs: "dirs" };
|
|
7
|
+
const BUCKETS = Object.keys(SESSION_RULE_BUCKETS);
|
|
8
|
+
const ENTRY_FIELDS = BUCKETS.filter((f) => SESSION_RULE_BUCKETS[f] !== "dirs");
|
|
9
|
+
const STORE_STAMPS = new Set(["rev", "gen"]);
|
|
5
10
|
export class SessionPolicyError extends Error {
|
|
6
11
|
code;
|
|
7
12
|
detail;
|
|
@@ -12,8 +17,6 @@ export class SessionPolicyError extends Error {
|
|
|
12
17
|
this.name = "SessionPolicyError";
|
|
13
18
|
}
|
|
14
19
|
}
|
|
15
|
-
const HOLDS_TOOL_NAMES = { toolAllow: true, toolDeny: true, allowDirs: false, commandAllow: false, commandDeny: false };
|
|
16
|
-
const TOOL_NAME_FIELDS = Object.keys(HOLDS_TOOL_NAMES).filter((f) => HOLDS_TOOL_NAMES[f]);
|
|
17
20
|
const NO_DECLARED_TOOLS = new Set();
|
|
18
21
|
export function readMountedToolNames(value) {
|
|
19
22
|
if (value === undefined)
|
|
@@ -33,35 +36,6 @@ export function readMountedToolNames(value) {
|
|
|
33
36
|
}
|
|
34
37
|
return names;
|
|
35
38
|
}
|
|
36
|
-
export function assertSessionRuleNamesCurrent(rules, mountedToolNames = NO_DECLARED_TOOLS) {
|
|
37
|
-
for (const field of TOOL_NAME_FIELDS) {
|
|
38
|
-
for (const name of rules[field] ?? []) {
|
|
39
|
-
const verdict = judgeRuleToolName(name, mountedToolNames);
|
|
40
|
-
if (verdict.kind === "current")
|
|
41
|
-
continue;
|
|
42
|
-
const quoted = JSON.stringify(name);
|
|
43
|
-
const successor = verdict.kind === "unprefixed_namespace" ? undefined : verdict.currentName;
|
|
44
|
-
const guidance = verdict.kind === "unprefixed_namespace" ? undefined : verdict.guidance;
|
|
45
|
-
let message;
|
|
46
|
-
switch (verdict.kind) {
|
|
47
|
-
case "retired":
|
|
48
|
-
message = `session rules ${field} names ${quoted}, a retired tool name (${verdict.guidance}) — with it in place no run of this session could start, so nothing was written.${successor !== undefined ? ` Use the current name ${JSON.stringify(successor)}.` : ""} If this deployment mounts its own tool under ${quoted}, declare it in the store's mountedToolNames.`;
|
|
49
|
-
break;
|
|
50
|
-
case "unprefixed_namespace":
|
|
51
|
-
message = `session rules ${field} names ${quoted}, which carries the "__" namespace separator but no protocol prefix — no tool is named that way (protocol tools are named ${NAMESPACED_NAME_SHAPES}), and with it in place no run of this session could start, so nothing was written.`;
|
|
52
|
-
break;
|
|
53
|
-
case "unmintable_segment":
|
|
54
|
-
message = `session rules ${field} names ${quoted}, which can match no tool (${verdict.guidance}) — with it in place no run of this session could start, so nothing was written.${successor !== undefined ? ` Write ${JSON.stringify(successor)}.` : ""}`;
|
|
55
|
-
break;
|
|
56
|
-
default: {
|
|
57
|
-
const exhaustive = verdict;
|
|
58
|
-
return exhaustive;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
throw new SessionPolicyError("legacy_tool_name", message, { field, name, verdict: verdict.kind, ...(guidance !== undefined ? { guidance } : {}), ...(successor !== undefined ? { currentName: successor } : {}) });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
39
|
function lexWithin(root, p) {
|
|
66
40
|
const fold = (x) => {
|
|
67
41
|
let n = isWinFormPath(x) ? x.replace(/\\/g, "/") : x;
|
|
@@ -75,61 +49,109 @@ function lexWithin(root, p) {
|
|
|
75
49
|
return true;
|
|
76
50
|
return np === nr || np.startsWith(nr + "/");
|
|
77
51
|
}
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
};
|
|
84
|
-
if (Array.isArray(value) && Array.from(value).every((entry) => typeof entry === "string")) {
|
|
85
|
-
if (value.some((entry) => entry.trim() === ""))
|
|
86
|
-
refuse("an array holding an empty or blank entry");
|
|
87
|
-
return value;
|
|
52
|
+
export function readSessionRulesShape(value) {
|
|
53
|
+
const fault = (field, got, sentence) => ({ fault: { field, got }, sentence });
|
|
54
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
55
|
+
const got = Array.isArray(value) ? "an array" : valueTypeLabel(value);
|
|
56
|
+
return fault("", got, `session rules must be an object of rule buckets (${BUCKETS.join(", ")}), got ${got}`);
|
|
88
57
|
}
|
|
89
|
-
|
|
58
|
+
const rules = {};
|
|
59
|
+
for (const [key, v] of Object.entries(value)) {
|
|
60
|
+
if (STORE_STAMPS.has(key) || v === undefined)
|
|
61
|
+
continue;
|
|
62
|
+
if (!Object.hasOwn(SESSION_RULE_BUCKETS, key)) {
|
|
63
|
+
return fault(key, "not a rule bucket", `session rules hold ${JSON.stringify(key)}, which is not a rule bucket — the buckets are ${BUCKETS.join(", ")}, and a shell command rule is a toolDeny / toolAllow entry written Bash(<command>) (Bash(rm:*) matches a command whose first word is rm)`);
|
|
64
|
+
}
|
|
65
|
+
const field = key;
|
|
66
|
+
const listed = Array.isArray(v) ? Array.from(v) : undefined;
|
|
67
|
+
const got = listed === undefined
|
|
68
|
+
? valueTypeLabel(v)
|
|
69
|
+
: !listed.every((e) => typeof e === "string")
|
|
70
|
+
? "an array holding an entry that is not a string"
|
|
71
|
+
: SESSION_RULE_BUCKETS[field] === "dirs" && listed.some((e) => e.trim() === "")
|
|
72
|
+
? "an array holding an empty or blank entry"
|
|
73
|
+
: undefined;
|
|
74
|
+
if (got !== undefined)
|
|
75
|
+
return fault(field, got, `session rules ${field} must be an array of ${SESSION_RULE_BUCKETS[field] === "dirs" ? "directory paths" : "rule entries"}, got ${got} — a bucket is written as a list (["Bash(rm:*)"], not "Bash(rm:*)")`);
|
|
76
|
+
rules[field] = listed;
|
|
77
|
+
}
|
|
78
|
+
return { rules };
|
|
90
79
|
}
|
|
91
80
|
export function normalizeRules(rules) {
|
|
81
|
+
const read = readSessionRulesShape(rules);
|
|
82
|
+
if ("fault" in read)
|
|
83
|
+
throw new SessionPolicyError("invalid_shape", read.sentence, read.fault);
|
|
92
84
|
const out = {};
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
out.toolAllow = [...toolAllow];
|
|
100
|
-
if (toolDeny !== undefined)
|
|
101
|
-
out.toolDeny = [...toolDeny];
|
|
102
|
-
if (commandAllow !== undefined)
|
|
103
|
-
out.commandAllow = [...commandAllow];
|
|
104
|
-
if (commandDeny !== undefined)
|
|
105
|
-
out.commandDeny = [...commandDeny];
|
|
106
|
-
if (allowDirs !== undefined)
|
|
107
|
-
out.allowDirs = allowDirs.map((d) => posix.normalize(d).replace(/\/+$/, "") || "/");
|
|
85
|
+
for (const field of BUCKETS) {
|
|
86
|
+
const list = read.rules[field];
|
|
87
|
+
if (list === undefined)
|
|
88
|
+
continue;
|
|
89
|
+
out[field] = SESSION_RULE_BUCKETS[field] === "dirs" ? list.map((d) => posix.normalize(d).replace(/\/+$/, "") || "/") : [...list];
|
|
90
|
+
}
|
|
108
91
|
return out;
|
|
109
92
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
93
|
+
function effectiveEntries(field, list, mountedToolNames) {
|
|
94
|
+
return compileAllowList(list ?? [], SESSION_RULE_BUCKETS[field], mountedToolNames).rows.map((r) => r.entry);
|
|
95
|
+
}
|
|
96
|
+
function assertEntriesRead(rules, mountedToolNames) {
|
|
97
|
+
for (const field of ENTRY_FIELDS) {
|
|
98
|
+
for (const entry of rules[field] ?? []) {
|
|
99
|
+
const read = readRuleEntry(entry, SESSION_RULE_BUCKETS[field], mountedToolNames);
|
|
100
|
+
if (read.kind === "row")
|
|
116
101
|
continue;
|
|
117
|
-
|
|
102
|
+
const { kind: _kind, ...why } = read;
|
|
103
|
+
void _kind;
|
|
104
|
+
const way = why.verdict === "retired"
|
|
105
|
+
? ` If this deployment mounts its own tool under ${renderListEntry(entry)}, declare it in the store's mountedToolNames.`
|
|
106
|
+
: why.currentName === undefined
|
|
107
|
+
? ` An entry is a tool name (Read; a protocol tool is named ${NAMESPACED_NAME_SHAPES}), a protocol namespace (mcp__<server>), or a rule in its tool's own grammar (Bash(<command>), Edit(<path>), Skill(<name>), WebFetch(domain:<host>)).`
|
|
108
|
+
: "";
|
|
109
|
+
const sentence = why.guidance !== undefined ? ` — ${why.guidance}` : "";
|
|
110
|
+
throw new SessionPolicyError("inert_entry", `session rules ${field} holds ${inertEntryPhrase({ entry, ...why })}, which ${SESSION_RULE_BUCKETS[field] === "deny" ? "refuses" : "admits"} nothing as written${sentence}; nothing was written.${way}`, { field, entry, ...why });
|
|
118
111
|
}
|
|
119
112
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
113
|
+
}
|
|
114
|
+
export function judgeSessionRulesWrite(prior, rules, opts, mountedToolNames = NO_DECLARED_TOOLS) {
|
|
115
|
+
if (opts?.expectedRev !== undefined && (prior?.rev ?? 0) !== opts.expectedRev) {
|
|
116
|
+
throw new SessionPolicyError("conflict", `session rules rev mismatch (expected ${opts.expectedRev}, have ${prior?.rev ?? 0})`);
|
|
117
|
+
}
|
|
118
|
+
const clean = normalizeRules(rules);
|
|
119
|
+
assertEntriesRead(clean, mountedToolNames);
|
|
120
|
+
if (prior !== null && opts?.operator !== true) {
|
|
121
|
+
const stored = readSessionRulesShape(prior);
|
|
122
|
+
if ("fault" in stored) {
|
|
123
|
+
throw new SessionPolicyError("corrupt", `the stored session rules cannot be read as rules (${stored.sentence}) — refusing to judge a non-operator write against them as fewer rules; an operator write replaces them`);
|
|
124
|
+
}
|
|
125
|
+
const loosens = loosenReasons(stored.rules, clean, mountedToolNames);
|
|
126
|
+
if (loosens.length > 0) {
|
|
127
|
+
throw new SessionPolicyError("loosen_forbidden", `a non-operator session-rule write may only tighten; refused: ${loosens.join("; ")}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return clean;
|
|
131
|
+
}
|
|
132
|
+
export function loosenReasons(priorValue, nextValue, mountedToolNames = NO_DECLARED_TOOLS) {
|
|
133
|
+
const shapeOf = (side, value) => {
|
|
134
|
+
const read = readSessionRulesShape(value);
|
|
135
|
+
if ("fault" in read)
|
|
136
|
+
throw new SessionPolicyError("invalid_shape", `the ${side} rules: ${read.sentence}`, read.fault);
|
|
137
|
+
return read.rules;
|
|
138
|
+
};
|
|
139
|
+
const prior = shapeOf("prior", priorValue);
|
|
140
|
+
const next = shapeOf("next", nextValue);
|
|
141
|
+
const reasons = [];
|
|
142
|
+
const keptDeny = new Set(effectiveEntries("toolDeny", next.toolDeny, mountedToolNames));
|
|
143
|
+
for (const x of effectiveEntries("toolDeny", prior.toolDeny, mountedToolNames))
|
|
144
|
+
if (!keptDeny.has(x))
|
|
145
|
+
reasons.push(`removed toolDeny ${renderListEntry(x)}`);
|
|
146
|
+
if (prior.toolAllow !== undefined) {
|
|
147
|
+
if (next.toolAllow === undefined)
|
|
148
|
+
reasons.push("removed toolAllow allowlist (all allowed)");
|
|
149
|
+
else {
|
|
150
|
+
const had = new Set(effectiveEntries("toolAllow", prior.toolAllow, mountedToolNames));
|
|
151
|
+
for (const x of effectiveEntries("toolAllow", next.toolAllow, mountedToolNames))
|
|
152
|
+
if (!had.has(x))
|
|
153
|
+
reasons.push(`widened toolAllow with ${renderListEntry(x)}`);
|
|
128
154
|
}
|
|
129
|
-
const ps = new Set(p);
|
|
130
|
-
for (const x of n)
|
|
131
|
-
if (!ps.has(x))
|
|
132
|
-
reasons.push(`widened ${field} with "${x}"`);
|
|
133
155
|
}
|
|
134
156
|
const pd = prior.allowDirs;
|
|
135
157
|
if (pd !== undefined && pd.length > 0) {
|
|
@@ -146,11 +168,6 @@ export function loosenReasons(prior, next, mountedToolNames = NO_DECLARED_TOOLS)
|
|
|
146
168
|
export function continuesLineage(a, b) {
|
|
147
169
|
return (a.gen ?? 0) === (b.gen ?? 0);
|
|
148
170
|
}
|
|
149
|
-
export function stripRev(rules) {
|
|
150
|
-
const { ...rest } = rules;
|
|
151
|
-
delete rest.rev;
|
|
152
|
-
return rest;
|
|
153
|
-
}
|
|
154
171
|
export class InMemorySessionPolicyStore {
|
|
155
172
|
map = new Map();
|
|
156
173
|
mountedToolNames;
|
|
@@ -168,17 +185,7 @@ export class InMemorySessionPolicyStore {
|
|
|
168
185
|
async putRules(sessionId, principal, rules, opts) {
|
|
169
186
|
const key = this.key(sessionId, principal);
|
|
170
187
|
const prior = this.map.get(key);
|
|
171
|
-
|
|
172
|
-
throw new SessionPolicyError("conflict", `session rules rev mismatch (expected ${opts.expectedRev}, have ${prior?.rev ?? 0})`);
|
|
173
|
-
}
|
|
174
|
-
const clean = normalizeRules(stripRev(rules));
|
|
175
|
-
assertSessionRuleNamesCurrent(clean, this.mountedToolNames);
|
|
176
|
-
if (prior && opts?.operator !== true) {
|
|
177
|
-
const loosens = loosenReasons(prior, clean, this.mountedToolNames);
|
|
178
|
-
if (loosens.length > 0) {
|
|
179
|
-
throw new SessionPolicyError("loosen_forbidden", `a non-operator session-rule write may only tighten; refused: ${loosens.join("; ")}`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
188
|
+
const clean = judgeSessionRulesWrite(prior ?? null, rules, opts, this.mountedToolNames);
|
|
182
189
|
const mark = this.highWater.get(key) ?? { rev: 0, gen: 0 };
|
|
183
190
|
const floor = Math.max(prior?.rev ?? 0, mark.rev);
|
|
184
191
|
const gen = prior?.gen ?? mark.gen;
|
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
import type { ShellReadVerdict } from "../tools/fs/shell-read-check.js";
|
|
2
|
+
import { type ShellReadDisposition } from "../tools/fs/shell-read-judge.js";
|
|
2
3
|
import type { GatePass } from "./runner/gate-exit.js";
|
|
3
4
|
import type { AskGuard } from "./tool-policy.js";
|
|
5
|
+
/** One station answer, received and read: what a reader acts on (`kind`) and the answer it acts on (`verdict` — the engine's own
|
|
6
|
+
* copy, received once). `unbelieved`: the station's answer was not received or contradicted its own finding, and `verdict` is the
|
|
7
|
+
* engine's refusal of it (its sentence names the station's words only where they are words of the closed sets — nothing a station
|
|
8
|
+
* spelled reaches the model), reported once on the call's lane ({@link StationAnswerCall.report}). */
|
|
9
|
+
export interface StationAnswerReading {
|
|
10
|
+
readonly kind: ShellReadDisposition | "silence";
|
|
11
|
+
readonly verdict: ShellReadVerdict;
|
|
12
|
+
readonly unbelieved?: true;
|
|
13
|
+
}
|
|
14
|
+
/** The call a reader reads an answer for: its name (the refusal names it), its id, and the lane a station's failure is reported on
|
|
15
|
+
* (absent ⇒ unreported — the resumed leg reads the engine's own stations). */
|
|
16
|
+
export interface StationAnswerCall {
|
|
17
|
+
readonly toolName: string;
|
|
18
|
+
readonly toolCallId?: string;
|
|
19
|
+
readonly report?: (err: Error) => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Receive and read a station's answer — the ONE reading for every reader. The answer is received whole ({@link receivedAnswer}:
|
|
23
|
+
* the engine's one receipt for a caller's object, then each member by its kind — none of the caller's code runs) into the engine's
|
|
24
|
+
* copy every later read is of; an answer that is not received (an accessor, a member its prototype or a non-enumerable property
|
|
25
|
+
* supplies, a Proxy anywhere in it, a word that is no string, a mark that is not `true`, not an object) is not believed. Then, in order:
|
|
26
|
+
* · `refuse` stands, whatever beside it: no reading is stricter;
|
|
27
|
+
* · the station's SILENCE — the finding `unanswered`, or nothing found (`none`) beside a silence mark (`environmentSilent`,
|
|
28
|
+
* `rowsUnread`): the station made no finding, whatever disposition it spelled beside it (the engine's own station spells a
|
|
29
|
+
* vouch beside unread rows as `unanswered`);
|
|
30
|
+
* · a disposition the decision table does not give the finding on any seat, under any form of the question
|
|
31
|
+
* ({@link dispositionsOfRow}) — a `vouch` beside a deny hit, an ask beside one, a word outside the closed sets — is NOT believed:
|
|
32
|
+
* the station's answer contradicts its own finding, and the reader refuses the call;
|
|
33
|
+
* · otherwise the disposition, as the table gives it.
|
|
34
|
+
* A silence mark beside a finding the station DID make is not the silence: it classes that finding's question
|
|
35
|
+
* ({@link silencedBy}).
|
|
36
|
+
*/
|
|
37
|
+
export declare function readStationAnswer(answer: ShellReadVerdict, call: StationAnswerCall): StationAnswerReading;
|
|
4
38
|
/** Whose silence a station's answer carries, each its own class on the question it keeps or mints: unread rows are a rule that could
|
|
5
39
|
* not be read (the rule lane's class); the finding `unanswered` is, unless rows are unread, the environment's silence — shell text the
|
|
6
40
|
* engine cannot read (a guard) — and the marks only pick whose. Either is a question only a person answers; `probe` follows the finding. */
|
|
@@ -12,10 +46,10 @@ export declare function silencedBy(verdict: ShellReadVerdict, d: {
|
|
|
12
46
|
guards?: readonly AskGuard[];
|
|
13
47
|
requiresRealApproval?: true;
|
|
14
48
|
};
|
|
15
|
-
/** The
|
|
16
|
-
* spell `vouch` or `
|
|
17
|
-
* gains the silence's class; an allow becomes the silence's question (`ask.message`), one only a person
|
|
18
|
-
* no `probe` is stamped and no grant is offered — the station made no finding. */
|
|
49
|
+
/** The station's SILENCE ({@link readStationAnswer}), applied once, before any disposition arm: a station — a host-mounted one
|
|
50
|
+
* included — may spell `vouch`, `mandated` or `ordinary` beside it, and no word vouches for what nobody judged. A deny stands; an
|
|
51
|
+
* ask keeps its words and gains the silence's class; an allow becomes the silence's question (`ask.message`), one only a person
|
|
52
|
+
* answers. Nothing is admitted, no `probe` is stamped and no grant is offered — the station made no finding. */
|
|
19
53
|
export declare function applyUnansweredSilence(pass: GatePass, verdict: ShellReadVerdict, ask: {
|
|
20
54
|
readonly message: string;
|
|
21
55
|
readonly account: string | undefined;
|
|
@@ -1,3 +1,105 @@
|
|
|
1
|
+
import { types } from "node:util";
|
|
2
|
+
import { dispositionsOfRow, SHELL_READ_FINDINGS } from "../tools/fs/shell-read-judge.js";
|
|
3
|
+
import { receiveContract } from "./decision-receipt.js";
|
|
4
|
+
import { egressThrownText } from "./untrusted-egress.js";
|
|
5
|
+
const DISPOSITIONS = ["refuse", "mandated", "ordinary", "vouch"];
|
|
6
|
+
const MEMBER_KINDS = {
|
|
7
|
+
disposition: "word",
|
|
8
|
+
finding: "word",
|
|
9
|
+
environmentSilent: "mark",
|
|
10
|
+
rowsUnread: "mark",
|
|
11
|
+
face: "face",
|
|
12
|
+
sentence: "word",
|
|
13
|
+
details: "record",
|
|
14
|
+
readRootCandidate: "record",
|
|
15
|
+
unestablished: "mark",
|
|
16
|
+
writesPlaced: "mark",
|
|
17
|
+
writeProtected: "mark",
|
|
18
|
+
targetUnresolved: "mark",
|
|
19
|
+
unplacedOptions: "record",
|
|
20
|
+
admit: "admission",
|
|
21
|
+
};
|
|
22
|
+
const VERDICT_MEMBERS = Object.keys(MEMBER_KINDS);
|
|
23
|
+
const DATA_DEPTH = 8;
|
|
24
|
+
function ownedData(value, depth, path) {
|
|
25
|
+
if (value === null || value === undefined || typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
26
|
+
return { ok: true, value };
|
|
27
|
+
if (typeof value !== "object" || depth >= DATA_DEPTH || types.isProxy(value) || path.has(value))
|
|
28
|
+
return { ok: false };
|
|
29
|
+
const list = Array.isArray(value);
|
|
30
|
+
const proto = Object.getPrototypeOf(value);
|
|
31
|
+
if (list ? proto !== Array.prototype : proto !== Object.prototype && proto !== null)
|
|
32
|
+
return { ok: false };
|
|
33
|
+
const own = Object.getOwnPropertyDescriptors(value);
|
|
34
|
+
path.add(value);
|
|
35
|
+
try {
|
|
36
|
+
const out = list ? [] : {};
|
|
37
|
+
for (const key of Object.keys(own)) {
|
|
38
|
+
if (list && key === "length")
|
|
39
|
+
continue;
|
|
40
|
+
const d = own[key];
|
|
41
|
+
if (!Object.hasOwn(d, "value") || d.enumerable !== true)
|
|
42
|
+
return { ok: false };
|
|
43
|
+
const inner = ownedData(d.value, depth + 1, path);
|
|
44
|
+
if (!inner.ok)
|
|
45
|
+
return { ok: false };
|
|
46
|
+
Object.defineProperty(out, key, { value: inner.value, enumerable: true, writable: true, configurable: true });
|
|
47
|
+
}
|
|
48
|
+
return { ok: true, value: out };
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
path.delete(value);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function receivedAnswer(answer, call) {
|
|
55
|
+
if (typeof answer !== "object" || answer === null)
|
|
56
|
+
return undefined;
|
|
57
|
+
const outer = receiveContract(answer, VERDICT_MEMBERS, "");
|
|
58
|
+
if (outer.kind !== "held")
|
|
59
|
+
return undefined;
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const [member, kind] of Object.entries(MEMBER_KINDS)) {
|
|
62
|
+
let v = outer.held[member];
|
|
63
|
+
if (v === undefined)
|
|
64
|
+
continue;
|
|
65
|
+
if (kind === "word" ? typeof v !== "string" : kind === "mark" ? v !== true : kind === "face" ? v !== "write" : kind === "admission" ? typeof v !== "function" || types.isProxy(v) : false)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (kind === "record") {
|
|
68
|
+
const data = ownedData(v, 0, new Set());
|
|
69
|
+
if (!data.ok || typeof data.value !== "object" || data.value === null || Array.isArray(data.value))
|
|
70
|
+
return undefined;
|
|
71
|
+
v = data.value;
|
|
72
|
+
}
|
|
73
|
+
if (kind === "admission") {
|
|
74
|
+
const admission = v;
|
|
75
|
+
v = () => {
|
|
76
|
+
try {
|
|
77
|
+
Reflect.apply(admission, undefined, []);
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
call.report?.(new Error(`shell read station's admission threw for tool "${call.toolName}"${call.toolCallId !== undefined ? ` (toolCallId ${call.toolCallId})` : ""} — nothing was admitted: ${egressThrownText("same-principal", e)}`));
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
Object.defineProperty(out, member, { value: v, enumerable: true, writable: true, configurable: true });
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
export function readStationAnswer(answer, call) {
|
|
89
|
+
const verdict = receivedAnswer(answer, call);
|
|
90
|
+
if (verdict?.disposition === "refuse")
|
|
91
|
+
return { kind: "refuse", verdict };
|
|
92
|
+
if (verdict !== undefined && (verdict.finding === "unanswered" || (verdict.finding === "none" && (verdict.environmentSilent === true || verdict.rowsUnread === true))))
|
|
93
|
+
return { kind: "silence", verdict };
|
|
94
|
+
if (verdict !== undefined && DISPOSITIONS.includes(verdict.disposition) && dispositionsOfRow(verdict.finding).has(verdict.disposition))
|
|
95
|
+
return { kind: verdict.disposition, verdict };
|
|
96
|
+
const word = (w, set) => (w !== undefined && set.includes(w) ? `"${w}"` : "a word outside its closed set");
|
|
97
|
+
const sentence = verdict === undefined
|
|
98
|
+
? `tool "${call.toolName}" was refused: the read station's answer is not one the engine can receive (an ordinary object holding its members as its own data properties, each of its kind), so it is not believed (denied fail-closed)`
|
|
99
|
+
: `tool "${call.toolName}" was refused: the read station answered ${word(verdict.disposition, DISPOSITIONS)} beside the finding ${word(verdict.finding, SHELL_READ_FINDINGS)}, an answer the read decision table never gives that finding — the station's answer contradicts its own finding and is not believed (denied fail-closed)`;
|
|
100
|
+
call.report?.(new Error(`shell read station answered out of its contract for tool "${call.toolName}"${call.toolCallId !== undefined ? ` (toolCallId ${call.toolCallId})` : ""} — ${sentence}`));
|
|
101
|
+
return { kind: "refuse", verdict: { disposition: "refuse", finding: "none", sentence }, unbelieved: true };
|
|
102
|
+
}
|
|
1
103
|
export function silencedBy(verdict, d) {
|
|
2
104
|
const environment = verdict.environmentSilent === true || (verdict.finding === "unanswered" && verdict.rowsUnread !== true);
|
|
3
105
|
const rows = verdict.rowsUnread === true && d.ruleUnreadable === undefined ? { ruleUnreadable: "persisted_rows" } : {};
|
|
@@ -63,17 +63,16 @@ export interface SideQuerySpec {
|
|
|
63
63
|
* Per-model auth — MIRRORS {@link TaskSpec.getApiKeyAndHeaders} (same signature, resolved per
|
|
64
64
|
* call against the RESOLVED model, exactly like the task path's per-call hook). The brain
|
|
65
65
|
* contract is the route pairing law (route-adjudicator.ts): a per-model credential (this hook,
|
|
66
|
-
* or an auth header on `Model.headers`) always rides; the deployment credential rides only
|
|
67
|
-
*
|
|
68
|
-
*
|
|
66
|
+
* or an auth header on `Model.headers`) always rides; the deployment credential rides only to the
|
|
67
|
+
* deployment's own root — a model carrying a root of its own is refused
|
|
68
|
+
* (`route.credential_mismatch` / `route.credential_missing`), never silently followed.
|
|
69
69
|
* Historically the fallback was unconditional (`options.apiKey ?? config.apiKey`, with a model's
|
|
70
70
|
* own `baseUrl` outranking the brain's), so a side query routed to a model carrying its own
|
|
71
71
|
* `baseUrl` + per-model key sent the GATEWAY key to the per-model (possibly external) URL — the
|
|
72
|
-
* credential leak the pairing law now stops. Absent seat ⇒ no options are minted
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* for a side query to fall back to.
|
|
72
|
+
* credential leak the pairing law now stops. Absent seat ⇒ no options are minted: the
|
|
73
|
+
* construction-time credential applies on the deployment's own root, and a model rooted elsewhere
|
|
74
|
+
* hard-fails the side query with the loud refusal (`stopReason: "error"`) instead of leaking —
|
|
75
|
+
* there is no primary model for a side query to fall back to.
|
|
77
76
|
*/
|
|
78
77
|
getApiKeyAndHeaders?: TaskSpec["getApiKeyAndHeaders"];
|
|
79
78
|
signal?: AbortSignal;
|
|
@@ -15,30 +15,51 @@ export interface BoundSkillGrants {
|
|
|
15
15
|
/** The skill's `disallowed-tools` entries — what activating it refuses for the rest of the run. */
|
|
16
16
|
readonly disallowedTools: readonly string[];
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Why a card carries no binding digest — the closed set of `boundGrantAbsence`: `not_bindable` (the call is not an
|
|
20
|
+
* activation of the engine's `Skill` tool: it binds no grant) or `unreadable` (the card binds a grant and what it binds
|
|
21
|
+
* cannot be read: a stored row whose digest is not a string, or that says neither).
|
|
22
|
+
*/
|
|
23
|
+
export declare const BOUND_GRANT_ABSENCES: readonly ["not_bindable", "unreadable"];
|
|
24
|
+
export type BoundGrantAbsence = (typeof BOUND_GRANT_ABSENCES)[number];
|
|
18
25
|
/**
|
|
19
26
|
* A stored value read as a {@link BoundSkillGrants}, whole or not at all: a fresh copy of the three members and nothing else,
|
|
20
27
|
* or `undefined` when any member is not of its type. Every member is read ONCE, so the value screened is the value published.
|
|
21
28
|
*/
|
|
22
29
|
export declare function readBoundSkillGrants(raw: unknown): BoundSkillGrants | undefined;
|
|
23
|
-
/** The card members a call to the engine's `Skill` tool carries
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
boundGrantDigest
|
|
27
|
-
boundGrants
|
|
28
|
-
|
|
30
|
+
/** The card members every ask carries about its binding — exactly one arm: a call to the engine's `Skill` tool carries the
|
|
31
|
+
* digest and the value it hashes, every other call the absence word. */
|
|
32
|
+
export type SkillGrantBindingMembers = {
|
|
33
|
+
readonly boundGrantDigest: string;
|
|
34
|
+
readonly boundGrants: BoundSkillGrants;
|
|
35
|
+
readonly boundGrantAbsence?: never;
|
|
36
|
+
} | {
|
|
37
|
+
readonly boundGrantAbsence: BoundGrantAbsence;
|
|
38
|
+
readonly boundGrantDigest?: never;
|
|
39
|
+
readonly boundGrants?: never;
|
|
40
|
+
};
|
|
41
|
+
/** The members of a card that is not a `Skill` activation: the factory's other arm, and what a tool's own consent card (never a
|
|
42
|
+
* `Skill` call) spreads. */
|
|
43
|
+
export declare const NOT_BINDABLE: Readonly<{
|
|
44
|
+
readonly boundGrantAbsence: "not_bindable";
|
|
45
|
+
}>;
|
|
29
46
|
/** The leg's binding factory ({@link SkillGrantBindingMembers} for a call over `args`), the one every ask-mint site spreads. */
|
|
30
47
|
export type GrantBindingOf = (call: {
|
|
31
48
|
readonly toolName: string;
|
|
32
49
|
}, args: unknown) => SkillGrantBindingMembers;
|
|
33
50
|
/**
|
|
34
|
-
* The binding members of a stored parked row, as the inbox projection publishes them: the digest when it
|
|
35
|
-
* only as the value that digest hashes — {@link readBoundSkillGrants} whole, and equal to the digest by
|
|
36
|
-
* disagrees, or lists beside no digest, would show an approver one list while the approval binds another
|
|
51
|
+
* The binding members of a stored parked row, as the inbox projection publishes them — exactly one of three: the digest when it
|
|
52
|
+
* is a string (and the lists only as the value that digest hashes — {@link readBoundSkillGrants} whole, and equal to the digest by
|
|
53
|
+
* hash; a pair that disagrees, or lists beside no digest, would show an approver one list while the approval binds another), else
|
|
54
|
+
* the row's absence word when it is a member of the set, else `unreadable` — the row does not say what it binds.
|
|
37
55
|
*/
|
|
38
56
|
export declare function readBoundBinding(row: {
|
|
39
57
|
readonly boundGrantDigest?: unknown;
|
|
40
58
|
readonly boundGrants?: unknown;
|
|
41
|
-
|
|
42
|
-
|
|
59
|
+
readonly boundGrantAbsence?: unknown;
|
|
60
|
+
}): {
|
|
61
|
+
boundGrantDigest: string;
|
|
43
62
|
boundGrants?: BoundSkillGrants;
|
|
63
|
+
} | {
|
|
64
|
+
boundGrantAbsence: BoundGrantAbsence;
|
|
44
65
|
};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { boundInputHashOf } from "./canonical-json.js";
|
|
2
|
+
export const BOUND_GRANT_ABSENCES = Object.freeze(["not_bindable", "unreadable"]);
|
|
3
|
+
const BOUND_GRANT_ABSENCE_SET = new Set(BOUND_GRANT_ABSENCES);
|
|
4
|
+
const isBoundGrantAbsence = (v) => BOUND_GRANT_ABSENCE_SET.has(v);
|
|
2
5
|
export function readBoundSkillGrants(raw) {
|
|
3
6
|
if (typeof raw !== "object" || raw === null)
|
|
4
7
|
return undefined;
|
|
@@ -15,8 +18,13 @@ export function readBoundSkillGrants(raw) {
|
|
|
15
18
|
const disallowedTools = entries(disallowed);
|
|
16
19
|
return allowedTools !== undefined && disallowedTools !== undefined ? { skill, allowedTools, disallowedTools } : undefined;
|
|
17
20
|
}
|
|
21
|
+
export const NOT_BINDABLE = Object.freeze({ boundGrantAbsence: "not_bindable" });
|
|
18
22
|
export function readBoundBinding(row) {
|
|
19
|
-
const digest = row
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
const digest = row.boundGrantDigest;
|
|
24
|
+
if (typeof digest === "string") {
|
|
25
|
+
const grants = readBoundSkillGrants(row.boundGrants);
|
|
26
|
+
return { boundGrantDigest: digest, ...(grants !== undefined && boundInputHashOf(grants) === digest ? { boundGrants: grants } : {}) };
|
|
27
|
+
}
|
|
28
|
+
const word = row.boundGrantAbsence;
|
|
29
|
+
return { boundGrantAbsence: digest === undefined && isBoundGrantAbsence(word) ? word : "unreadable" };
|
|
22
30
|
}
|
|
@@ -2,7 +2,7 @@ import type { PermissionResult, ToolCallRequest } from "./tool-policy.js";
|
|
|
2
2
|
import type { PermissionMode } from "./ask-origin.js";
|
|
3
3
|
import { type PermissionSeatInputs, type PermissionSeatReading } from "./mode-policy.js";
|
|
4
4
|
import type { AgentTool } from "../internal/harness-types.js";
|
|
5
|
-
import type
|
|
5
|
+
import { type SkillGrantBindingMembers } from "./skill-grant-binding.js";
|
|
6
6
|
import { type AllowEntryGateSeats, type AllowEntryInert, type AllowEntryJudgeContext, type CompiledAllowEntry } from "./allow-entry.js";
|
|
7
7
|
/** Who wrote a row: the `Skill` tool on a successful activation, prepare for a skill the person invoked by name, or a
|
|
8
8
|
* delegated child's seed from its parent. */
|
|
@@ -53,8 +53,9 @@ export declare function skillGrantDigest(skill: Pick<SkillGrantSource, "name" |
|
|
|
53
53
|
* `AskRequest` an approver is handed, an ancestor's, the parked row and (through the row) the inbox projection.
|
|
54
54
|
* `boundGrants` is the skill's name and canonical lists, `boundGrantDigest` its hash — the same value, so what a person was
|
|
55
55
|
* shown and what an approving resume is judged by cannot differ. The binding is a property of the CALL, whoever asked and
|
|
56
|
-
* however the question was worded.
|
|
57
|
-
* mounted, so a call by that name is not an activation)
|
|
56
|
+
* however the question was worded. Every other call — and any call when no skill is loaded (no engine `Skill` tool is
|
|
57
|
+
* mounted, so a call by that name is not an activation) — carries the absence word `not_bindable` instead: one of the two,
|
|
58
|
+
* always, so no reader infers the binding from a tool name.
|
|
58
59
|
*/
|
|
59
60
|
export declare function skillGrantBindingMembers(call: {
|
|
60
61
|
readonly toolName: string;
|
package/dist/core/skill-grant.js
CHANGED
|
@@ -3,8 +3,9 @@ import { parsePermissionRule } from "./permission-rules.js";
|
|
|
3
3
|
import { mountedToolPermissionSeat } from "./mode-policy.js";
|
|
4
4
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
5
5
|
import { boundInputHashOf } from "./canonical-json.js";
|
|
6
|
+
import { NOT_BINDABLE } from "./skill-grant-binding.js";
|
|
6
7
|
import { readToolNameList } from "./tool-name-list.js";
|
|
7
|
-
import { allowingRows, autoStripped,
|
|
8
|
+
import { allowingRows, autoStripped, compileAllowList, readRuleEntry, inertEntryPhrase, judgeAllowEntries, nonListValueLabel, nonStringMemberLabel, renderListEntry } from "./allow-entry.js";
|
|
8
9
|
export const SKILL_GRANT_ORIGINS = Object.freeze(["model", "user", "inherited"]);
|
|
9
10
|
const FROZEN_SKILL_GRANT_ORIGINS = new Set(["model", "inherited"]);
|
|
10
11
|
export function skillNamedBy(args, skills) {
|
|
@@ -25,13 +26,13 @@ export function skillGrantDigest(skill) {
|
|
|
25
26
|
return skillGrantBinding(skill).boundGrantDigest;
|
|
26
27
|
}
|
|
27
28
|
export function skillGrantBindingMembers(call, args, skills, skillToolName) {
|
|
28
|
-
return isSkillActivationCall(call, skills, skillToolName) ? skillGrantBinding(skillNamedBy(args, skills)) :
|
|
29
|
+
return isSkillActivationCall(call, skills, skillToolName) ? skillGrantBinding(skillNamedBy(args, skills)) : NOT_BINDABLE;
|
|
29
30
|
}
|
|
30
31
|
function isSkillActivationCall(call, skills, skillToolName) {
|
|
31
32
|
return skills.length > 0 && wireOf("skillGrant.binding", call) === skillToolName;
|
|
32
33
|
}
|
|
33
34
|
export function parkedSkillApprovalRefusal(row, args, skills, skillToolName, site = "claim") {
|
|
34
|
-
if (row.boundGrantDigest === undefined &&
|
|
35
|
+
if (row.boundGrantDigest === undefined && !isSkillActivationCall(row, skills, skillToolName))
|
|
35
36
|
return undefined;
|
|
36
37
|
if (skillGrantDigest(skillNamedBy(args, skills)) === row.boundGrantDigest)
|
|
37
38
|
return undefined;
|
|
@@ -244,7 +245,7 @@ function readCarriedRecords(raw, carrier) {
|
|
|
244
245
|
const { skill, behavior, entry, origin } = r;
|
|
245
246
|
if (typeof skill !== "string" || typeof entry !== "string" || (behavior !== "allow" && behavior !== "deny"))
|
|
246
247
|
return undefined;
|
|
247
|
-
if (
|
|
248
|
+
if (readRuleEntry(entry, behavior).kind === "inert")
|
|
248
249
|
return undefined;
|
|
249
250
|
if (carrier === "capture") {
|
|
250
251
|
out.push({ skill, behavior, entry });
|
|
@@ -51,7 +51,9 @@ export type SkillsDirectoryWarningCode =
|
|
|
51
51
|
/** An attachment was left out (non-text bytes, over budget, unreadable, or resolving outside the skill). */
|
|
52
52
|
| "attachment_skipped"
|
|
53
53
|
/** A file or directory entry existed but could not be read/resolved. */
|
|
54
|
-
| "read_failed"
|
|
54
|
+
| "read_failed"
|
|
55
|
+
/** A skill directory or its `SKILL.md` resolves outside {@link SkillsDirectoryOptions.containWithin} — skipped. */
|
|
56
|
+
| "outside_root";
|
|
55
57
|
/** One non-fatal diagnostic from a load. Safe to log: no file contents, only names/reasons. */
|
|
56
58
|
export interface SkillsDirectoryWarning {
|
|
57
59
|
code: SkillsDirectoryWarningCode;
|
|
@@ -69,6 +71,15 @@ export interface SkillsDirectoryOptions {
|
|
|
69
71
|
* ride a separate delivery cap at invoke time, so a generous value here does not enlarge the prompt.
|
|
70
72
|
*/
|
|
71
73
|
maxAttachmentBytes?: number;
|
|
74
|
+
/**
|
|
75
|
+
* The directory this root's skills must physically lie inside. Declared ⇒ every skill directory, and every
|
|
76
|
+
* `SKILL.md`, resolves (links followed) inside it or is skipped with an `outside_root` warning; the attachments
|
|
77
|
+
* already lie inside their skill directory, so they are inside it too. Absent ⇒ each root entry is a mount the
|
|
78
|
+
* deployment declared, trusted like the root itself. A root a third party writes (a repository's skills
|
|
79
|
+
* directory) declares the repository root here, so a link in it cannot load a skill from elsewhere on disk.
|
|
80
|
+
* Must name an existing directory; anything else throws, like an unreadable root.
|
|
81
|
+
*/
|
|
82
|
+
containWithin?: string;
|
|
72
83
|
}
|
|
73
84
|
/**
|
|
74
85
|
* Load every `SKILL.md`-bearing subdirectory of `dir` into a {@link SkillSpec}, ready for
|