@sema-agent/core 7.23.3 → 7.23.5

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 (52) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/agents/list-agents-tool.d.ts +5 -1
  3. package/dist/agents/list-agents-tool.js +14 -2
  4. package/dist/brain/anthropic.js +3 -2
  5. package/dist/brain/circuit-breaker.js +3 -3
  6. package/dist/brain/degrading.js +3 -5
  7. package/dist/brain/errors.d.ts +35 -18
  8. package/dist/brain/errors.js +49 -31
  9. package/dist/brain/failover.js +2 -3
  10. package/dist/brain/open-responses.js +6 -5
  11. package/dist/brain/openai.js +6 -5
  12. package/dist/brain/route-adjudicator.d.ts +26 -4
  13. package/dist/brain/route-adjudicator.js +26 -0
  14. package/dist/brain/stream-engine.js +8 -2
  15. package/dist/core/checkpoint-store.d.ts +4 -6
  16. package/dist/core/checkpoint-store.js +2 -2
  17. package/dist/core/permission-rule-model.js +8 -5
  18. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -0
  19. package/dist/core/runner/prepare-delegation-surface.js +1 -0
  20. package/dist/core/runner/prepare-hands-readface.d.ts +4 -1
  21. package/dist/core/runner/prepare-hands-readface.js +7 -7
  22. package/dist/core/runner/prepare-task.js +2 -2
  23. package/dist/core/runner/run-leg.js +2 -13
  24. package/dist/core/task-spec.d.ts +2 -1
  25. package/dist/core/thrown-value.d.ts +30 -0
  26. package/dist/core/thrown-value.js +23 -1
  27. package/dist/core/tool-catalog-entries.js +1 -1
  28. package/dist/core/tool-model-gate.js +3 -15
  29. package/dist/core/tools.js +7 -7
  30. package/dist/core/untrusted-text.d.ts +6 -0
  31. package/dist/core/untrusted-text.js +1 -0
  32. package/dist/core/workflow-run-store-contract.js +13 -0
  33. package/dist/core/workflow-run-store.d.ts +15 -0
  34. package/dist/core/workflow-run-store.js +1 -0
  35. package/dist/engine/loop/agent-loop.js +10 -24
  36. package/dist/tools/fs/bash-program-position.d.ts +23 -0
  37. package/dist/tools/fs/bash-program-position.js +11 -0
  38. package/dist/tools/fs/bash-readonly-classifier.d.ts +64 -42
  39. package/dist/tools/fs/bash-readonly-classifier.js +33 -10
  40. package/dist/tools/fs/fs-bash.d.ts +41 -3
  41. package/dist/tools/fs/fs-bash.js +152 -58
  42. package/dist/tools/fs/index.d.ts +2 -1
  43. package/dist/tools/fs/index.js +1 -0
  44. package/dist/tools/fs/read-deny.d.ts +28 -0
  45. package/dist/tools/fs/read-deny.js +11 -0
  46. package/dist/tools/fs/safety.d.ts +1 -1
  47. package/dist/tools/fs/safety.js +2 -1
  48. package/dist/tools/monitor.d.ts +5 -0
  49. package/dist/tools/monitor.js +7 -0
  50. package/package.json +1 -1
  51. package/wire-changes/core@7.23.4.json +58 -0
  52. package/wire-changes/core@7.23.5.json +133 -0
@@ -159,6 +159,17 @@ export function argvReentryUnjudged(pos, acceptedTargets) {
159
159
  return runs === undefined || !acceptedTargets.has(runs);
160
160
  }
161
161
  export const SHELL_SCAN_DECLINES = ["unreadable_program_position", "stream_fed_operands"];
162
+ export const BOUNDARY_SEAT_READS_DECLINE = {
163
+ unreadable_program_position: true,
164
+ stream_fed_operands: true,
165
+ };
166
+ export const CLASSIFY_SEAT_READS_DECLINE = {
167
+ unreadable_program_position: false,
168
+ stream_fed_operands: false,
169
+ };
170
+ export function seatDeclineEvidence(table) {
171
+ return new Set(SHELL_SCAN_DECLINES.filter((decline) => table[decline] === true));
172
+ }
162
173
  export function programPositionRunsShell(pos, shellProgram) {
163
174
  if (pos.refusal !== undefined)
164
175
  return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell === true;
@@ -76,11 +76,13 @@ export interface LeadingCommandNameOptions {
76
76
  * Read a shell operator that sits inside a QUOTED region as ordinary argument TEXT rather than as an
77
77
  * operator, so `grep -E "a|b" f.log` parses as the single simple command it is.
78
78
  *
79
- * OPT-IN, and used by ONE caller: the compound classify face ({@link classifyCompoundReadonlyDetailed}),
80
- * which has already rejected the whole command for backslashes / `$` / backticks / subshells / line
81
- * breaks and so is inside {@link quoteMask}'s exactness envelope. The declaration and rule faces do
82
- * NOT set it — their contract is "a single simple command with no operator characters at all", which
83
- * is a spelling rule about what a person can read at a glance, not a claim about bash's grammar.
79
+ * OPT-IN, and the two callers that set it share one premise: the whole command has ALREADY been refused
80
+ * for backslashes / an expanding `$` / backticks / subshells / line breaks, so they are inside
81
+ * {@link quoteMask}'s exactness envelope — the compound classify face
82
+ * ({@link classifyCompoundReadonlyDetailed}) and the persisted permission-RULE lane (`RULE_LANE_FLOOR`),
83
+ * whose segments come out of that same gate ({@link splitShellCompoundSegments}). The `bash_readonly`
84
+ * DECLARATION face does NOT set it: its contract is "a single simple command with no operator characters
85
+ * at all", and it runs on raw tool input with no whole-string gate in front of it.
84
86
  *
85
87
  * Fail-closed on unbalanced quoting: with a region left open, the mask is not trusted and every
86
88
  * operator character counts, exactly as without this option.
@@ -272,28 +274,24 @@ export interface BashReadonlyRootBoundary {
272
274
  * and demotes (with no out-of-root signal — the classifier does not know where it points, which is a
273
275
  * different verdict from knowing it points outside). */
274
276
  homeDir?: string;
275
- /** The sensitive-path read deny judge over LEXICALLY RESOLVED operands: a hit
276
- * demotes the command (ask, never auto-allow), independently of the roots — in-root operands are
277
- * judged too. Returns the matched pattern, or null. TWO named residuals, both inherited from this
278
- * classifier's declared purity (synchronous, zero I/O — the same scope stated for the
279
- * containment half): ① operand TARGET matching only — no ancestor intersection: the judge sees
280
- * the operand's own resolved spelling, never its subtree. The RECURSIVE-reach half of that
281
- * residual has since been narrowed: a listed recursive/expanding verb's path
282
- * operand under this wired seat rides {@link CompoundReadonlyVerdict.recursiveReadPaths}
283
- * (⊂ undecidedPaths), so `grep -r x ~/` no longer auto-allows — it demotes to ask through the
284
- * undecided contract. What REMAINS of ① is the form table's stated open set (see the KNOWN OPEN
285
- * SET note at {@link RECURSIVE_READ_FORMS}: a traversal verb absent there that a deployment
286
- * allowlists classifies by its ordinary form and can auto-run a traversal unjudged);
287
- * ② LEXICAL only HERE — an in-root symlink whose target is a
288
- * guarded path reads as its innocent spelling at this seat, exactly as it does for the containment half,
289
- * because the classify auto-allow lane has no I/O seat by contract. The CANONICAL view is judged by the
290
- * full shell's execute-time recheck, which asks THIS judge (the same function object, forwarded) where
291
- * each candidate really lands — a `checkedPaths` literal and a `undecidedPaths` pattern's expansions
292
- * alike. The `bash_readonly` face wires no deny seat at all, so its own recheck carries nothing. The structured read faces judge BOTH
293
- * views (canonical + lexical); what this seat is and is not is stated once, at
294
- * `shell.read_boundary.friction_floor`. The `bash_readonly` face deliberately does NOT wire this seat (its
295
- * roots containment + command allowlist double gate is the deployment's own read-safety
296
- * declaration — v1 ruling). */
277
+ /** The sensitive-path read deny judge over LEXICALLY RESOLVED operands: a hit demotes the command (ask,
278
+ * never auto-allow), independently of the roots — in-root operands are judged too. Returns the matched
279
+ * pattern, or null. TWO named residuals, both inherited from this classifier's declared purity
280
+ * (synchronous, zero I/O — the same scope stated for the containment half): ① operand TARGET matching
281
+ * only, no ancestor intersection: the judge sees the operand's own resolved spelling, never its subtree.
282
+ * The RECURSIVE-reach half of ① is narrowed — a listed recursive verb's path operand under this wired
283
+ * seat rides {@link CompoundReadonlyVerdict.recursiveReadPaths} (⊂ undecidedPaths), so `grep -r x ~/` no
284
+ * longer auto-allows; what REMAINS is the form table's stated open set (the KNOWN OPEN SET note at
285
+ * {@link RECURSIVE_READ_FORMS}: a traversal verb absent there that a deployment allowlists classifies by
286
+ * its ordinary form and can auto-run a traversal unjudged). ② LEXICAL only HERE — an in-root symlink
287
+ * whose target is guarded reads as its innocent spelling at this seat, as it does for the containment
288
+ * half, because the classify auto-allow lane has no I/O seat by contract; the CANONICAL view is judged by
289
+ * an execute-time recheck that asks THIS judge (the same function object, forwarded) where each candidate
290
+ * really lands, on a `checkedPaths` literal and a pattern's expansions alike.
291
+ * EVERY read face wires this one seat, the `bash_readonly` shell included, at BOTH of its judging
292
+ * moments — roots answer WHERE a command may read and the allowlist WHICH verb runs, neither answers
293
+ * WHICH FILES are off the table. What this seat is and is not is stated once, at
294
+ * `shell.read_boundary.friction_floor`. */
297
295
  denyMatch?: (resolvedPath: string) => string | null;
298
296
  /** What the wired {@link denyMatch} judge would apply — the ACTIVE deny patterns. Read for ONE question:
299
297
  * is there anything for a traversal to defeat? EMPTY ⇒ the judge refuses nothing, so a walk under it
@@ -333,6 +331,10 @@ export interface CompoundReadonlyVerdict {
333
331
  * reads it to mint `mandated` — a stored allow rule or the read-only shell arm may retire a classify-
334
332
  * tier question, never one the read face itself asked. Structural sibling of `outOfRootRead`. */
335
333
  readDenied?: true;
334
+ /** The matched operand (resolved spelling) and the pattern — present iff {@link readDenied} (the
335
+ * `outOfRootRead`/`outOfRootPaths` idiom), so a refusal CARD names the target without parsing prose. */
336
+ readDeniedPath?: string;
337
+ readDeniedPattern?: string;
336
338
  /**
337
339
  * The paths this scan resolved INSIDE the roots (de-duplicated, first-seen order), i.e. the
338
340
  * candidates a caller holding a filesystem should re-check with symlinks resolved. Absent when there
@@ -419,20 +421,19 @@ export interface CompoundReadonlyVerdict {
419
421
  /**
420
422
  * Operands of a RECURSIVE/EXPANDING read form (`grep -r`, `ls -R`, `du`, … — see
421
423
  * {@link RECURSIVE_READ_FORMS}) judged with a {@link BashReadonlyRootBoundary.denyMatch} seat wired.
422
- * The deny judge sees only the operand's own resolved spelling, but a recursive verb reads the
423
- * operand's whole SUBTREE — `grep -r x /home/user` touches `/home/user/.ssh/*` while the judged
424
- * spelling `/home/user` matches no deny pattern. The traversal's reach set is therefore not covered
425
- * by the lexical check at all, and with zero I/O "provably not a directory" does not exist — so
426
- * every such operand is UNDECIDED (no directory guessing, no bounded pre-check: both would be a
427
- * false "resolved, inside" of exactly the kind {@link undecidedPaths} exists to prevent).
424
+ * The judge sees only the operand's own resolved spelling, but a recursive verb reads the operand's whole
425
+ * SUBTREE — `grep -r x /home/user` touches `/home/user/.ssh/*` while the judged spelling matches nothing.
426
+ * The traversal's reach set is therefore not covered by the lexical check at all, and with zero I/O
427
+ * "provably not a directory" does not exist — so every such operand is UNDECIDED (no directory guessing,
428
+ * no bounded pre-check: both would be a false "resolved, inside" of exactly the kind
429
+ * {@link undecidedPaths} exists to prevent).
428
430
  *
429
- * Subset of {@link undecidedPaths} (same consumer contract: ask, never auto-allow); carried
430
- * separately so a consumer minting prose can name the recursive-reach cause rather than the glob
431
- * one. Minted ONLY when `denyMatch` is wired: without a deny judge there is nothing the traversal
432
- * bypasses — the containment half already judges the operand itself, and its lexical residuals are
433
- * recorded on {@link checkedPaths}. The `bash_readonly` face never wires `denyMatch` (v1 ruling,
434
- * see that seat's note), so this field never appears there and its expand-and-verify execution
435
- * path is unchanged.
431
+ * Subset of {@link undecidedPaths} (same consumer contract: ask, never auto-allow); carried separately so
432
+ * a consumer minting prose can name the recursive-reach cause rather than the glob one. Minted ONLY when
433
+ * `denyMatch` is wired: without a judge there is nothing the traversal bypasses. EVERY face wiring the
434
+ * seat sees this field, the `bash_readonly` shell included, and answers it by its OWN rule for a demotion
435
+ * — that face has no approval channel, so what it cannot establish it REFUSES (an unbounded traversal is
436
+ * the same "the read set is not in the text" finding as a pattern the expander cannot enumerate).
436
437
  */
437
438
  recursiveReadPaths?: readonly string[];
438
439
  /**
@@ -618,8 +619,29 @@ export interface OutOfRootReadGateVerdict {
618
619
  readonly paths: readonly string[];
619
620
  /** Did the deployment's DENY judge speak about an operand this command names? The other half of the same
620
621
  * question, over the very same readers, in a second pass WITH the deny judge — which is what lets the
621
- * containment answer above stay deny-free and keep its `paths` contract. Absent ⇔ it said nothing. */
622
- readonly denied?: true;
622
+ * containment answer above stay deny-free and keep its `paths` contract. Absent ⇔ it said nothing.
623
+ *
624
+ * It carries the FINDING, not a boolean: this is the arm a command the grammar gate refused WHOLE
625
+ * (`cat .env > out.txt`, a subshell, a substitution) reaches, and there the compound verdict carries no
626
+ * deny fields at all — so a caller disclosing the hit has nowhere else to read the path and the pattern
627
+ * from, and said nothing. The second pass already computed both; throwing them away was the whole bug. */
628
+ readonly denied?: {
629
+ readonly path: string;
630
+ readonly pattern: string;
631
+ };
632
+ /** The reader operands this pass RESOLVED and placed inside the roots, plus the expansions it parsed but
633
+ * cannot enumerate — the candidates an execute-time recheck must canonicalize.
634
+ *
635
+ * Published because this pass reads SHAPES the compound walk hands nothing out for: a launcher whose
636
+ * program it cannot name (`env -- cat f`, `sudo -u root cat f`), a stream-fed one (`… | xargs cat f`).
637
+ * There the compound verdict carries no candidates at all, so the recheck had nothing to resolve and an
638
+ * in-root symlink to a deny-listed target RAN after an approval — while the same read spelled without
639
+ * the launcher was refused. Same operands, same judge, one more pass allowed to hand them over.
640
+ *
641
+ * NOT the out-of-root ones: those ride {@link paths}, they are what the boundary RAISED, and a leg that
642
+ * re-refused them would be a second adjudicator of a decision already made. */
643
+ readonly checked: readonly string[];
644
+ readonly undecided: readonly string[];
623
645
  }
624
646
  /** Test seam for the lockstep pin that the launcher table (the ONE launcher truth, in the program-position
625
647
  * reader's module) stays a SUBSET of the rule lane's curated `BARE_INTERPRETER_NAMES`. Exported for that one
@@ -1,3 +1,4 @@
1
+ import { sensitivePathDenyList } from "./read-deny.js";
1
2
  import { hasWin32AliasedComponent, isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, win32NamespaceScreen, pathFamilyOf, withinAnyRoot } from "./safety.js";
2
3
  import { lexShellText, literalTildeSpelling, quoteMask, substitutionsInHeredocBody, SUBSTITUTION_PLACEHOLDER, wordsOf, } from "./bash-lexer.js";
3
4
  import { ASSIGNMENT_WORD, COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programPositionRunsShell, shellScanDeclineOf, programBasename, programPositionRefusalSentence, readProgramPosition, } from "./bash-program-position.js";
@@ -19,11 +20,16 @@ function hasBareShellOperator(s, quotedOperatorsAreText) {
19
20
  if (!mask.balanced)
20
21
  return SHELL_OPERATORS.test(s);
21
22
  for (let i = 0; i < s.length; i++) {
22
- if (!mask.quoted[i] && SHELL_OPERATORS.test(s[i]))
23
+ const ch = s[i];
24
+ if (!SHELL_OPERATORS.test(ch))
25
+ continue;
26
+ const literal = SHELL_EXPANDS_IN_DOUBLE_QUOTES.test(ch) ? mask.openedBy[i] === "'" : mask.quoted[i];
27
+ if (!literal)
23
28
  return true;
24
29
  }
25
30
  return false;
26
31
  }
32
+ const SHELL_EXPANDS_IN_DOUBLE_QUOTES = /[$`]/;
27
33
  export function parseLeadingCommandName(command, options) {
28
34
  const trimmed = command.trim();
29
35
  if (!trimmed)
@@ -2169,8 +2175,10 @@ function evaluateReadBoundary(foldedSegments, boundary, mintGrantCandidate, reac
2169
2175
  const denied = boundary.denyMatch?.(finding.path);
2170
2176
  if (denied != null) {
2171
2177
  return {
2172
- reason: `a command operand resolves to "${finding.path}", which matches the sensitive-path read deny list (pattern "${denied}") ${NOT_AUTO_ALLOWED}`,
2178
+ reason: `a command operand resolves to "${finding.path}", which matches ${sensitivePathDenyList(denied)} ${NOT_AUTO_ALLOWED}`,
2173
2179
  readDenied: true,
2180
+ readDeniedPath: finding.path,
2181
+ readDeniedPattern: denied,
2174
2182
  };
2175
2183
  }
2176
2184
  if (finding.kind === "inside") {
@@ -2299,7 +2307,7 @@ export function classifySimpleCommandReadBoundary(command, boundary) {
2299
2307
  }
2300
2308
  return evaluateReadBoundary([toks], boundary, true);
2301
2309
  }
2302
- const NOT_GATED = { gated: false, paths: [] };
2310
+ const NOT_GATED = { gated: false, paths: [], checked: [], undecided: [] };
2303
2311
  const UNREADABLE_WHITELIST_WORD = /[$`()*?[\]{}]/;
2304
2312
  const UNMODELLED_BASE_MOVERS = new Set(["pushd", "popd"]);
2305
2313
  export const COMMAND_LAUNCHERS_FOR_TEST = COMMAND_LAUNCHERS;
@@ -2357,18 +2365,33 @@ export function classifyOutOfRootReadGate(command, allow, boundary) {
2357
2365
  if (readers.length === 0)
2358
2366
  return NOT_GATED;
2359
2367
  if (baseMoved)
2360
- return { gated: true, paths: [] };
2368
+ return { gated: true, paths: [], checked: [], undecided: [] };
2361
2369
  const verdict = evaluateReadBoundary(readers, containmentOnly, false, readerReach);
2362
- const denied = boundary.denyMatch === undefined ? false : evaluateReadBoundary(readers, boundary, false, readerReach).readDenied === true;
2363
- if (verdict.outOfRootRead === true)
2364
- return { gated: true, paths: verdict.outOfRootPaths ?? [], ...(denied ? { denied: true } : {}) };
2365
- return denied ? { gated: false, paths: [], denied: true } : NOT_GATED;
2370
+ const denyPass = boundary.denyMatch === undefined ? undefined : evaluateReadBoundary(readers, boundary, false, readerReach);
2371
+ const denied = denyPass?.readDenied === true && denyPass.readDeniedPath !== undefined && denyPass.readDeniedPattern !== undefined
2372
+ ? { path: denyPass.readDeniedPath, pattern: denyPass.readDeniedPattern }
2373
+ : undefined;
2374
+ const resolvedOperands = {
2375
+ checked: [...(verdict.checkedPaths ?? []), ...(verdict.checkedSpellings ?? [])],
2376
+ undecided: [...(verdict.undecidedPaths ?? []), ...(verdict.undecidedSpellings ?? [])],
2377
+ };
2378
+ if (verdict.outOfRootRead === true) {
2379
+ return { gated: true, paths: verdict.outOfRootPaths ?? [], ...resolvedOperands, ...(denied !== undefined ? { denied } : {}) };
2380
+ }
2381
+ if (denied !== undefined)
2382
+ return { gated: false, paths: [], ...resolvedOperands, denied };
2383
+ return resolvedOperands.checked.length === 0 && resolvedOperands.undecided.length === 0 ? NOT_GATED : { gated: false, paths: [], ...resolvedOperands };
2366
2384
  };
2367
2385
  const direct = judge(command);
2368
- if (direct.gated || direct.denied === true)
2386
+ if (direct.gated || direct.denied !== undefined)
2369
2387
  return direct;
2370
2388
  const shape = POLL_LOOP_SHAPE.exec(command.trim());
2371
- return shape !== null ? judge(shape[3]) : NOT_GATED;
2389
+ if (shape === null)
2390
+ return direct;
2391
+ const loop = judge(shape[3]);
2392
+ return loop.gated || loop.denied !== undefined
2393
+ ? loop
2394
+ : { ...loop, checked: [...direct.checked, ...loop.checked], undecided: [...direct.undecided, ...loop.undecided] };
2372
2395
  }
2373
2396
  export function classifyCompoundReadonly(command, allow, boundary) {
2374
2397
  return classifyCompoundReadonlyDetailed(command, allow, boundary).reason;
@@ -4,6 +4,7 @@ import { type CwdRef } from "./fs-shared.js";
4
4
  import type { ReversibilityVerdict } from "../../core/types.js";
5
5
  import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
6
6
  import { type ShellScanDecline } from "./bash-program-position.js";
7
+ import { type ReadDenyMatcher } from "./read-deny.js";
7
8
  /**
8
9
  * A parsed-command classifier for the `bash` tool, exposed as a
9
10
  * `ToolSpec.reversibilityProbe`. A full shell is treated as egress+irreversible by DEFAULT; this probe is the
@@ -65,6 +66,10 @@ export declare function bashReversibilityProbe(allow?: readonly string[], bounda
65
66
  * ONE rule, TWO TABLES: which {@link ShellScanDecline} a SEAT reads as read evidence. A decline names no word,
66
67
  * so it is never the boundary DECLARING anything about a path.
67
68
  *
69
+ * BOTH tables are ROW-PER-MEMBER objects behind a compile-time fence ({@link BOUNDARY_SEAT_READS_DECLINE} /
70
+ * {@link CLASSIFY_SEAT_READS_DECLINE}); the sets below are DERIVED from those rows and the ARGUMENT for the
71
+ * rows is this block. "Empty by the argument" is a row saying `false`, not a literal nobody can judge.
72
+ *
68
73
  * The READ-BOUNDARY seat is what the default/off doctrine and a bypass mode mount ALONE: there "not mandated"
69
74
  * is VOUCHED, so a decline must count as evidence or a read nobody judged runs silently (`<out-of-root> |
70
75
  * xargs cat` at zero asks — a closed P0). It keeps both. The CLASSIFY seat composes the same boundary with the
@@ -86,8 +91,13 @@ export declare function bashReversibilityProbe(allow?: readonly string[], bounda
86
91
  * DID read naming a path outside the roots still gates the call ({@link classifyOutOfRootReadGate}).
87
92
  */
88
93
  export declare const BOUNDARY_SEAT_DECLINE_EVIDENCE: ReadonlySet<ShellScanDecline>;
89
- /** The classify doctrine's table — see above; empty by the argument. */
94
+ /** The classify doctrine's table in the shape its readers take — see above; empty because every row says `false`. */
90
95
  export declare const CLASSIFY_SEAT_DECLINE_EVIDENCE: ReadonlySet<ShellScanDecline>;
96
+ export declare function fitMiddle(s: string, budget: number): string;
97
+ /** `s` shortened to `budget` by removing its MIDDLE, marked — cut through the repo's surrogate-safe
98
+ * primitive, never a raw `slice` (a raw cut leaves half an astral code point on a card and in a durable
99
+ * row). Under a dozen characters the HEAD is taken. */
100
+ export declare function elideMiddle(s: string, budget: number): string;
91
101
  /**
92
102
  * The READ-BOUNDARY seat of the full shell: a `ToolSpec.reversibilityProbe` that answers exactly ONE
93
103
  * question — does the deployment's read boundary object to where this command reads? — and nothing about
@@ -291,6 +301,26 @@ export declare function createBashTool(env: ExecutionEnv, rootCanonical: string,
291
301
  * Absent ⇒ historic bare tag. */
292
302
  reminderMark?: string;
293
303
  }): AgentTool;
304
+ type ShellCanonicalRefusal = {
305
+ content: string;
306
+ isError: true;
307
+ details?: unknown;
308
+ };
309
+ /** What a face contributes to the shared refusal cards: the TOOL refusing, and the three prose slots. */
310
+ export interface ShellFaceProse {
311
+ readonly tool: string;
312
+ readonly subject: string;
313
+ readonly verdictPhrase: string;
314
+ readonly note: string;
315
+ }
316
+ export type ShellCanonicalRecheck = (command: string, signal?: AbortSignal) => Promise<ShellCanonicalRefusal | undefined>;
317
+ /**
318
+ * THE execute-time canonical recheck, mounted by every face that spawns a shell command — the full `Bash`
319
+ * tool and `Monitor`, whose approval-time seats judge the WRITTEN spelling while this re-judges where the
320
+ * kernel takes those words. The SAME leg, not a second one; the contract below is the leg's own. */
321
+ export declare function createShellCanonicalRecheck(env: ExecutionEnv, readBoundary: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined) | undefined, face?: ShellFaceProse): ShellCanonicalRecheck;
322
+ /** The `Monitor` face's prose — the full shell's with its own tool name: one leg, one finding, two tools. */
323
+ export declare const MONITOR_ESCAPE_PROSE: ShellFaceProse;
294
324
  /**
295
325
  * `bash_readonly` (effect:read) — a restricted shell for the verifier read-only boundary: a single
296
326
  * allowlisted, bare command with no shell operators. effect:read lets it survive the verifier's
@@ -306,12 +336,17 @@ export declare function createBashTool(env: ExecutionEnv, rootCanonical: string,
306
336
  * read-only band), so an out-of-root read here is not something to escalate, it is outside what the tool
307
337
  * is. It is refused, fail-closed, before anything executes — including the conservative direction, where
308
338
  * the operand cannot be resolved statically (there is nothing to degrade to).
339
+ *
340
+ * The boundary has TWO halves and this face wires both: roots answer WHERE a command may read and the
341
+ * allowlist WHICH verb runs, neither answers WHICH FILES are off the table (a deny row holds INSIDE the
342
+ * roots). The judge is the structured read faces' own, asked at both judging moments; every finding lands
343
+ * on the standing rule above — a demotion here is a refusal.
309
344
  */
310
345
  export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, opts?: {
311
346
  /** Same timeout-cap overrides the full `bash` leg takes — see
312
347
  * {@link import("./index.js").HandsToolkitOptions.bashDefaultTimeoutMs} /
313
- * {@link import("./index.js").HandsToolkitOptions.bashMaxTimeoutMs} (类修完整域: both foreground
314
- * legs share runShell's clamp, so both must share the configurable resolution). */
348
+ * {@link import("./index.js").HandsToolkitOptions.bashMaxTimeoutMs} — both foreground legs share
349
+ * runShell's clamp, so both must share the configurable resolution. */
315
350
  bashDefaultTimeoutMs?: number;
316
351
  bashMaxTimeoutMs?: number;
317
352
  /** Structured sink for the timeout-cap discard announcements — same seat the full `bash` leg
@@ -321,6 +356,9 @@ export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical:
321
356
  * canonical). A read this deployment already sanctions for Read/Grep/Glob is equally sanctioned
322
357
  * here — the two faces must not disagree about which directories exist for this session. */
323
358
  additionalRoots?: readonly string[];
359
+ /** The deployment's compiled read deny set — the SAME matcher object the structured read faces of this
360
+ * mount hold (one compile, one table). Absent ⇒ no deny judge, and both judging moments are unchanged. */
361
+ readDeny?: ReadDenyMatcher;
324
362
  /** The run's tool-progress sink — the SAME seat the full shell leg takes, for the same reason its
325
363
  * two neighbours above are shared: both foreground legs run through `runShell`, so a long command
326
364
  * must breathe on both or a person watching one of them is told nothing. This face has no detach