@sema-agent/core 7.9.2 → 7.11.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/agents/child-model-seat.d.ts +71 -14
  3. package/dist/agents/child-model-seat.js +23 -7
  4. package/dist/agents/subagent.d.ts +2 -2
  5. package/dist/agents/subagent.js +8 -5
  6. package/dist/agents/teacher.js +2 -2
  7. package/dist/core/ask-unresolvable-notice.d.ts +52 -0
  8. package/dist/core/ask-unresolvable-notice.js +25 -0
  9. package/dist/core/auto-mode-defaults.d.ts +15 -3
  10. package/dist/core/auto-mode-defaults.js +1 -0
  11. package/dist/core/auto-mode.d.ts +86 -23
  12. package/dist/core/auto-mode.js +43 -12
  13. package/dist/core/checkpoint-store.d.ts +14 -0
  14. package/dist/core/checkpoint-store.js +2 -1
  15. package/dist/core/engine-notice.d.ts +28 -7
  16. package/dist/core/gate-fold.js +1 -0
  17. package/dist/core/gate-lanes.d.ts +6 -1
  18. package/dist/core/gate-lanes.js +60 -18
  19. package/dist/core/governance-codes.d.ts +1 -1
  20. package/dist/core/governance-codes.js +4 -0
  21. package/dist/core/hooks.d.ts +37 -1
  22. package/dist/core/hooks.js +2 -0
  23. package/dist/core/permission-rule-model.d.ts +53 -16
  24. package/dist/core/permission-rule-model.js +59 -21
  25. package/dist/core/permission-rules.d.ts +6 -4
  26. package/dist/core/permission-rules.js +14 -14
  27. package/dist/core/persisted-rule-arms.js +4 -3
  28. package/dist/core/read-only-shell-table.d.ts +87 -0
  29. package/dist/core/read-only-shell-table.js +485 -0
  30. package/dist/core/read-only-shell.d.ts +42 -0
  31. package/dist/core/read-only-shell.js +316 -0
  32. package/dist/core/roles.d.ts +3 -2
  33. package/dist/core/runner/contracts.d.ts +26 -2
  34. package/dist/core/runner/denial-limit-arms.d.ts +14 -3
  35. package/dist/core/runner/denial-limit-arms.js +15 -5
  36. package/dist/core/runner/gate-exit.d.ts +5 -0
  37. package/dist/core/runner/permission-rule-lanes.d.ts +7 -1
  38. package/dist/core/runner/permission-rule-lanes.js +9 -3
  39. package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
  40. package/dist/core/runner/prepare-caps-and-workflow.js +35 -14
  41. package/dist/core/runner/prepare-gate-stations.d.ts +3 -2
  42. package/dist/core/runner/prepare-gate-stations.js +12 -0
  43. package/dist/core/runner/prepare-policy-chain.js +7 -6
  44. package/dist/core/runner/prepare-task.js +1 -1
  45. package/dist/core/runner/prepare-turn-wiring.js +1 -1
  46. package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
  47. package/dist/core/runner/prepare-wiring-manifest.js +8 -1
  48. package/dist/core/runner/runtask.d.ts +33 -32
  49. package/dist/core/runner/runtask.js +63 -33
  50. package/dist/core/runner-deps.d.ts +9 -2
  51. package/dist/core/shell-lexer.d.ts +18 -0
  52. package/dist/core/shell-lexer.js +17 -10
  53. package/dist/core/shell-wrapper-table.js +8 -5
  54. package/dist/core/swappable-deps.d.ts +90 -0
  55. package/dist/core/swappable-deps.js +55 -0
  56. package/dist/core/tool-policy.d.ts +30 -1
  57. package/dist/core/tool-policy.js +6 -2
  58. package/dist/core/tools.d.ts +28 -7
  59. package/dist/core/tools.js +44 -4
  60. package/dist/core/trace.d.ts +15 -0
  61. package/dist/core/wiring-manifest.d.ts +15 -1
  62. package/dist/core/wiring-manifest.js +10 -2
  63. package/dist/core/workflow-journal-store.d.ts +21 -2
  64. package/dist/core/workflow-journal-store.js +1 -1
  65. package/dist/engine/execution-env/node-execution-env.d.ts +2 -0
  66. package/dist/engine/execution-env/node-execution-env.js +2 -1
  67. package/dist/engine/harness/agent-harness.d.ts +3 -1
  68. package/dist/engine/harness/agent-harness.js +1 -1
  69. package/dist/engine/harness/types.d.ts +15 -2
  70. package/dist/index.d.ts +8 -4
  71. package/dist/index.js +7 -3
  72. package/dist/orchestration/run-workflow-tool.d.ts +22 -2
  73. package/dist/orchestration/run-workflow-tool.js +14 -1
  74. package/dist/orchestration/workflow-governance.d.ts +3 -2
  75. package/dist/orchestration/workflow-observe.d.ts +1 -1
  76. package/dist/orchestration/workflow-observe.js +2 -0
  77. package/dist/orchestration/workflow-primitives.d.ts +4 -1
  78. package/dist/orchestration/workflow-primitives.js +1 -6
  79. package/dist/orchestration/workflow-types.d.ts +37 -2
  80. package/dist/orchestration/workflow-types.js +16 -0
  81. package/dist/orchestration/workflow.d.ts +53 -6
  82. package/dist/orchestration/workflow.js +319 -51
  83. package/dist/prompt-assembly/turn-snapshot.d.ts +4 -2
  84. package/dist/stores/file/workflow-journal-store.js +10 -3
  85. package/package.json +1 -1
  86. package/test/export-surface.snapshot.json +85 -5
@@ -0,0 +1,316 @@
1
+ import { readShellCommand } from "./shell-lexer.js";
2
+ import { FIND_ACTION_PRIMARIES, FIND_NEWER_PRIMARY, FIND_VALUE_PRIMARIES, FLAG_VALUE_ACCEPTS, READ_ONLY_BARE_ONLY, READ_ONLY_BARE_PROGRAMS, READ_ONLY_COMMAND_TABLE, READ_ONLY_ENV_NAMES, READ_ONLY_EXACT_FORMS, READ_ONLY_GLOB_PROGRAMS, XARGS_READ_ONLY_TARGETS, dockerRetargetDanger, } from "./read-only-shell-table.js";
3
+ const NOT = (reason) => ({ readOnly: false, reason });
4
+ const READ_ONLY = { readOnly: true };
5
+ const BARE_PROGRAMS = new Set(READ_ONLY_BARE_PROGRAMS.filter((p) => !p.includes(" ")));
6
+ const BARE_MULTIWORD = READ_ONLY_BARE_PROGRAMS.filter((p) => p.includes(" ")).map((p) => p.split(" "));
7
+ const GLOB_PROGRAMS = new Set(READ_ONLY_GLOB_PROGRAMS);
8
+ const BARE_ONLY = new Set(READ_ONLY_BARE_ONLY);
9
+ const ENV_NAMES = new Set(READ_ONLY_ENV_NAMES);
10
+ const FIND_ACTIONS = new Set(FIND_ACTION_PRIMARIES);
11
+ const FIND_VALUES = new Set(FIND_VALUE_PRIMARIES);
12
+ const XARGS_TARGETS = XARGS_READ_ONLY_TARGETS;
13
+ const TABLE_ROWS = Object.entries(READ_ONLY_COMMAND_TABLE)
14
+ .map(([key, row]) => ({ words: key.split(" "), row }))
15
+ .sort((a, b) => b.words.length - a.words.length);
16
+ const READING_REDIRECTS = new Set(["<", "<<", "<<-", "<&", "<<<"]);
17
+ const LITERAL_ASSIGNMENT = /^([A-Za-z_][A-Za-z0-9_]*)\+?=(?:"[^"$`\\]*"|'[^']*'|[A-Za-z0-9_./:+-]*)$/;
18
+ const ASSIGNMENT_NAME = /^([A-Za-z_][A-Za-z0-9_]*)(?:\[[^\]]*\])?\+?=/;
19
+ const FLAG_WORD = /^-[a-zA-Z0-9_-]/;
20
+ const NUMBER_LITERAL = /^[-+]?(0[xX][0-9a-fA-F]+|[0-9]+#[0-9a-zA-Z]+|[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)$/;
21
+ const PRINTF_LENGTH = "[lLhqjzZt]*";
22
+ const PRINTF_ESCAPE = new RegExp(`%[^%a-zA-Z]*${PRINTF_LENGTH}\\\\[0-7xX]`);
23
+ const PRINTF_NUMERIC = new RegExp(`%[-+ 0#']*[0-9.*]*${PRINTF_LENGTH}[diouxXeEfFgGaAn]`);
24
+ const JQ_DANGER_FLAG = /^['"]?-[a-zA-Z]*[fL]/;
25
+ const JQ_DANGER_TEXT = /--from-file|--rawfile|--slurpfile|--run-tests|--library-path|\benv\b|\$ENV\b|\binclude\b|\bimport\b/;
26
+ export function readOnlyShellVerdict(command, shape = readShellCommand(command)) {
27
+ if (shape.grouped)
28
+ return NOT("a subshell, group or control structure — not a flat list of simple commands");
29
+ if (shape.segments.length === 0)
30
+ return NOT("no command");
31
+ if (shape.backgrounded)
32
+ return NOT("a background `&` defers execution past the approval-time reading");
33
+ if (shape.strayRedirection)
34
+ return NOT("a redirection on a piece that runs no program — the shell still opens its target");
35
+ for (const seg of shape.segments) {
36
+ if (seg.unreadable !== undefined)
37
+ return NOT(seg.unreadable);
38
+ if (seg.peelUnreadable !== undefined)
39
+ return NOT(seg.peelUnreadable);
40
+ for (const w of [...seg.argv, ...seg.redirections.map((r) => r.target)]) {
41
+ if (expansionKind(w) === "variable")
42
+ return NOT("a variable, arithmetic or command substitution — its value is not known at approval time");
43
+ if (w.expands !== false && /[()]/.test(w.raw))
44
+ return NOT("a pattern with a parenthesised part (an extended pattern or a shell-specific glob qualifier)");
45
+ if (/^=[A-Za-z_]/.test(w.raw) || w.raw.includes("~["))
46
+ return NOT("a shell-specific expansion spelling (`=cmd`, `~[…]`)");
47
+ }
48
+ }
49
+ const programs = shape.segments.map((s) => peelBuiltinPrefix(s.argv.slice(leadingAssignmentCount(s.argv)).map((w) => w.text))[0] ?? "");
50
+ if (shape.segments.length > 1 && programs.includes("cd") && programs.includes("git"))
51
+ return NOT("a compound that changes directory and runs git");
52
+ for (const seg of shape.segments) {
53
+ const verdict = segmentVerdict(seg);
54
+ if (!verdict.readOnly)
55
+ return verdict;
56
+ }
57
+ return READ_ONLY;
58
+ }
59
+ function expansionKind(w) {
60
+ if (w.expands === false)
61
+ return "none";
62
+ if (/[$`]|[<>]\(/.test(w.raw))
63
+ return "variable";
64
+ if (w.expands === "many")
65
+ return "glob";
66
+ return "none";
67
+ }
68
+ function leadingAssignmentCount(argv) {
69
+ let k = 0;
70
+ while (k < argv.length && ASSIGNMENT_NAME.test(argv[k].raw))
71
+ k++;
72
+ return k;
73
+ }
74
+ function peelBuiltinPrefix(words) {
75
+ let t = words;
76
+ for (;;) {
77
+ if (t[0] === "command") {
78
+ let r = 1;
79
+ while (t[r] !== undefined && /^-p+$/.test(t[r]))
80
+ r++;
81
+ if (t[r] === "--")
82
+ r++;
83
+ if (r >= t.length || t[r].startsWith("-"))
84
+ return t;
85
+ t = t.slice(r);
86
+ }
87
+ else if (t[0] === "builtin") {
88
+ const r = t[1] === "--" ? 2 : 1;
89
+ if (r >= t.length)
90
+ return t;
91
+ t = t.slice(r);
92
+ }
93
+ else if (t[0] === "noglob") {
94
+ if (t.length <= 1)
95
+ return t;
96
+ t = t.slice(1);
97
+ }
98
+ else
99
+ return t;
100
+ }
101
+ }
102
+ function segmentVerdict(seg) {
103
+ for (const r of seg.redirections) {
104
+ const target = r.target.text;
105
+ if (!READING_REDIRECTS.has(r.op) && target !== "/dev/null" && !(r.op === ">&" && /^\d+$/.test(target)))
106
+ return NOT(`the redirection \`${r.op} ${target}\` writes`);
107
+ if (/^\/dev\/(tcp|udp)\//.test(target))
108
+ return NOT("a redirection to a network pseudo-device");
109
+ }
110
+ const k = leadingAssignmentCount(seg.argv);
111
+ for (const w of seg.argv.slice(0, k)) {
112
+ const m = LITERAL_ASSIGNMENT.exec(w.raw);
113
+ if (m === null)
114
+ return NOT(`the assignment \`${w.raw}\` is not a literal value`);
115
+ if (!ENV_NAMES.has(m[1]))
116
+ return NOT(`the environment variable \`${m[1]}\` can alter what a program does`);
117
+ }
118
+ const run = seg.argv.slice(k);
119
+ if (run.length === 0)
120
+ return NOT("a bare assignment runs no program");
121
+ const words = peelBuiltinPrefix(run.map((w) => w.text));
122
+ const program = words[0];
123
+ if (run.some((w) => expansionKind(w) === "glob"))
124
+ return GLOB_PROGRAMS.has(program) ? READ_ONLY : NOT(`\`${program}\` with a glob argument — the glob may name anything`);
125
+ const structural = structuralReading(words);
126
+ if (structural !== null)
127
+ return structural ? READ_ONLY : NOT(`\`${program}\` with these arguments is not a read-only form`);
128
+ return tableReading(words, run);
129
+ }
130
+ function structuralReading(words) {
131
+ const program = words[0];
132
+ if (BARE_ONLY.has(program))
133
+ return words.length === 1;
134
+ for (const form of READ_ONLY_EXACT_FORMS)
135
+ if (words.length === form.length && words.every((w, i) => w === form[i]))
136
+ return true;
137
+ if (BARE_PROGRAMS.has(program))
138
+ return true;
139
+ for (const form of BARE_MULTIWORD)
140
+ if (words.length >= form.length && form.every((w, i) => words[i] === w))
141
+ return form[0] !== "docker" || !dockerRetargetDanger(words);
142
+ if (program === "echo")
143
+ return true;
144
+ if (program === "printf")
145
+ return printfReading(words);
146
+ if (program === "ls")
147
+ return true;
148
+ if (program === "cd")
149
+ return words.length <= 2;
150
+ if (program === "find") {
151
+ for (let i = 1; i < words.length; i++) {
152
+ const w = words[i];
153
+ if (FIND_ACTIONS.has(w))
154
+ return false;
155
+ if (FIND_VALUES.has(w) || FIND_NEWER_PRIMARY.test(w)) {
156
+ i++;
157
+ continue;
158
+ }
159
+ }
160
+ return true;
161
+ }
162
+ if (program === "history")
163
+ return words.length === 1 || (words.length === 2 && /^\d+$/.test(words[1]));
164
+ if (program === "arch")
165
+ return words.length === 1 || (words.length === 2 && (words[1] === "-h" || words[1] === "--help"));
166
+ if (program === "ifconfig")
167
+ return words.length === 1 || (words.length === 2 && /^[a-zA-Z]/.test(words[1]));
168
+ return null;
169
+ }
170
+ function printfReading(words) {
171
+ if (words[1]?.startsWith("-") === true && words[1] !== "--")
172
+ return false;
173
+ const at = words[1] === "--" ? 2 : 1;
174
+ const format = words[at] ?? "";
175
+ if (format.includes("$"))
176
+ return false;
177
+ const stripped = format.replace(/%%/g, "");
178
+ if (PRINTF_ESCAPE.test(stripped) || /\\[uU]/.test(stripped))
179
+ return false;
180
+ if (PRINTF_NUMERIC.test(stripped) || /%[^%a-zA-Z]*\*/.test(stripped)) {
181
+ for (let i = at + 1; i < words.length; i++) {
182
+ const operand = words[i];
183
+ if (operand.includes("[") || operand.includes("`") || operand.includes("$(") || !NUMBER_LITERAL.test(operand))
184
+ return false;
185
+ }
186
+ }
187
+ return true;
188
+ }
189
+ function tableReading(words, run) {
190
+ const program = words[0];
191
+ const hit = TABLE_ROWS.find((r) => words.length >= r.words.length && r.words.every((w, i) => words[i] === w));
192
+ if (hit !== undefined) {
193
+ const args = words.slice(hit.words.length);
194
+ for (const a of args) {
195
+ if (a.includes("$"))
196
+ return NOT(`\`${program}\`: an argument spells \`$\``);
197
+ if (a.includes("{") && (a.includes(",") || a.includes("..")))
198
+ return NOT(`\`${program}\`: a brace pattern argument`);
199
+ }
200
+ if (!flagWalk(args, hit.row, program))
201
+ return NOT(`\`${program}\`: an option outside its read-only set (or a malformed option value)`);
202
+ if (hit.row.wordsShape !== undefined ? !hit.row.wordsShape(args) : run.some((w) => w.raw.includes("`")))
203
+ return NOT(`\`${program}\`: arguments outside its read-only form`);
204
+ if (hit.row.wordsShape === undefined && (program === "rg" || program === "grep" || program === "egrep" || program === "fgrep") && run.some((w) => /[\n\r]/.test(w.raw)))
205
+ return NOT(`\`${program}\`: a line break inside an argument`);
206
+ if (hit.row.dangerous?.(args) === true)
207
+ return NOT(`\`${program}\`: an argument form upstream marks as writing, executing or resolving`);
208
+ return READ_ONLY;
209
+ }
210
+ if (program === "uniq" && words.slice(1).every((w, i, all) => /^-[a-zA-Z]+$/.test(w) || /^--[a-zA-Z-]+(=\S+)?$/.test(w) || (i > 0 && /^-[fsw]$/.test(all[i - 1]) && /^\d+$/.test(w))))
211
+ return gitOptionGuard(words);
212
+ if (program === "jq" && jqReading(run.slice(run.length - words.length + 1)))
213
+ return gitOptionGuard(words);
214
+ return NOT(`\`${program}\` is not a read-only program (or not in a read-only form)`);
215
+ }
216
+ function gitOptionGuard(words) {
217
+ if (words.includes("git") && words.some((w) => /^-c(=|$)|^--exec-path(=|$)|^--config-env(=|$)/.test(w)))
218
+ return NOT("a git configuration override on the line");
219
+ return READ_ONLY;
220
+ }
221
+ function jqReading(args) {
222
+ if (args.some((w) => JQ_DANGER_FLAG.test(w.raw) || JQ_DANGER_TEXT.test(w.raw)))
223
+ return false;
224
+ let i = 0;
225
+ while (i < args.length && (/^-[a-zA-Z]+$/.test(args[i].raw) || /^--[a-zA-Z-]+(=\S+)?$/.test(args[i].raw)))
226
+ i++;
227
+ if (i >= args.length)
228
+ return false;
229
+ for (; i < args.length; i++) {
230
+ const w = args[i];
231
+ const quoted = /^'[^'`]*'$|^"[^"`]*"$/.test(w.raw);
232
+ if (!quoted && (/^[-'"\s]/.test(w.raw) || /['"]/.test(w.raw)))
233
+ return false;
234
+ }
235
+ return true;
236
+ }
237
+ function flagWalk(args, row, program) {
238
+ let i = 0;
239
+ while (i < args.length) {
240
+ let word = args[i];
241
+ if (word === "") {
242
+ i++;
243
+ continue;
244
+ }
245
+ if (program === "xargs" && (!word.startsWith("-") || word === "--")) {
246
+ if (word === "--" && i + 1 < args.length) {
247
+ i++;
248
+ word = args[i];
249
+ }
250
+ return XARGS_TARGETS.includes(word);
251
+ }
252
+ if (word === "--") {
253
+ if (row.respectsDoubleDash !== false)
254
+ break;
255
+ i++;
256
+ continue;
257
+ }
258
+ if (FLAG_WORD.test(word)) {
259
+ const eq = word.indexOf("=");
260
+ const flag = eq === -1 ? word : word.slice(0, eq);
261
+ const attached = eq === -1 ? undefined : word.slice(eq + 1);
262
+ const arity = row.safeFlags[flag];
263
+ if (arity === undefined) {
264
+ if (program === "git" && /^-\d+$/.test(flag)) {
265
+ i++;
266
+ continue;
267
+ }
268
+ if ((program === "grep" || program === "egrep" || program === "fgrep" || program === "rg") && !flag.startsWith("--") && flag.length > 2) {
269
+ const base = flag.slice(0, 2);
270
+ const rest = flag.slice(2);
271
+ const baseArity = row.safeFlags[base];
272
+ if (baseArity !== undefined && /^\d+$/.test(rest) && (baseArity === "number" || baseArity === "string")) {
273
+ if (FLAG_VALUE_ACCEPTS[baseArity](rest)) {
274
+ i++;
275
+ continue;
276
+ }
277
+ return false;
278
+ }
279
+ }
280
+ if (!flag.startsWith("--") && flag.length > 2) {
281
+ for (let c = 1; c < flag.length; c++)
282
+ if (row.safeFlags[`-${flag[c]}`] !== "none")
283
+ return false;
284
+ i++;
285
+ continue;
286
+ }
287
+ return false;
288
+ }
289
+ if (arity === "none") {
290
+ if (attached !== undefined)
291
+ return false;
292
+ i++;
293
+ continue;
294
+ }
295
+ let value;
296
+ if (attached !== undefined) {
297
+ value = attached;
298
+ i++;
299
+ }
300
+ else {
301
+ const next = args[i + 1];
302
+ if (next === undefined || FLAG_WORD.test(next))
303
+ return false;
304
+ value = next;
305
+ i += 2;
306
+ }
307
+ if (arity === "string" && value.startsWith("-") && !(flag === "--sort" && program === "git" && /^-[a-zA-Z]/.test(value)))
308
+ return false;
309
+ if (!FLAG_VALUE_ACCEPTS[arity](value))
310
+ return false;
311
+ continue;
312
+ }
313
+ i++;
314
+ }
315
+ return true;
316
+ }
@@ -104,8 +104,9 @@ export interface ResolvedRole {
104
104
  * `modelRole` and NO run model: it runs on the deployment's roles alone — the boot default at the end of
105
105
  * its chain — and never follows the model a run was switched to. Only a run's CHILDREN follow the parent:
106
106
  * the delegation seat (`childModelSeat`) passes the parent's `Model` as `spec.model` with the role kept
107
- * on `modelRole`, so the role's presets still apply. A side channel that must follow a run passes that
108
- * run's model explicitly; nothing here infers it.
107
+ * on `modelRole` and keeps that role on an EXPLICIT-model seat too, so the explicit branch below reads
108
+ * the child's presets off the subagent role, not the default one. A side channel that must follow a run
109
+ * passes that run's model explicitly; nothing here infers it.
109
110
  */
110
111
  export declare function resolveTaskModel(spec: {
111
112
  model?: ModelRef;
@@ -1486,6 +1486,22 @@ export interface RunInternals {
1486
1486
  * see from `spec`. Consumed by the `run_workflow` tool wiring (S8c), not by `prepareTask` itself.
1487
1487
  */
1488
1488
  workflowDepth?: number;
1489
+ /**
1490
+ * #642 — the host's DECISIONS for parked workflow-agent rows (`wa*` rows with `status:"parked"`), a TRUSTED
1491
+ * run channel like `workflowDepth` (never a `TaskSpec` field, never a tool argument — a model cannot decide
1492
+ * an approval). A host that decided a workflow child's parked checkpoint launches the run that re-invokes
1493
+ * `Workflow({resumeFromRunId})` with the decision here; the Workflow tool applies the entries naming the
1494
+ * resumed run (`RunWorkflowOptions.parkedResume`) and the resume drives the parked child on instead of
1495
+ * re-parking. `inheritedGate` is the cross-process re-supply of the checkpoint's opaque parent constraints
1496
+ * (the same seat the background revival's `reviveClaim.parkedResume` takes). Consumed by the `run_workflow`
1497
+ * tool wiring, not by `prepareTask` itself.
1498
+ */
1499
+ workflowParkedResume?: ReadonlyArray<{
1500
+ runId: string;
1501
+ token: CheckpointToken;
1502
+ outcome: ResumeOutcome;
1503
+ inheritedGate?: InheritedGate;
1504
+ }>;
1489
1505
  /**
1490
1506
  * design/110 — set ONLY by the Agent tool's fork route (`Agent(subagent_type:"fork")`, a core caller) on the
1491
1507
  * child it spawns: this run IS a forked child. `prepareTask` threads it to tool ctx as `insideFork` so the
@@ -1762,6 +1778,14 @@ export interface RunInternals {
1762
1778
  * channel, same posture as every other field here.
1763
1779
  */
1764
1780
  sessionReadStates?: SessionReadFileStates;
1781
+ /**
1782
+ * #616 — the Runner's per-session breaker ledger ({@link import("../auto-mode.js").AutoModeBreakerLedger}):
1783
+ * the arming site's `onBreakerOpen` wrap records a trip here, the wiring-manifest phase of a LATER leg of
1784
+ * the same session reads the most recent one onto `autoMode.breaker`. Always set by the Runner's own
1785
+ * prepare call (overriding any caller value, like `sessionReadStates` beside it); absent on a standalone
1786
+ * prepareTask, where no trip is recorded and none is reported. Trusted internals channel.
1787
+ */
1788
+ autoModeBreakerLedger?: import("../auto-mode.js").AutoModeBreakerLedger;
1765
1789
  onTaskNotification?: (notification: TaskNotificationPayload,
1766
1790
  /** Injection tier (design/373 — the ladder is LIVE): "next" = the running turn's next boundary
1767
1791
  * (arrival order, consecutive frames batch); "later" = the run's would-otherwise-stop seat
@@ -2357,7 +2381,7 @@ export interface HarnessHandlersDeps {
2357
2381
  * THE MEMBER SET IS MEASURED, NOT DESIGNED: it is exactly the union of what those consumers read off a
2358
2382
  * Runner today (a TS-Program census over `src/`, pinned in test/runner-self-seat.test.ts so a member
2359
2383
  * nobody reads reds as dead weight and a consumer reaching for a member outside it reds as a widening).
2360
- * `swapModels`, `sideQuery` and the constructor are NOT here — no consumer reaches them; a host that
2384
+ * `swapDeps`, `sideQuery` and the constructor are NOT here — no consumer reaches them; a host that
2361
2385
  * needs them holds the class. The class implements this interface, so the two cannot drift apart.
2362
2386
  *
2363
2387
  * Signatures are spelled out (not `Pick<Runner, …>`), because naming the class from here would be the
@@ -2365,7 +2389,7 @@ export interface HarnessHandlersDeps {
2365
2389
  */
2366
2390
  /**
2367
2391
  * The Runner's deployment deps as a LIVE read for the lanes that read them after an await (the resume ladder's
2368
- * rungs). `Runner.deps` is not a constant: `swapModels` replaces the object, so a lane handed the object at its
2392
+ * rungs). `Runner.deps` is not a constant: `swapDeps` replaces the object, so a lane handed the object at its
2369
2393
  * call would read a stale catalog — and a stale anything — where the one-function ladder read `this.deps.x` at the
2370
2394
  * instant of the read. The seat is the Runner's own view (a getter over the private field), so `runner.deps.x`
2371
2395
  * in a lane is exactly `this.deps.x` in the method it came from.
@@ -1,4 +1,4 @@
1
- import { type AutoModeDecider, type AutoModeDenialLimitOptions, type AutoModeDenialTracker, type DenialLimitFallback, type DenialLimitFallbackFace, type UnarmedDenialLimitFallback } from "../auto-mode.js";
1
+ import { type AutoModeDecider, type AutoModeDenialLimitOptions, type AutoModeDenialTracker, type AutoModeUnavailableCause, type DenialLimitFallback, type DenialLimitFallbackFace, type UnarmedDenialLimitFallback } from "../auto-mode.js";
2
2
  import { type AutoModeArmingRecipe } from "../auto-mode-arming.js";
3
3
  import type { PermissionResult, ResolvedAsk, ToolCallRequest } from "../tool-policy.js";
4
4
  import { type EngineNotice } from "../types.js";
@@ -84,10 +84,14 @@ export type InheritedAsk = Extract<PermissionResult, {
84
84
  }>;
85
85
  /** The members of a surviving ask that ride onto the approval request with a COMPUTED value — what the
86
86
  * four request mint stations (the gate's own and the three inherited-lane ones) spread after the seats
87
- * they spell themselves: the ask's origin word and its denial-limit fallback. */
87
+ * they spell themselves: the ask's origin word, its denial-limit fallback and the classifier-unavailable
88
+ * fact (#616). */
88
89
  export interface AskRequestCarry {
89
90
  origin?: AskOrigin;
90
91
  denialLimitFallback?: DenialLimitFallback;
92
+ classifierUnavailable?: {
93
+ readonly cause: AutoModeUnavailableCause;
94
+ };
91
95
  }
92
96
  /** The GATE's own mint station: both members are read off the surviving decision, which is the gate's
93
97
  * OWN snapshot (its stamp point spreads the policy's object and stamps the word onto the copy), so the
@@ -99,7 +103,9 @@ export declare function gateAskCarry(decision: InheritedAsk, liveApprover: boole
99
103
  * fallback member it captured — each read ONCE there and threaded here, never re-read off the
100
104
  * caller-owned object (a stateful accessor could otherwise answer the card with one value and the
101
105
  * settlement with another; a self-declared `origin` on the ancestor's decision is never read). */
102
- export declare function inheritedAskCarry(origin: AskOrigin, fallback: UnarmedDenialLimitFallback | undefined, liveApprover: boolean, tracker: AutoModeDenialTracker | undefined): AskRequestCarry;
106
+ export declare function inheritedAskCarry(judged: Pick<Extract<InheritedClassifierJudgment<InheritedAsk>, {
107
+ kind: "resolve";
108
+ }>, "origin" | "fallback" | "ask">, liveApprover: boolean, tracker: AutoModeDenialTracker | undefined): AskRequestCarry;
103
109
  /** What {@link judgeInheritedClassifier} hands back to the arm that called it. */
104
110
  export type InheritedClassifierJudgment<A extends InheritedAsk> =
105
111
  /** The frozen classifier allowed — the arm returns its own allow shape (with or without the ask's rewrite). */
@@ -118,6 +124,11 @@ export type InheritedClassifierJudgment<A extends InheritedAsk> =
118
124
  * unchanged-ask branches it is the SAME read the exclusion judged by (a caller-owned decision with a
119
125
  * stateful accessor cannot answer the exclusion with one word and the card with another); on the
120
126
  * bound branch it is derived from the re-spoken ask this station minted (its own object). */
127
+ /** `ask` is the incoming ask unchanged — EXCEPT on an UNAVAILABLE round, where it is an engine-owned copy
128
+ * carrying `classifierUnavailable: { cause }` (#616): the fact rides the ASK because the ask is what the
129
+ * wrapper arms hand back to the gate when the frozen approver answers unavailable (the inherited-unavailable
130
+ * float), and the gate's own park carry reads the fact off the decision it parks — a side member on this
131
+ * judgment would have been lost on that route (codex r1). The request mint reads the same object. */
121
132
  | {
122
133
  kind: "resolve";
123
134
  ask: A;
@@ -60,10 +60,18 @@ function fallbackCarry(fallback, liveApprover, tracker) {
60
60
  return { denialLimitFallback: liveApprover && tracker !== undefined ? tracker.armTimedWindow(fallback) : unarmedWindow(fallback) };
61
61
  }
62
62
  export function gateAskCarry(decision, liveApprover, tracker) {
63
- return { ...(decision.origin !== undefined ? { origin: decision.origin } : {}), ...fallbackCarry(decision.denialLimitFallback, liveApprover, tracker) };
63
+ return {
64
+ ...(decision.origin !== undefined ? { origin: decision.origin } : {}),
65
+ ...fallbackCarry(decision.denialLimitFallback, liveApprover, tracker),
66
+ ...(decision.classifierUnavailable !== undefined ? { classifierUnavailable: { cause: decision.classifierUnavailable.cause } } : {}),
67
+ };
64
68
  }
65
- export function inheritedAskCarry(origin, fallback, liveApprover, tracker) {
66
- return { origin, ...fallbackCarry(fallback, liveApprover, tracker) };
69
+ export function inheritedAskCarry(judged, liveApprover, tracker) {
70
+ return {
71
+ origin: judged.origin,
72
+ ...fallbackCarry(judged.fallback, liveApprover, tracker),
73
+ ...(judged.ask.classifierUnavailable !== undefined ? { classifierUnavailable: { cause: judged.ask.classifierUnavailable.cause } } : {}),
74
+ };
67
75
  }
68
76
  export async function judgeInheritedClassifier(opts) {
69
77
  const { autoMode, ask, req } = opts;
@@ -77,8 +85,10 @@ export async function judgeInheritedClassifier(opts) {
77
85
  autoMode.denialTracking?.recordAllow();
78
86
  return { kind: "allow" };
79
87
  }
80
- if (verdict.kind !== "block")
81
- return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false, origin };
88
+ if (verdict.kind !== "block") {
89
+ const resolved = verdict.kind === "unavailable" ? { ...ask, classifierUnavailable: { cause: verdict.cause } } : ask;
90
+ return { kind: "resolve", ask: resolved, fallback: ask.denialLimitFallback, mintedHere: false, origin };
91
+ }
82
92
  const reason = verdict.reason ? inlineUntrusted(verdict.reason) : "";
83
93
  const category = verdict.category ? inlineUntrusted(verdict.category) : "";
84
94
  const tracked = autoMode.denialTracking?.recordBlock();
@@ -218,6 +218,11 @@ export interface GatePass {
218
218
  * touch. Recorded by the gate rather than read off `decisionReason` because that member is a policy's to
219
219
  * compose (a policy stamping `"safety"` on its own ask must read as `policy`). Writer: the fold. */
220
220
  tightenedBy: AskOriginFacts["tightened"];
221
+ /** borrowed-mutable — the reversibility probe was consulted on this pass and DID NOT ANSWER (it threw or
222
+ * timed out). The out-of-root mandate has no other source than the probe's answer, so an unanswered probe
223
+ * leaves the call's boundary UNKNOWN: the allow layer (a stored allow rule, the read-only reader) must
224
+ * not clear the surviving ask — a person confirms it. Writer: the fold. Reader: the lanes' mandate. */
225
+ probeUnanswered?: true;
221
226
  /** borrowed-mutable — the rewrite remembered BEFORE a hook-ask promotion / tighten, re-applied on the
222
227
  * approved path (a redaction is not cancelled because a person confirmed). Writer: the fold. Readers: the
223
228
  * fold's probe and write-protection judges, the lanes, the allow exit. */
@@ -107,7 +107,8 @@ export declare function pathRuleLaneAnswer(table: readonly PersistedRule[], req:
107
107
  root: string | undefined;
108
108
  sessionId: string | undefined;
109
109
  liveCwd: string | undefined;
110
- }): PersistedRuleHit | undefined;
110
+ home: string | undefined;
111
+ }): PersistedRuleHit | PersistedRuleUnreadable | undefined;
111
112
  /** The two lanes prepare hands the gate. Each is `undefined` when its partition is not configured, so
112
113
  * a deployment without one keeps a decision path byte-identical to a build without the feature. */
113
114
  export interface PermissionRuleLanes {
@@ -137,6 +138,11 @@ export declare function createPermissionRuleLanes(cfg: {
137
138
  /** The LIVE tracked working directory (adversarial-review P1): the base a relative cd resolves against
138
139
  * after an earlier observable `cd`; read per call. */
139
140
  liveCwd: () => string | undefined;
141
+ /** #644 — the EXECUTION ENVIRONMENT's home directory (`ExecutionEnv.homeDir`, adapter-declared), the base a
142
+ * `~/` path rule resolves against. `undefined` = the adapter declares none: a `~/` deny/ask row then reads
143
+ * UNREADABLE for every call (a fail-closed ask), never the engine process's home. A declared value that is
144
+ * not an absolute path is refused HERE, loudly (a bad declaration is a wiring defect, not an absence). */
145
+ home: string | undefined;
140
146
  /** True iff the reserved question-tool NAME resolves to the ENGINE's own content-ask tool on this leg. */
141
147
  questionToolMounted: boolean;
142
148
  questionToolName: string;
@@ -1,5 +1,6 @@
1
1
  import { persistedRuleMandateOf } from "../hooks.js";
2
2
  import { adjudicatePersistedPathRules, adjudicatePersistedRules, ruleToolGrammarOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
3
+ import { isAbsolutePathForm } from "../../tools/fs/safety.js";
3
4
  import { effectivePathTargetOf } from "../effective-path-target.js";
4
5
  import { declaredPathTargetOf, protectivePathTargetOf } from "../tool-registry.js";
5
6
  import { orgRuleVerdictFor } from "../permission-rule-org.js";
@@ -31,15 +32,20 @@ export function persistedRuleAnswerOf(verdict) {
31
32
  return persistedRuleHitOf(verdict);
32
33
  }
33
34
  export function pathRuleLaneAnswer(table, req, ctx) {
34
- const bases = { ...(ctx.root !== undefined ? { root: ctx.root } : {}), ...(ctx.liveCwd !== undefined ? { cwd: ctx.liveCwd } : {}) };
35
+ const bases = { ...(ctx.root !== undefined ? { root: ctx.root } : {}), ...(ctx.liveCwd !== undefined ? { cwd: ctx.liveCwd } : {}), ...(ctx.home !== undefined ? { home: ctx.home } : {}) };
35
36
  const tighten = effectivePathTargetOf(req, protectivePathTargetOf(req), bases);
36
37
  const allow = effectivePathTargetOf(req, declaredPathTargetOf(req), bases);
37
38
  if (tighten === undefined && allow === undefined)
38
39
  return undefined;
39
- return persistedRuleHitOf(adjudicatePersistedPathRules(table, { tool: req.toolName, cwd: ctx.root, sessionId: ctx.sessionId }, { ...(tighten !== undefined ? { tighten } : {}), ...(allow !== undefined ? { allow } : {}) }, bases));
40
+ return persistedRuleAnswerOf(adjudicatePersistedPathRules(table, { tool: req.toolName, cwd: ctx.root, sessionId: ctx.sessionId }, { ...(tighten !== undefined ? { tighten } : {}), ...(allow !== undefined ? { allow } : {}) }, bases));
40
41
  }
41
42
  export function createPermissionRuleLanes(cfg) {
42
43
  const provider = cfg.provider;
44
+ if (cfg.home !== undefined && !isAbsolutePathForm(cfg.home)) {
45
+ const e = new Error(`the execution environment declares homeDir ${JSON.stringify(cfg.home)}, which is not an absolute path — a \`~/\` permission rule could not be resolved against it; declare an absolute home directory or none`);
46
+ e.code = "config.execution_env_home_dir_invalid";
47
+ throw e;
48
+ }
43
49
  if (cfg.localOwnerDeclared) {
44
50
  if (provider === undefined || !provider.partitions.durable) {
45
51
  throw new Error("RunnerDeps.localOwnerRules is declared but no durable permission-rule partition is wired — there is no bucket for the local owner to hold rules in; refusing rather than running as if the declaration were absent");
@@ -127,7 +133,7 @@ export function createPermissionRuleLanes(cfg) {
127
133
  const table = view.rules;
128
134
  const liveCwd = cfg.liveCwd();
129
135
  if (grammar === "path") {
130
- return pathRuleLaneAnswer(table, req, { root: cfg.root, sessionId: cfg.sessionId, liveCwd });
136
+ return pathRuleLaneAnswer(table, req, { root: cfg.root, sessionId: cfg.sessionId, liveCwd, home: cfg.home });
131
137
  }
132
138
  const command = req.args?.command;
133
139
  if (typeof command !== "string")
@@ -49,7 +49,7 @@ export interface PrepareCapsAndWorkflowInput {
49
49
  deps: RunnerDeps;
50
50
  /** borrowed-readonly — the trusted spawn-side channel; the Workflow mount forwards `rootSessionId`, `placementRoot`,
51
51
  * `onTaskNotification`, `workflowDepth`, and the capture-floor getter reads `memoryCaptureAncestors`. */
52
- internals: Pick<RunInternals, "rootSessionId" | "placementRoot" | "onTaskNotification" | "workflowDepth" | "memoryCaptureAncestors"> | undefined;
52
+ internals: Pick<RunInternals, "rootSessionId" | "placementRoot" | "onTaskNotification" | "workflowDepth" | "memoryCaptureAncestors" | "workflowParkedResume" | "autoModeBreakerLedger"> | undefined;
53
53
  /** borrowed-readonly — the acquired session; the classifier leg rebuilds its transcript window from `buildContext`. */
54
54
  session: Pick<StoredSession, "buildContext">;
55
55
  /** borrowed-readonly — the acquired session id (operator-line context, the refusal codes' phase record). */