@sema-agent/core 7.45.0 → 7.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +298 -1
- package/dist/agents/extra-tool-names.d.ts +65 -0
- package/dist/agents/extra-tool-names.js +124 -0
- package/dist/agents/subagent.d.ts +17 -5
- package/dist/agents/subagent.js +33 -25
- package/dist/agents/tool-filter.d.ts +17 -1
- package/dist/agents/tool-filter.js +7 -0
- package/dist/core/agent-config-dir.d.ts +20 -0
- package/dist/core/agent-config-dir.js +49 -0
- package/dist/core/allow-entry.d.ts +6 -4
- package/dist/core/allow-entry.js +6 -5
- package/dist/core/ask-origin.d.ts +35 -12
- package/dist/core/ask-origin.js +24 -7
- package/dist/core/checkpoint-store.d.ts +39 -2
- package/dist/core/effective-path-target.d.ts +136 -4
- package/dist/core/effective-path-target.js +110 -2
- package/dist/core/engine-notice.d.ts +12 -4
- package/dist/core/gate-fold.js +5 -2
- package/dist/core/gate-lanes.js +11 -4
- package/dist/core/gate-outcome.d.ts +4 -2
- package/dist/core/gate-outcome.js +2 -2
- package/dist/core/governance-codes.d.ts +3 -1
- package/dist/core/governance-codes.js +3 -0
- package/dist/core/hooks.d.ts +3 -0
- package/dist/core/path-rule-binding.d.ts +65 -0
- package/dist/core/path-rule-binding.js +99 -0
- package/dist/core/permission-rule-model.d.ts +59 -24
- package/dist/core/permission-rule-model.js +48 -44
- package/dist/core/permission-rule-provider.d.ts +7 -0
- package/dist/core/permission-rule-provider.js +10 -3
- package/dist/core/permission-rule-store.d.ts +8 -0
- package/dist/core/permission-rule-store.js +6 -2
- package/dist/core/permission-rule-sync.js +2 -2
- package/dist/core/permission-rules.d.ts +26 -45
- package/dist/core/permission-rules.js +77 -57
- package/dist/core/runner/child-tree.d.ts +97 -0
- package/dist/core/runner/child-tree.js +104 -0
- package/dist/core/runner/construction-doors.d.ts +5 -0
- package/dist/core/runner/construction-doors.js +6 -0
- package/dist/core/runner/contracts.d.ts +46 -0
- package/dist/core/runner/gate-exit.d.ts +4 -1
- package/dist/core/runner/gate-exit.js +5 -1
- package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
- package/dist/core/runner/permission-rule-lanes.js +18 -12
- package/dist/core/runner/persisted-path-rows.d.ts +31 -7
- package/dist/core/runner/persisted-path-rows.js +31 -12
- package/dist/core/runner/prepare-ask-lane.d.ts +4 -0
- package/dist/core/runner/prepare-ask-lane.js +4 -2
- package/dist/core/runner/prepare-execution-env.d.ts +5 -2
- package/dist/core/runner/prepare-execution-env.js +10 -3
- package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
- package/dist/core/runner/prepare-gate-stations.js +2 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
- package/dist/core/runner/prepare-hands-readface.js +11 -11
- package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
- package/dist/core/runner/prepare-inherited-gate.js +8 -2
- package/dist/core/runner/prepare-park-ask.d.ts +3 -0
- package/dist/core/runner/prepare-park-ask.js +5 -3
- package/dist/core/runner/prepare-policy-chain.d.ts +14 -2
- package/dist/core/runner/prepare-policy-chain.js +13 -5
- package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
- package/dist/core/runner/prepare-suspend-saga.js +7 -1
- package/dist/core/runner/prepare-task.js +37 -18
- package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
- package/dist/core/runner/prepare-turn-wiring.js +2 -1
- package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
- package/dist/core/runner/prepare-workspace-restore.js +1 -0
- package/dist/core/runner/resume-apply.js +17 -3
- package/dist/core/runner/resume-checkpoint-screen.js +13 -0
- package/dist/core/runner/run-leg.js +5 -1
- package/dist/core/runner/runtask.js +2 -2
- package/dist/core/runner/sub-agent-deny-lists.d.ts +1 -1
- package/dist/core/runner/write-protection-station.d.ts +3 -2
- package/dist/core/runner/write-protection-station.js +1 -0
- package/dist/core/runner-deps.d.ts +14 -0
- package/dist/core/sensitive-path-policy.d.ts +8 -0
- package/dist/core/sensitive-path-policy.js +18 -2
- package/dist/core/shell-wrapper-projection.d.ts +11 -0
- package/dist/core/shell-wrapper-projection.js +56 -0
- package/dist/core/shell-wrapper-table.d.ts +144 -34
- package/dist/core/shell-wrapper-table.js +40 -20
- package/dist/core/synthetic-read-verdict.d.ts +3 -0
- package/dist/core/synthetic-read-verdict.js +3 -1
- package/dist/core/tool-catalog-entries.d.ts +21 -0
- package/dist/core/tool-catalog-entries.js +12 -2
- package/dist/core/tool-face.d.ts +10 -1
- package/dist/core/tool-name-readings.d.ts +8 -0
- package/dist/core/tool-name-readings.js +2 -0
- package/dist/core/tool-policy.d.ts +40 -17
- package/dist/core/tool-policy.js +11 -9
- package/dist/core/tool-registry.d.ts +9 -0
- package/dist/core/tool-registry.js +10 -0
- package/dist/core/tool-spec.d.ts +3 -2
- package/dist/core/write-protect.d.ts +14 -3
- package/dist/core/write-protect.js +41 -10
- package/dist/index.d.ts +6 -4
- package/dist/index.js +5 -3
- package/dist/orchestration/run-spec.js +8 -7
- package/dist/orchestration/workflow.js +1 -1
- package/dist/shell/lex.d.ts +22 -0
- package/dist/shell/lex.js +75 -13
- package/dist/tools/fs/bash-program-position.d.ts +14 -90
- package/dist/tools/fs/bash-program-position.js +41 -74
- package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
- package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
- package/dist/tools/fs/index.d.ts +3 -2
- package/dist/tools/fs/program-read-model.d.ts +711 -0
- package/dist/tools/fs/program-read-model.js +1259 -0
- package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
- package/dist/tools/fs/program-read-model.rows.js +624 -0
- package/dist/tools/fs/read-deny-home.d.ts +19 -1
- package/dist/tools/fs/read-deny-home.js +62 -24
- package/dist/tools/fs/read-deny.d.ts +54 -14
- package/dist/tools/fs/read-deny.js +104 -50
- package/dist/tools/fs/removal-targets.d.ts +7 -6
- package/dist/tools/fs/removal-targets.js +34 -24
- package/dist/tools/fs/safety.d.ts +3 -0
- package/dist/tools/fs/safety.js +10 -3
- package/dist/tools/fs/shell-read-argv.d.ts +86 -0
- package/dist/tools/fs/shell-read-argv.js +388 -0
- package/dist/tools/fs/shell-read-bindings.d.ts +300 -0
- package/dist/tools/fs/shell-read-bindings.js +755 -0
- package/dist/tools/fs/shell-read-check.js +23 -15
- package/dist/tools/fs/shell-read-establish.js +82 -8
- package/dist/tools/fs/shell-read-judge.d.ts +11 -1
- package/dist/tools/fs/shell-read-judge.js +9 -1
- package/dist/tools/fs/shell-read-plan.d.ts +33 -8
- package/dist/tools/fs/shell-read-plan.js +309 -407
- package/dist/tools/fs/shell-read-word.d.ts +60 -26
- package/dist/tools/fs/shell-read-word.js +47 -37
- package/dist/tools/fs/shell-read-write-targets.d.ts +15 -64
- package/dist/tools/fs/shell-read-write-targets.js +85 -137
- package/dist/tools/fs/shell-reentry-text.d.ts +136 -6
- package/dist/tools/fs/shell-reentry-text.js +203 -53
- package/dist/tools/monitor.d.ts +3 -3
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +25 -5
- package/wire-changes/core@7.46.0.json +1522 -0
- package/wire-changes/core@7.47.0.json +412 -0
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
|
|
2
2
|
import { splitNamespacedName } from "./tool-name-aliases.js";
|
|
3
|
-
import { catalogRuleFaceOf, pathTargetOf } from "./tool-registry.js";
|
|
3
|
+
import { catalogRuleFaceOf, isShellFaceToolName, pathTargetOf } from "./tool-registry.js";
|
|
4
4
|
import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule } from "./permission-rule-syntax.js";
|
|
5
|
-
import { NAME_RULE_FACES, PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, nameRuleAdmits, nameRuleSubjectOf, parseRuleText,
|
|
6
|
-
import { pathFamilyOf
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { COMMAND_RULE_TOOL, NAME_RULE_FACES, PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, laneToolNamesOf, nameRuleAdmits, nameRuleSubjectOf, parseRuleText, programRunReachOf, ruleBasesNeeded, ruleToolGrammarOf } from "./permission-rule-model.js";
|
|
6
|
+
import { pathFamilyOf } from "../tools/fs/safety.js";
|
|
7
|
+
import { callTargetReadingOf, callTargetViewsOf } from "./effective-path-target.js";
|
|
8
|
+
import { bindPathRules, pathRowsReach, policyAnchors, rowsWhereTheyLead } from "./path-rule-binding.js";
|
|
9
9
|
import { ruleDenialSentence } from "./rule-denial-sentence.js";
|
|
10
10
|
import { heldValues } from "./tool-name-readings.js";
|
|
11
11
|
export { parsePermissionRule };
|
|
12
|
-
export const BASH_GENERIC_PARAMS = new Set(catalogRuleFaceOf("Bash")?.params ?? []);
|
|
13
12
|
function describeBehavior(v) {
|
|
14
13
|
switch (typeof v) {
|
|
15
14
|
case "undefined": return "undefined";
|
|
@@ -93,22 +92,6 @@ export const fileToolParamVocabulary = (canonicalTool) => {
|
|
|
93
92
|
const face = catalogRuleFaceOf(canonicalTool);
|
|
94
93
|
return face?.params === undefined || pathTargetOf(canonicalTool) === undefined ? undefined : new Set(face.params);
|
|
95
94
|
};
|
|
96
|
-
export function pathRuleDenyRowOf(rule, bases) {
|
|
97
|
-
const family = pathFamilyOf(bases);
|
|
98
|
-
const tree = bases.root ?? bases.cwd;
|
|
99
|
-
const head = pathRuleHeadOf(rule.parsed, bases);
|
|
100
|
-
return {
|
|
101
|
-
rule: rule.ruleText,
|
|
102
|
-
tool: rule.parsed.tool,
|
|
103
|
-
pattern: parsePermissionRule(rule.ruleText).ruleContent ?? "",
|
|
104
|
-
faces: rule.faces,
|
|
105
|
-
reaches: (absolutePath) => {
|
|
106
|
-
const id = pathIdentityOf(absolutePath, family, tree);
|
|
107
|
-
return id.ok && pathRuleReachOf(rule.parsed, id.identity, bases).reach !== "not_reached";
|
|
108
|
-
},
|
|
109
|
-
...(head !== undefined ? { head: spellingOfIdentity(head, family) } : {}),
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
95
|
function countStars(s) {
|
|
113
96
|
let n = 0;
|
|
114
97
|
for (const c of s)
|
|
@@ -126,13 +109,14 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
126
109
|
return { face, judged: face !== undefined };
|
|
127
110
|
};
|
|
128
111
|
const byTool = new Map();
|
|
112
|
+
const command = { deny: [], ask: [] };
|
|
129
113
|
const namespacedCovering = { deny: [], ask: [] };
|
|
130
114
|
const issues = [];
|
|
131
115
|
const bad = (rule, code, message) => {
|
|
132
116
|
issues.push({ rule, code, message });
|
|
133
117
|
};
|
|
134
118
|
if (rules.length > caps.maxRules) {
|
|
135
|
-
return { byTool, namespacedCovering, issues: [{ rule: "", code: "invalid.cap_exceeded", message: `${rules.length} rules > maxRules ${caps.maxRules}` }] };
|
|
119
|
+
return { byTool, command, namespacedCovering, issues: [{ rule: "", code: "invalid.cap_exceeded", message: `${rules.length} rules > maxRules ${caps.maxRules}` }] };
|
|
136
120
|
}
|
|
137
121
|
for (const r of rules) {
|
|
138
122
|
const text = r.rule;
|
|
@@ -190,17 +174,24 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
190
174
|
}
|
|
191
175
|
const content = parsed.ruleContent;
|
|
192
176
|
const colon = content.indexOf(":");
|
|
193
|
-
if (toolName
|
|
177
|
+
if (isShellFaceToolName(toolName)) {
|
|
194
178
|
const paramPart = colon > 0 ? content.slice(0, colon).trim() : "";
|
|
195
|
-
const
|
|
196
|
-
|
|
179
|
+
const declaresParam = (tool) => colon > 0 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(paramPart) && (catalogRuleFaceOf(tool)?.params ?? []).includes(paramPart);
|
|
180
|
+
const looksLikeParamRule = declaresParam(toolName);
|
|
181
|
+
if (toolName !== COMMAND_RULE_TOOL && !looksLikeParamRule) {
|
|
182
|
+
const commandWordParam = declaresParam(COMMAND_RULE_TOOL) && !content.endsWith(":*");
|
|
183
|
+
bad(text, "invalid.command_rule", commandWordParam
|
|
184
|
+
? `"${text}" is a parameter rule on "${paramPart}", which ${toolName} does not declare (its rule parameters: ${(catalogRuleFaceOf(toolName)?.params ?? []).join(", ")}); a parameter rule reaches only the tool that declares the parameter, so ${COMMAND_RULE_TOOL}(${content}) is a rule on ${COMMAND_RULE_TOOL}'s own argument and does not reach ${toolName}. A shell command rule is written ${COMMAND_RULE_TOOL}(<command>) — it reaches ${toolName} and every other tool that runs a shell command by the command it runs.`
|
|
185
|
+
: `a shell command rule is written ${COMMAND_RULE_TOOL}(…) — it reaches ${toolName} and every other tool that runs a shell command by the command it runs; "${text}" names no rule upstream reads. Write ${COMMAND_RULE_TOOL}(${content}).`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (toolName === COMMAND_RULE_TOOL && (!looksLikeParamRule || content.endsWith(":*"))) {
|
|
197
189
|
const parsedCommand = parseRuleText(text, verb);
|
|
198
190
|
if ("reject" in parsedCommand) {
|
|
199
191
|
bad(text, "invalid.command_rule", `${parsedCommand.reject.code}: ${parsedCommand.reject.message}`);
|
|
200
192
|
continue;
|
|
201
193
|
}
|
|
202
|
-
|
|
203
|
-
byTool.set(toolName, entry);
|
|
194
|
+
command[verb].push({ parsed: parsedCommand.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}) });
|
|
204
195
|
continue;
|
|
205
196
|
}
|
|
206
197
|
}
|
|
@@ -233,9 +224,9 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
233
224
|
continue;
|
|
234
225
|
}
|
|
235
226
|
for (const base of ruleBasesNeeded(parsedPath.rule)) {
|
|
236
|
-
const
|
|
237
|
-
if (!isUsablePathBase(
|
|
238
|
-
bad(text, "unsupported.path_base", `"${text}" is relative to ${PATH_RULE_BASE_LABEL[base]}, and
|
|
227
|
+
const declared = base === "cwd" ? (pathBases.cwd ?? pathBases.root) : pathBases[base];
|
|
228
|
+
if (declared !== undefined && !isUsablePathBase(declared, pathFamilyOf(pathBases))) {
|
|
229
|
+
bad(text, "unsupported.path_base", `"${text}" is relative to ${PATH_RULE_BASE_LABEL[base]}, and the \`pathBases.${base === "cwd" && pathBases.cwd === undefined ? "root" : base}\` this policy declares is not an absolute path of its tree's family — the rule could not be judged on any call`);
|
|
239
230
|
}
|
|
240
231
|
}
|
|
241
232
|
if (issues.length > 0 && issues[issues.length - 1].rule === text && issues[issues.length - 1].code === "unsupported.path_base")
|
|
@@ -280,7 +271,7 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
280
271
|
entry.param[verb].push({ param, pattern, behavior: verb, ruleText: text, source: r.source });
|
|
281
272
|
byTool.set(toolName, entry);
|
|
282
273
|
}
|
|
283
|
-
return { byTool, namespacedCovering, issues };
|
|
274
|
+
return { byTool, command, namespacedCovering, issues };
|
|
284
275
|
}
|
|
285
276
|
export function validatePermissionRules(rules, opts) {
|
|
286
277
|
const caps = { ...DEFAULT_CAPS, ...opts?.caps };
|
|
@@ -315,10 +306,11 @@ function ruleDenial(req, ruleText, source) {
|
|
|
315
306
|
function ruleAskMessage(ruleText, source) {
|
|
316
307
|
return `flagged by permission rule "${ruleText}"${source ? ` (${source})` : ""}`;
|
|
317
308
|
}
|
|
309
|
+
const NO_LEADS = new Map();
|
|
318
310
|
export function createPermissionRulePolicy(rules, opts) {
|
|
319
311
|
const caps = { ...DEFAULT_CAPS, ...opts?.caps };
|
|
320
312
|
const pathBases = opts?.pathBases ?? {};
|
|
321
|
-
const { byTool, namespacedCovering, issues } = compile(rules, caps, opts?.primaryFieldGeneric ?? "reject", opts?.ruleFaces, pathBases);
|
|
313
|
+
const { byTool, command, namespacedCovering, issues } = compile(rules, caps, opts?.primaryFieldGeneric ?? "reject", opts?.ruleFaces, pathBases);
|
|
322
314
|
const setLevelCap = issues.some((i) => i.code === "invalid.cap_exceeded" && i.rule === "");
|
|
323
315
|
if (issues.length > 0 && ((opts?.onInvalidRule ?? "throw") === "throw" || setLevelCap)) {
|
|
324
316
|
const e = new Error(`createPermissionRulePolicy: ${issues.length} invalid/unsupported rule(s):\n` +
|
|
@@ -350,6 +342,9 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
350
342
|
if (entry.bare.ask !== undefined || entry.param.ask.length > 0 || entry.content.ask.length > 0)
|
|
351
343
|
nameSets.ask.push(toolName);
|
|
352
344
|
}
|
|
345
|
+
for (const behavior of ["deny", "ask"])
|
|
346
|
+
if (command[behavior].length > 0 && !nameSets[behavior].includes(COMMAND_RULE_TOOL))
|
|
347
|
+
nameSets[behavior].push(COMMAND_RULE_TOOL);
|
|
353
348
|
for (const behavior of ["deny", "ask"]) {
|
|
354
349
|
for (const r of namespacedCovering[behavior])
|
|
355
350
|
nameSets[behavior].push(r.ruleName);
|
|
@@ -360,19 +355,57 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
360
355
|
deny: [...byTool.values()].flatMap((e) => e.content.deny.filter((r) => r.faces !== undefined)),
|
|
361
356
|
ask: [...byTool.values()].flatMap((e) => e.content.ask.filter((r) => r.faces !== undefined)),
|
|
362
357
|
};
|
|
363
|
-
const
|
|
364
|
-
const contentLaneOf = (behavior, entry, callFace) => [
|
|
358
|
+
const contentLaneOf = (behavior, entry, shellCall, callFace) => [
|
|
365
359
|
...(entry?.content[behavior].filter((r) => r.faces === undefined) ?? []),
|
|
360
|
+
...(shellCall ? command[behavior] : []),
|
|
366
361
|
...(callFace !== undefined ? faceRules[behavior].filter((r) => r.faces.includes(callFace)) : []),
|
|
367
362
|
];
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
const
|
|
363
|
+
const hasCommandRules = command.deny.length > 0 || command.ask.length > 0;
|
|
364
|
+
const commandParam = catalogRuleFaceOf(COMMAND_RULE_TOOL)?.primaryParams[0] ?? "command";
|
|
365
|
+
const pathRuleOf = new Map();
|
|
366
|
+
for (const e of byTool.values()) {
|
|
367
|
+
for (const behavior of ["deny", "ask"]) {
|
|
368
|
+
for (const r of e.content[behavior]) {
|
|
369
|
+
if (r.parsed.match !== "path" && r.parsed.match !== "subpath")
|
|
370
|
+
continue;
|
|
371
|
+
pathRuleOf.set(r, { parsed: r.parsed, ruleText: r.ruleText, ...(behavior === "deny" && r.faces !== undefined ? { faces: r.faces } : {}) });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const pathSpecs = [...pathRuleOf.values()];
|
|
376
|
+
const bound = new Map();
|
|
377
|
+
const boundAt = (anchors) => {
|
|
378
|
+
const key = JSON.stringify([anchors.root ?? null, anchors.cwd ?? null, anchors.home ?? null]);
|
|
379
|
+
const prior = bound.get(key);
|
|
380
|
+
if (prior !== undefined)
|
|
381
|
+
return prior;
|
|
382
|
+
const rows = bindPathRules(pathSpecs, anchors);
|
|
383
|
+
const of = new Map();
|
|
384
|
+
[...pathRuleOf.keys()].forEach((r, i) => of.set(r, rows[i]));
|
|
385
|
+
const out = { rows, of };
|
|
386
|
+
bound.set(key, out);
|
|
387
|
+
return out;
|
|
388
|
+
};
|
|
371
389
|
const contentHit = (lane, req) => {
|
|
372
390
|
if (lane.length === 0)
|
|
373
391
|
return undefined;
|
|
374
|
-
const
|
|
375
|
-
|
|
392
|
+
const pathLane = lane.filter((r) => pathRuleOf.has(r));
|
|
393
|
+
if (pathLane.length > 0) {
|
|
394
|
+
const views = callTargetViewsOf(req, "tighten", { ...(pathBases.root !== undefined ? { root: pathBases.root } : {}), ...(pathBases.cwd !== undefined ? { cwd: pathBases.cwd } : {}) });
|
|
395
|
+
if (views !== undefined) {
|
|
396
|
+
const { of } = boundAt(policyAnchors(pathBases, req.pathRuleAnchors));
|
|
397
|
+
const rows = rowsWhereTheyLead(pathLane.map((r) => of.get(r)), callTargetReadingOf(req)?.leads ?? NO_LEADS);
|
|
398
|
+
const hit = pathRowsReach(rows, views, "tighten");
|
|
399
|
+
const rule = hit === undefined ? undefined : pathLane.find((r) => of.get(r).spec === hit.row.spec);
|
|
400
|
+
if (hit !== undefined && hit.unreadable === undefined)
|
|
401
|
+
return { rule: rule };
|
|
402
|
+
if (hit !== undefined)
|
|
403
|
+
return judgeOthers(lane.filter((r) => !pathRuleOf.has(r)), req) ?? { rule: rule, unreadable: hit.unreadable };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return judgeOthers(lane.filter((r) => !pathRuleOf.has(r)), req);
|
|
407
|
+
};
|
|
408
|
+
const judgeOthers = (lane, req) => {
|
|
376
409
|
const command = isPlainObjectArgs(req.args) ? req.args[commandParam] : undefined;
|
|
377
410
|
let unreadable;
|
|
378
411
|
for (const r of lane) {
|
|
@@ -382,20 +415,6 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
382
415
|
return { rule: r };
|
|
383
416
|
continue;
|
|
384
417
|
}
|
|
385
|
-
if (r.parsed.match === "subpath" || r.parsed.match === "path") {
|
|
386
|
-
if (target === undefined || target.kind === "refused")
|
|
387
|
-
continue;
|
|
388
|
-
if (target.kind === "unreadable") {
|
|
389
|
-
unreadable ??= { rule: r, unreadable: target.reason };
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
const outcome = pathRuleReachOf(r.parsed, target.identity, bases);
|
|
393
|
-
if (outcome.reach === "reached")
|
|
394
|
-
return { rule: r };
|
|
395
|
-
if (outcome.reach === "unreadable")
|
|
396
|
-
unreadable ??= { rule: r, unreadable: outcome.reason };
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
418
|
if (typeof command !== "string")
|
|
400
419
|
continue;
|
|
401
420
|
const outcome = programRunReachOf(r.parsed, command);
|
|
@@ -430,7 +449,7 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
430
449
|
};
|
|
431
450
|
return {
|
|
432
451
|
nameSets: [nameSets],
|
|
433
|
-
...(
|
|
452
|
+
...(pathSpecs.length > 0 ? { pathRules: (anchors) => boundAt(policyAnchors(pathBases, anchors)).rows } : {}),
|
|
434
453
|
check(req) {
|
|
435
454
|
const entry = entryFor(req);
|
|
436
455
|
const covering = hasCovering && protocolOf(req.toolName) !== undefined;
|
|
@@ -443,7 +462,8 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
443
462
|
return { action: "deny", message: ruleDenial(req, coveringDeny.ruleText, coveringDeny.source) };
|
|
444
463
|
}
|
|
445
464
|
const callFace = faceRules.deny.length > 0 || faceRules.ask.length > 0 ? callPathFaceOf(req) : undefined;
|
|
446
|
-
const
|
|
465
|
+
const shellCall = hasCommandRules && laneToolNamesOf(req).includes(COMMAND_RULE_TOOL);
|
|
466
|
+
const denyLane = contentLaneOf("deny", entry, shellCall, callFace);
|
|
447
467
|
const contentDeny = denyLane.length > 0 ? contentHit(denyLane, req) : undefined;
|
|
448
468
|
if (contentDeny !== undefined && contentDeny.unreadable === undefined) {
|
|
449
469
|
return { action: "deny", message: ruleDenial(req, contentDeny.rule.ruleText, contentDeny.rule.source) };
|
|
@@ -466,7 +486,7 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
466
486
|
if (coveringAsk)
|
|
467
487
|
return { action: "ask", message: ruleAskMessage(coveringAsk.ruleText, coveringAsk.source), matchedAskRule: coveringAsk.ruleText };
|
|
468
488
|
}
|
|
469
|
-
const askLane = contentLaneOf("ask", entry, callFace);
|
|
489
|
+
const askLane = contentLaneOf("ask", entry, shellCall, callFace);
|
|
470
490
|
const contentAsk = askLane.length > 0 ? contentHit(askLane, req) : undefined;
|
|
471
491
|
if (contentAsk !== undefined) {
|
|
472
492
|
if (contentAsk.unreadable !== undefined)
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { RunnerDeps } from "../types.js";
|
|
2
|
+
import type { ExecutionEnv } from "../../internal/harness.js";
|
|
3
|
+
import type { PathRuleBases } from "../permission-rule-model.js";
|
|
4
|
+
import type { ChildPlacement, InheritedGate, RunInternals } from "./contracts.js";
|
|
5
|
+
import type { ToolPolicy } from "../tool-policy.js";
|
|
6
|
+
import { CheckpointError } from "../checkpoint-store.js";
|
|
2
7
|
/**
|
|
3
8
|
* Whether this deployment can place a child agent in a tree OTHER than the caller's — the one fact behind
|
|
4
9
|
* every request of that shape: an explicit `cwd`, a workflow agent's `isolation: "worktree"`, and the
|
|
@@ -11,3 +16,95 @@ import type { RunnerDeps } from "../types.js";
|
|
|
11
16
|
* keeps its own code and sentence for the refusal, but none decides the condition itself.
|
|
12
17
|
*/
|
|
13
18
|
export declare function deploymentCanRerootChild(deps: Pick<RunnerDeps, "executionEnvFactory">): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Where a child the caller asked to place at `asked` actually landed (`childRoot`, the root its environment gave it) — the
|
|
21
|
+
* ONE judgment every reader of that fact takes
|
|
22
|
+
* (the delegation tool's worktree request, and the anchors of the rules the child inherits):
|
|
23
|
+
* · `honored` — the environment's root is where the child was asked to be (canonical on both sides);
|
|
24
|
+
* · `caller_tree` — it is not, and it lies inside the caller's own tree (`callerRoot`): the same filesystem, the wrong
|
|
25
|
+
* directory — a worktree request then fails (the child would write the caller's checkout);
|
|
26
|
+
* · `elsewhere` — neither: another filesystem namespace (a sandbox the factory minted). The child runs there, it is not
|
|
27
|
+
* in the worktree it was offered, and it takes the caller's path rules again at its own anchors.
|
|
28
|
+
* A path the environment cannot canonicalize is compared as spelled. Residual: two namespaces that hold the same path
|
|
29
|
+
* string, with a factory claiming to honor it, read `honored`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function childPlacementOf(env: Pick<ExecutionEnv, "canonicalPath">, childRoot: string, asked: string | undefined, callerRoot: string): Promise<ChildPlacement>;
|
|
32
|
+
/** A delegated leg's placement: the place asked for is the hint (a minted worktree, an explicit cwd, the caller's own cwd)
|
|
33
|
+
* — or, for a worktree the factory mints itself (`isolation`), that worktree on the caller's host (a remote environment's
|
|
34
|
+
* is another namespace). A leg no delegation caller placed (no `callerRoot`) has no placement and reads nothing. */
|
|
35
|
+
export declare function childPlacementOnLeg(env: ExecutionEnv, internals: Pick<RunInternals, "isolation" | "parentCwd"> & {
|
|
36
|
+
readonly callerRoot: string;
|
|
37
|
+
}, root: string): Promise<ChildPlacement>;
|
|
38
|
+
/** The refusal of a worktree the caller minted for a child its environment placed in the caller's own tree. */
|
|
39
|
+
export declare function worktreeNotHonored(cwd: string): Error & {
|
|
40
|
+
code: "isolation.unavailable";
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The chain's top leg's anchors as THIS leg reads them (the person's global / session path rows and every `~/` row anchor
|
|
44
|
+
* there): the chain's own (`live` as recorded, else the checkpoint's `seed` rebased — {@link recordedAnchors}), unless this
|
|
45
|
+
* leg is a child placed `elsewhere` or no chain names one — then this leg is the top. A recorded root that is not an
|
|
46
|
+
* absolute path anchors nothing: the rows it would anchor read `unreadable`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function chainTopAnchors(cfg: {
|
|
49
|
+
leg: PathRuleBases;
|
|
50
|
+
live: InheritedGate["pathRuleAnchors"];
|
|
51
|
+
seed: InheritedGate["pathRuleAnchors"];
|
|
52
|
+
placement: ChildPlacement | undefined;
|
|
53
|
+
rebase: (p: string) => string;
|
|
54
|
+
}): PathRuleBases;
|
|
55
|
+
/** The installation records a leg reads: the live chain's (as recorded), else the checkpoint's (through the workspace rebase). */
|
|
56
|
+
export interface ChainInstallations {
|
|
57
|
+
readonly live: InheritedGate["pathRuleInstallations"];
|
|
58
|
+
readonly seed: InheritedGate["pathRuleInstallations"];
|
|
59
|
+
}
|
|
60
|
+
/** Why no usable record says where a chain entry was installed: no record (a chain written before the record existed), a
|
|
61
|
+
* record that does not cover the chain position for position, or a member that names no absolute root. */
|
|
62
|
+
export type ChainInstallationGap = "absent" | "misaligned" | "invalid";
|
|
63
|
+
/**
|
|
64
|
+
* The chain entries a leg could not install where they were installed: each UNINSTALLED entry (a host re-supplying a
|
|
65
|
+
* chain) whose path rules depend on the installing leg, with no usable record of that leg. Such a chain is refused —
|
|
66
|
+
* installing those entries anywhere else (the chain's top leg, this leg) would move what they protect. Entries whose rules
|
|
67
|
+
* do not depend on it, and installed entries, never need a record.
|
|
68
|
+
*/
|
|
69
|
+
export declare function chainInstallationGaps(chain: readonly ToolPolicy[], installations: ChainInstallations, rebase: (p: string) => string): Array<{
|
|
70
|
+
readonly position: number;
|
|
71
|
+
readonly gap: ChainInstallationGap;
|
|
72
|
+
}>;
|
|
73
|
+
/** The refusal of a chain with {@link chainInstallationGaps} (`resume.parent_constraint_mismatch`, `detail.reason` naming
|
|
74
|
+
* the first gap's form); `pending` says the checkpoint is still pending (the refusal came before the claim). */
|
|
75
|
+
export declare function chainInstallationRefusal(gaps: ReadonlyArray<{
|
|
76
|
+
readonly position: number;
|
|
77
|
+
readonly gap: ChainInstallationGap;
|
|
78
|
+
}>, pending: boolean): CheckpointError;
|
|
79
|
+
/**
|
|
80
|
+
* How a leg holds a caller policy — its own (`TaskSpec.toolPolicy` / `RunnerDeps.toolPolicy`) or one its delegation chain
|
|
81
|
+
* hands it: a policy is installed where it ENTERS the chain, and a child holds its ancestor's installation (one object,
|
|
82
|
+
* the same anchors) — its own caller slot included when that slot names the policy an ancestor installed. Installed again:
|
|
83
|
+
* on a child placed `elsewhere` (another filesystem namespace — the ancestor's anchors name another namespace's files), at
|
|
84
|
+
* this leg's anchors; a chain entry handed uninstalled (a host re-supplying a chain), where its installing leg recorded it
|
|
85
|
+
* (`InheritedGate.pathRuleInstallations`, live or from the checkpoint). An uninstalled entry whose rules depend on that
|
|
86
|
+
* leg and whose record is not usable refuses the leg ({@link chainInstallationRefusal}); one whose rules do not, reads
|
|
87
|
+
* the same wherever it is installed (the chain's top leg).
|
|
88
|
+
*/
|
|
89
|
+
export declare function chainPolicyOnLeg(cfg: {
|
|
90
|
+
leg: PathRuleBases;
|
|
91
|
+
chainTop: PathRuleBases;
|
|
92
|
+
placement: ChildPlacement | undefined;
|
|
93
|
+
chain: readonly ToolPolicy[];
|
|
94
|
+
installations: ChainInstallations;
|
|
95
|
+
rebase: (p: string) => string;
|
|
96
|
+
}): {
|
|
97
|
+
own: (policy: ToolPolicy | undefined) => ToolPolicy | undefined;
|
|
98
|
+
chained: (policy: ToolPolicy, position: number) => ToolPolicy;
|
|
99
|
+
};
|
|
100
|
+
/** The installation record of a chain entry (its root and home), `null` for an entry with no path rules installed. */
|
|
101
|
+
export declare function installationRecordOf(policy: ToolPolicy): {
|
|
102
|
+
readonly root: string;
|
|
103
|
+
readonly home?: string;
|
|
104
|
+
} | null;
|
|
105
|
+
/** What a leg hands its children about anchors (data half): the chain's top leg's anchors — the person's global / session
|
|
106
|
+
* rows and every `~/` row anchor there — and where each entry handed down was installed, position for position (a chain
|
|
107
|
+
* re-supplied uninstalled is installed there again; absent when no entry is an installation). */
|
|
108
|
+
export declare function chainAnchorRecords(chainTop: PathRuleBases, chain: readonly {
|
|
109
|
+
readonly policy: ToolPolicy;
|
|
110
|
+
}[]): Pick<InheritedGate, "pathRuleAnchors" | "pathRuleInstallations">;
|
|
@@ -1,3 +1,107 @@
|
|
|
1
|
+
import { isRemoteExecutionEnv } from "../remote-env.js";
|
|
2
|
+
import { isAbsolutePathForm } from "../../tools/fs/safety.js";
|
|
3
|
+
import { installPolicyAnchors, installationAnchorsOf, installedFrom } from "../path-rule-binding.js";
|
|
4
|
+
import { CheckpointError } from "../checkpoint-store.js";
|
|
1
5
|
export function deploymentCanRerootChild(deps) {
|
|
2
6
|
return deps.executionEnvFactory !== undefined;
|
|
3
7
|
}
|
|
8
|
+
export async function childPlacementOf(env, childRoot, asked, callerRoot) {
|
|
9
|
+
const canon = async (p) => {
|
|
10
|
+
try {
|
|
11
|
+
const r = await env.canonicalPath(p);
|
|
12
|
+
return r.ok ? r.value : p;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return p;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const root = await canon(childRoot);
|
|
19
|
+
if (asked !== undefined && (await canon(asked)) === root)
|
|
20
|
+
return "honored";
|
|
21
|
+
const tree = await canon(callerRoot);
|
|
22
|
+
const sep = tree.includes("\\") && !tree.includes("/") ? "\\" : "/";
|
|
23
|
+
return root === tree || root.startsWith(tree.endsWith(sep) ? tree : tree + sep) ? "caller_tree" : "elsewhere";
|
|
24
|
+
}
|
|
25
|
+
export function childPlacementOnLeg(env, internals, root) {
|
|
26
|
+
const asked = internals.isolation === "worktree" ? (isRemoteExecutionEnv(env) ? undefined : root) : internals.parentCwd;
|
|
27
|
+
return childPlacementOf(env, root, asked, internals.callerRoot);
|
|
28
|
+
}
|
|
29
|
+
export function worktreeNotHonored(cwd) {
|
|
30
|
+
return Object.assign(new Error(`isolation "worktree" was requested but cannot be honored: the execution environment factory placed the sub-agent at "${cwd}", inside the caller's own tree, instead of the worktree it was offered. The sub-agent was not started (fail-closed — no silent fallback to the shared working tree).`), { code: "isolation.unavailable" });
|
|
31
|
+
}
|
|
32
|
+
const AS_RECORDED = (p) => p;
|
|
33
|
+
function recordedAnchors(r, rebase) {
|
|
34
|
+
if (r === null || r === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
const read = (v) => {
|
|
37
|
+
if (typeof v !== "string")
|
|
38
|
+
return undefined;
|
|
39
|
+
const p = rebase(v);
|
|
40
|
+
return isAbsolutePathForm(p) ? p : undefined;
|
|
41
|
+
};
|
|
42
|
+
const [root, home] = [read(r.root), read(r.home)];
|
|
43
|
+
return { ...(root !== undefined ? { root } : {}), ...(home !== undefined ? { home } : {}) };
|
|
44
|
+
}
|
|
45
|
+
export function chainTopAnchors(cfg) {
|
|
46
|
+
if (cfg.placement === "elsewhere")
|
|
47
|
+
return cfg.leg;
|
|
48
|
+
return (cfg.live !== undefined ? recordedAnchors(cfg.live, AS_RECORDED) : recordedAnchors(cfg.seed, cfg.rebase)) ?? cfg.leg;
|
|
49
|
+
}
|
|
50
|
+
const GAP_REASON = { absent: "installation_record_absent", misaligned: "installation_record_misaligned", invalid: "installation_record_invalid" };
|
|
51
|
+
function dependsOnInstallation(policy) {
|
|
52
|
+
if (installedFrom(policy) !== undefined)
|
|
53
|
+
return false;
|
|
54
|
+
return typeof policy.pathRules === "function" && policy.pathRules({}).some((row) => row.unanchored !== undefined);
|
|
55
|
+
}
|
|
56
|
+
function recordedInstallation(installations, chainLength, position, rebase) {
|
|
57
|
+
const [record, read] = installations.live !== undefined ? [installations.live, AS_RECORDED] : [installations.seed, rebase];
|
|
58
|
+
if (record === undefined)
|
|
59
|
+
return "absent";
|
|
60
|
+
if (record.length !== chainLength)
|
|
61
|
+
return "misaligned";
|
|
62
|
+
const a = recordedAnchors(record[position], read);
|
|
63
|
+
return a?.root === undefined ? "invalid" : a;
|
|
64
|
+
}
|
|
65
|
+
export function chainInstallationGaps(chain, installations, rebase) {
|
|
66
|
+
return chain.flatMap((policy, position) => {
|
|
67
|
+
if (!dependsOnInstallation(policy))
|
|
68
|
+
return [];
|
|
69
|
+
const r = recordedInstallation(installations, chain.length, position, rebase);
|
|
70
|
+
return typeof r === "string" ? [{ position, gap: r }] : [];
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export function chainInstallationRefusal(gaps, pending) {
|
|
74
|
+
const first = gaps[0];
|
|
75
|
+
const form = { absent: "absent (a chain written before installation records existed, or one built without them)", misaligned: "misaligned (it does not cover the chain entry for entry)", invalid: "invalid for that entry (it names no absolute root)" }[first.gap];
|
|
76
|
+
return new CheckpointError("resume.parent_constraint_mismatch", `the parent-constraint chain carries ${gaps.length} polic${gaps.length === 1 ? "y" : "ies"} (entry ${gaps.map((g) => g.position).join(", ")}) whose path rules resolve against the leg that installed ${gaps.length === 1 ? "it" : "them"}, and the installation record is ${form} — installing ${gaps.length === 1 ? "it" : "them"} anywhere else would move what ${gaps.length === 1 ? "it protects" : "they protect"}, so the chain is refused` +
|
|
77
|
+
(pending ? " before the claim (the checkpoint stays pending). A task parked before 7.46.0 under such a chain cannot be resumed by this version: let a worker of the version that parked it finish it, or let it expire and submit the task again." : "."), { reason: GAP_REASON[first.gap] });
|
|
78
|
+
}
|
|
79
|
+
export function chainPolicyOnLeg(cfg) {
|
|
80
|
+
if (cfg.placement !== "elsewhere") {
|
|
81
|
+
const gaps = chainInstallationGaps(cfg.chain, cfg.installations, cfg.rebase);
|
|
82
|
+
if (gaps.length > 0)
|
|
83
|
+
throw chainInstallationRefusal(gaps, false);
|
|
84
|
+
}
|
|
85
|
+
const recorded = (position) => {
|
|
86
|
+
const r = recordedInstallation(cfg.installations, cfg.chain.length, position, cfg.rebase);
|
|
87
|
+
return typeof r === "string" ? undefined : r;
|
|
88
|
+
};
|
|
89
|
+
const chained = (policy, position) => (cfg.placement === "elsewhere" ? installPolicyAnchors(policy, cfg.leg) : installedFrom(policy) !== undefined ? policy : installPolicyAnchors(policy, recorded(position) ?? cfg.chainTop)) ?? policy;
|
|
90
|
+
const own = (policy) => {
|
|
91
|
+
if (policy === undefined)
|
|
92
|
+
return undefined;
|
|
93
|
+
const position = cfg.placement === "elsewhere" ? -1 : cfg.chain.findIndex((c) => (installedFrom(c) ?? c) === (installedFrom(policy) ?? policy));
|
|
94
|
+
return position >= 0 ? chained(cfg.chain[position], position) : installPolicyAnchors(policy, cfg.leg);
|
|
95
|
+
};
|
|
96
|
+
return { own, chained };
|
|
97
|
+
}
|
|
98
|
+
export function installationRecordOf(policy) {
|
|
99
|
+
const a = installationAnchorsOf(policy);
|
|
100
|
+
return a?.root === undefined ? null : { root: a.root, ...(a.home !== undefined ? { home: a.home } : {}) };
|
|
101
|
+
}
|
|
102
|
+
export function chainAnchorRecords(chainTop, chain) {
|
|
103
|
+
return {
|
|
104
|
+
...(chainTop.root !== undefined ? { pathRuleAnchors: { root: chainTop.root, ...(chainTop.home !== undefined ? { home: chainTop.home } : {}) } } : {}),
|
|
105
|
+
...(chain.some((pc) => installationAnchorsOf(pc.policy)?.root !== undefined) ? { pathRuleInstallations: chain.map((pc) => installationRecordOf(pc.policy)) } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RunnerDeps } from "../types.js";
|
|
2
|
+
/** The retired-seat door ({@link assertNoRetiredRunnerDeps}), then the seats whose value is screened at construction and
|
|
3
|
+
* never again: the declared configuration root must be the absolute path of an existing directory — a root that names no
|
|
4
|
+
* directory guards nothing, so it is refused here rather than read as absent. */
|
|
5
|
+
export declare function assertRunnerDepsDoors(deps: RunnerDeps): void;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { checkedAgentConfigDir } from "../agent-config-dir.js";
|
|
2
|
+
import { assertNoRetiredRunnerDeps } from "./retired-runner-deps.js";
|
|
3
|
+
export function assertRunnerDepsDoors(deps) {
|
|
4
|
+
assertNoRetiredRunnerDeps(deps);
|
|
5
|
+
checkedAgentConfigDir(deps.agentConfigDir, "RunnerDeps.agentConfigDir");
|
|
6
|
+
}
|
|
@@ -393,6 +393,8 @@ export interface Prepared {
|
|
|
393
393
|
cwdRef?: CwdRef;
|
|
394
394
|
/** The root the hand tools resolve root-relative spellings against (present with hands; read-only hands included). */
|
|
395
395
|
handsRoot?: string;
|
|
396
|
+
/** The leg's target-reading stamp (present with hands): the resume belts stamp the executed bytes' request with it. */
|
|
397
|
+
stampCallTarget?: import("../effective-path-target.js").CallTargetStamp;
|
|
396
398
|
/** The run's TOOL-PROGRESS lane seat, late-bound for the same reason `harnessRef` is (the tool band is
|
|
397
399
|
* built during prepare; the lane needs `pushContent` and `ident`, which exist only after it returns).
|
|
398
400
|
* The driver binds `current` right after `pushContent` exists. Always present on a completed prepare —
|
|
@@ -1417,6 +1419,29 @@ export interface InheritedGate {
|
|
|
1417
1419
|
* had a governance surface (their opaque posture is untouched). Serializable; persists on checkpoints.
|
|
1418
1420
|
*/
|
|
1419
1421
|
orgAdmissionGoverned?: true;
|
|
1422
|
+
/**
|
|
1423
|
+
* The chain's TOP leg's root and environment home (data half, serializable; persists on the checkpoint's data half):
|
|
1424
|
+
* the anchors of the person's global and session path rows and of every `~/` row on this chain — a child judges them
|
|
1425
|
+
* where the chain was installed, not at its own root (a project row anchors at its own scope root). Emitted by every leg
|
|
1426
|
+
* that hands a chain down (a top leg emits its own). Absent on an older chain ⇒ the child is its own top. A child placed
|
|
1427
|
+
* in ANOTHER filesystem namespace (its environment did not put it where it was asked, outside the caller's tree) ignores
|
|
1428
|
+
* it and takes its own leg as the top.
|
|
1429
|
+
*/
|
|
1430
|
+
pathRuleAnchors?: {
|
|
1431
|
+
readonly root: string;
|
|
1432
|
+
readonly home?: string;
|
|
1433
|
+
};
|
|
1434
|
+
/**
|
|
1435
|
+
* Where each `parentConstraints` entry was INSTALLED (data half, serializable; persists on the checkpoint's data half):
|
|
1436
|
+
* position for position with `parentConstraints`, the root and home its path rules are anchored at (`null` for an entry
|
|
1437
|
+
* with no path rules). A chain re-supplied with uninstalled policies (a resume in another process, a host-built chain)
|
|
1438
|
+
* installs each where this records — never at another leg's anchors. Absent on an older chain: an uninstalled entry is
|
|
1439
|
+
* installed at the chain's top leg (`pathRuleAnchors`).
|
|
1440
|
+
*/
|
|
1441
|
+
pathRuleInstallations?: ReadonlyArray<{
|
|
1442
|
+
readonly root: string;
|
|
1443
|
+
readonly home?: string;
|
|
1444
|
+
} | null>;
|
|
1420
1445
|
/** Live ancestor caller-policy constraints (opaque half), ancestors first. `policy` is the ancestor's
|
|
1421
1446
|
* RESOLVED caller policy (`spec.toolPolicy ?? deps.toolPolicy` — the same slot the ancestor itself
|
|
1422
1447
|
* enforces, so a deps-level baseline is inherited too).
|
|
@@ -2137,6 +2162,20 @@ export interface RunInternals {
|
|
|
2137
2162
|
* callers (ctx-injection posture, same as `parentCwd`).
|
|
2138
2163
|
*/
|
|
2139
2164
|
requestedCwd?: string;
|
|
2165
|
+
/**
|
|
2166
|
+
* The SPAWNING run's own root, filled ONLY by core delegation callers beside `parentCwd` (ctx-injection posture): with
|
|
2167
|
+
* it the child's prepare judges where its environment actually placed it against where it was asked
|
|
2168
|
+
* (`childPlacementOf`, child-tree.ts: honored / inside the caller's tree / elsewhere) — the fact the
|
|
2169
|
+
* worktree request below and the anchors of the rules the child inherits read. Absent ⇒ no placement is judged (a top
|
|
2170
|
+
* leg, a caller that does not delegate through core).
|
|
2171
|
+
*/
|
|
2172
|
+
callerRoot?: string;
|
|
2173
|
+
/**
|
|
2174
|
+
* `parentCwd` is a worktree the caller minted for THIS child (the delegation tool's `isolation: "worktree"`): a child
|
|
2175
|
+
* its environment placed inside the caller's own tree instead is refused before it starts (`isolation.unavailable`).
|
|
2176
|
+
* Filled ONLY by the core delegation tool; never a {@link TaskSpec} field.
|
|
2177
|
+
*/
|
|
2178
|
+
worktreeOffered?: true;
|
|
2140
2179
|
/**
|
|
2141
2180
|
* Subagent steer verb (dogfood finding 2026-07-03, "中途插话"): the host run's opt-in
|
|
2142
2181
|
* SUBAGENT-STEER-HANDLE sink. When set, `createSubagentTool` runs each child via `runTaskStream`
|
|
@@ -2237,6 +2276,10 @@ export interface RunInternals {
|
|
|
2237
2276
|
* non-isolated run reports its plain root with `isolated: false` — the observation face stays complete, while
|
|
2238
2277
|
* the honest non-claim keeps a consumer from mistaking a SHARED tree for its own worktree.
|
|
2239
2278
|
*/
|
|
2279
|
+
/** Where a delegated child's environment placed it against where its caller asked (`childPlacementOf`, child-tree.ts):
|
|
2280
|
+
* `honored` — where it was asked; `caller_tree` — elsewhere inside the caller's own tree; `elsewhere` — another
|
|
2281
|
+
* filesystem namespace. */
|
|
2282
|
+
export type ChildPlacement = "honored" | "caller_tree" | "elsewhere";
|
|
2240
2283
|
export interface ResolvedWorkspace {
|
|
2241
2284
|
/** The task's effective working root — the same value every fence / LSP / prompt consumer uses. */
|
|
2242
2285
|
cwd: string;
|
|
@@ -2247,6 +2290,9 @@ export interface ResolvedWorkspace {
|
|
|
2247
2290
|
* or names an unrelated directory — on the host. `isolated && !remote` is the only combination under
|
|
2248
2291
|
* which `cwd` is a host path a consumer may diff / merge / remove. */
|
|
2249
2292
|
remote: boolean;
|
|
2293
|
+
/** Where a delegated child's environment placed it against where its caller asked; absent on a leg no delegation
|
|
2294
|
+
* caller placed. */
|
|
2295
|
+
placement?: ChildPlacement;
|
|
2250
2296
|
}
|
|
2251
2297
|
/**
|
|
2252
2298
|
* The resolver's answer, with the WITHHELD cause beside the value.
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { type ClassifierDenyCause, type DeniedBy, type GateOutcome, type ModeRefusal, type ModeRelease, type Settlement } from "../gate-outcome.js";
|
|
25
25
|
import type { AutoModeClassifierRound } from "../auto-mode.js";
|
|
26
|
-
import { type Arbitration, type AskClassification, type AskOrigin, type AskOriginFacts } from "../ask-origin.js";
|
|
26
|
+
import { type Arbitration, type AskClassification, type AskFace, type AskOrigin, type AskOriginFacts } from "../ask-origin.js";
|
|
27
27
|
import { type PermissionResult, type ResolvedAsk, type ToolCallRequest } from "../tool-policy.js";
|
|
28
28
|
import type { SafeNotifier } from "../safe-notify.js";
|
|
29
29
|
import type { HookSeatOutcome, PermissionDeniedPayload, PreToolUseResult, ToolGateInput, ToolGateResult } from "../hooks.js";
|
|
@@ -143,6 +143,9 @@ export interface PreToolUseScreening {
|
|
|
143
143
|
/** The model-facing text for an answer the decision receipt refused (an ENGINE fail-closed, not the face's deny). */
|
|
144
144
|
refusedReason(answer: PreToolUseResult): string;
|
|
145
145
|
}
|
|
146
|
+
/** The FACE a pass's questions are put on ({@link askFaceOf} of the call's wire name) — the one reading of it the
|
|
147
|
+
* write-protection tighten (the fold) and every class derivation (the lanes) make. */
|
|
148
|
+
export declare function passAskFace(pass: Pick<GatePass, "req">): AskFace;
|
|
146
149
|
/**
|
|
147
150
|
* ONE gate pass — the record the station groups (`runGateFold`, `runGateLanes`) and the exits read and write,
|
|
148
151
|
* built once per call by `runToolGate` before the screening pass. What used to be the closure-captured `let`
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { screenGateOutcome, SETTLEMENT_IS_REFUSAL } from "../gate-outcome.js";
|
|
2
|
-
import { classifyAsk, headlessApprovalRemedy } from "../ask-origin.js";
|
|
2
|
+
import { askFaceOf, classifyAsk, headlessApprovalRemedy } from "../ask-origin.js";
|
|
3
|
+
import { wireOf } from "../tool-name-readings.js";
|
|
3
4
|
import { isProtectedWrite } from "../tool-registry.js";
|
|
4
5
|
import { askUnresolvableCauseOf, classifierDenyCauseOf, classifierRoundOf, decisionText, engineSettlementOf } from "../tool-policy.js";
|
|
5
6
|
import { askUnresolvableRemedySeat } from "../ask-unresolvable-notice.js";
|
|
@@ -97,6 +98,9 @@ function withParkFailureCause(reason, parkFailed) {
|
|
|
97
98
|
`(${inlineUntrusted(parkFailed, PARK_FAILURE_CAUSE_MAX)}), so the refusal above is what the fallback had ` +
|
|
98
99
|
`left to say, not the reason the call stopped.`);
|
|
99
100
|
}
|
|
101
|
+
export function passAskFace(pass) {
|
|
102
|
+
return askFaceOf(wireOf("gate.askFace", pass.req));
|
|
103
|
+
}
|
|
100
104
|
export async function engineFailClosedExit(pass, reason) {
|
|
101
105
|
const { input, toolName, toolCallId, notifyPermissionDenied } = pass;
|
|
102
106
|
const gate = directDeny("hook");
|