@sema-agent/core 7.45.0 → 7.46.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 (127) hide show
  1. package/CHANGELOG.md +246 -1
  2. package/dist/agents/extra-tool-names.d.ts +65 -0
  3. package/dist/agents/extra-tool-names.js +124 -0
  4. package/dist/agents/subagent.d.ts +17 -5
  5. package/dist/agents/subagent.js +33 -25
  6. package/dist/agents/tool-filter.d.ts +17 -1
  7. package/dist/agents/tool-filter.js +7 -0
  8. package/dist/core/agent-config-dir.d.ts +20 -0
  9. package/dist/core/agent-config-dir.js +49 -0
  10. package/dist/core/allow-entry.d.ts +5 -3
  11. package/dist/core/allow-entry.js +3 -1
  12. package/dist/core/ask-origin.js +1 -1
  13. package/dist/core/checkpoint-store.d.ts +39 -2
  14. package/dist/core/effective-path-target.d.ts +136 -4
  15. package/dist/core/effective-path-target.js +110 -2
  16. package/dist/core/engine-notice.d.ts +5 -0
  17. package/dist/core/gate-fold.js +3 -0
  18. package/dist/core/gate-lanes.js +9 -3
  19. package/dist/core/governance-codes.d.ts +3 -1
  20. package/dist/core/governance-codes.js +3 -0
  21. package/dist/core/hooks.d.ts +3 -0
  22. package/dist/core/path-rule-binding.d.ts +65 -0
  23. package/dist/core/path-rule-binding.js +99 -0
  24. package/dist/core/permission-rule-model.d.ts +47 -18
  25. package/dist/core/permission-rule-model.js +38 -42
  26. package/dist/core/permission-rule-provider.d.ts +7 -0
  27. package/dist/core/permission-rule-provider.js +10 -3
  28. package/dist/core/permission-rule-store.d.ts +8 -0
  29. package/dist/core/permission-rule-store.js +6 -2
  30. package/dist/core/permission-rule-sync.js +2 -2
  31. package/dist/core/permission-rules.d.ts +22 -40
  32. package/dist/core/permission-rules.js +50 -43
  33. package/dist/core/runner/child-tree.d.ts +97 -0
  34. package/dist/core/runner/child-tree.js +104 -0
  35. package/dist/core/runner/construction-doors.d.ts +5 -0
  36. package/dist/core/runner/construction-doors.js +6 -0
  37. package/dist/core/runner/contracts.d.ts +46 -0
  38. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  39. package/dist/core/runner/permission-rule-lanes.js +18 -12
  40. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  41. package/dist/core/runner/persisted-path-rows.js +31 -12
  42. package/dist/core/runner/prepare-ask-lane.d.ts +4 -0
  43. package/dist/core/runner/prepare-ask-lane.js +4 -2
  44. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  45. package/dist/core/runner/prepare-execution-env.js +10 -3
  46. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  47. package/dist/core/runner/prepare-gate-stations.js +2 -1
  48. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  49. package/dist/core/runner/prepare-hands-readface.js +6 -6
  50. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  51. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  52. package/dist/core/runner/prepare-park-ask.d.ts +3 -0
  53. package/dist/core/runner/prepare-park-ask.js +5 -3
  54. package/dist/core/runner/prepare-policy-chain.d.ts +14 -2
  55. package/dist/core/runner/prepare-policy-chain.js +11 -4
  56. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  57. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  58. package/dist/core/runner/prepare-task.js +37 -18
  59. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  60. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  61. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  62. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  63. package/dist/core/runner/resume-apply.js +17 -3
  64. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  65. package/dist/core/runner/run-leg.js +5 -1
  66. package/dist/core/runner/runtask.js +2 -2
  67. package/dist/core/runner/sub-agent-deny-lists.d.ts +1 -1
  68. package/dist/core/runner/write-protection-station.d.ts +3 -2
  69. package/dist/core/runner/write-protection-station.js +1 -0
  70. package/dist/core/runner-deps.d.ts +14 -0
  71. package/dist/core/sensitive-path-policy.d.ts +8 -0
  72. package/dist/core/sensitive-path-policy.js +18 -2
  73. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  74. package/dist/core/shell-wrapper-projection.js +56 -0
  75. package/dist/core/shell-wrapper-table.d.ts +144 -34
  76. package/dist/core/shell-wrapper-table.js +40 -20
  77. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  78. package/dist/core/synthetic-read-verdict.js +3 -1
  79. package/dist/core/tool-catalog-entries.d.ts +21 -0
  80. package/dist/core/tool-catalog-entries.js +9 -0
  81. package/dist/core/tool-policy.d.ts +30 -8
  82. package/dist/core/tool-policy.js +9 -7
  83. package/dist/core/tool-spec.d.ts +3 -2
  84. package/dist/core/write-protect.d.ts +4 -0
  85. package/dist/core/write-protect.js +30 -9
  86. package/dist/index.d.ts +5 -3
  87. package/dist/index.js +4 -2
  88. package/dist/orchestration/run-spec.js +8 -7
  89. package/dist/orchestration/workflow.js +1 -1
  90. package/dist/shell/lex.d.ts +7 -0
  91. package/dist/shell/lex.js +17 -0
  92. package/dist/tools/fs/bash-program-position.d.ts +8 -87
  93. package/dist/tools/fs/bash-program-position.js +33 -69
  94. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  95. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  96. package/dist/tools/fs/index.d.ts +3 -2
  97. package/dist/tools/fs/program-read-model.d.ts +694 -0
  98. package/dist/tools/fs/program-read-model.js +1252 -0
  99. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  100. package/dist/tools/fs/program-read-model.rows.js +613 -0
  101. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  102. package/dist/tools/fs/read-deny-home.js +62 -24
  103. package/dist/tools/fs/read-deny.d.ts +54 -14
  104. package/dist/tools/fs/read-deny.js +104 -50
  105. package/dist/tools/fs/removal-targets.d.ts +7 -6
  106. package/dist/tools/fs/removal-targets.js +22 -22
  107. package/dist/tools/fs/safety.d.ts +3 -0
  108. package/dist/tools/fs/safety.js +10 -3
  109. package/dist/tools/fs/shell-read-argv.d.ts +85 -0
  110. package/dist/tools/fs/shell-read-argv.js +386 -0
  111. package/dist/tools/fs/shell-read-bindings.d.ts +273 -0
  112. package/dist/tools/fs/shell-read-bindings.js +746 -0
  113. package/dist/tools/fs/shell-read-check.js +23 -15
  114. package/dist/tools/fs/shell-read-establish.js +79 -5
  115. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  116. package/dist/tools/fs/shell-read-judge.js +9 -1
  117. package/dist/tools/fs/shell-read-plan.d.ts +31 -7
  118. package/dist/tools/fs/shell-read-plan.js +299 -393
  119. package/dist/tools/fs/shell-read-word.d.ts +29 -26
  120. package/dist/tools/fs/shell-read-word.js +30 -38
  121. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -64
  122. package/dist/tools/fs/shell-read-write-targets.js +83 -136
  123. package/dist/tools/fs/shell-reentry-text.d.ts +38 -6
  124. package/dist/tools/fs/shell-reentry-text.js +90 -53
  125. package/package.json +1 -1
  126. package/test/export-surface.snapshot.json +17 -3
  127. package/wire-changes/core@7.46.0.json +1522 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,249 @@
1
+ ## 7.46.0 — 2026-10-08
2
+
3
+ **Hard-breaking changes (the whole release):** two — ① `ToolPolicy.pathDenyRules` and the root export `toolPolicyPathDenyRules` are removed; a policy's path rules are `ToolPolicy.pathRules(anchors)` and `toolPolicyPathRules(policies, anchors)` (rows bound at the anchors of the leg the policy is installed on; `PathRuleDenyRow` stays the bound row's type, and `HandsToolkitOptions.pathDenyRules` — the library-direct deny table — is unchanged). ② `adjudicatePersistedPathRules(rules, call, views, anchorsOf, leads)`: the call's target is its views (`TargetViews`: `{ spellings, unreadable? }`, read by `callTargetViewsOf`), a row's anchors come from `anchorsOf(row)` (the person's rows: `persistedRowAnchors(scope, chainTop)`), and `leads` is where row heads lead in the judging leg (an empty map judges rows as spelled). **Who breaks:** a host that reads `policy.pathDenyRules` or calls `toolPolicyPathDenyRules` / `adjudicatePersistedPathRules` directly (three repos' code reads none of them — server only names `pathDenyRules` in comments and passes the member through `wrapToolPolicy`). **How to migrate:** a wrapper built with `wrapToolPolicy` needs nothing (it carries `pathRules`); a direct reader calls `toolPolicyPathRules(policies, { root, home })` with the anchors of the leg it builds a table for and takes the `deny` rows whose `faces` are non-empty; a direct `adjudicatePersistedPathRules` caller passes `{ tighten: { spellings: [target] } }`, `(row) => persistedRowAnchors(row.scope, { root, home })` and `new Map()`.
4
+
5
+ **#1429 —** hard-breaking: none; closed sets: 0; exports: 0. **Wording:** the card's shadow reason for a matched `ask` rule no longer calls the rule a person's — a deployment's must-ask list is a legitimate minter of `matchedAskRule` (`tool-policy.ts`), so the sentence now reads "an explicit ask rule matched this call (a person's or the deployment's ask-me-each-time outranks a standing allow rule)".
6
+
7
+ **#1432 / #1430 —** hard-breaking: none; closed sets: 0; exports: 0; `SubagentToolOptions` +1 optional key (`extraToolNames`); one new code, `config.extra_tool_names` (thrown where the delegation tool is built, and on a refused spawn's error result — `detail.reason` / `details.reason` ∈ `shape` | `undeclared_name` | `undeclared_origin`). **Behavior narrowing:** two — a sub-agent's `denyTools` / `allowTools` now govern the engine's hand tools its own run mounts (the built-in Explore / Plan lose `Edit` / `Write` / `NotebookEdit`; an allowlist keeps only the hand tools it names), and a delegation tool that declares its factory's roster has its stale list spellings refused before the run again.
8
+
9
+ **#1428 —** hard-breaking: none; closed sets: 0; exports: +1 `renderListEntry` (function; no behavior change). A serving layer that refuses a list entry for an unseen character can now render the entry the way the engine's own notices do instead of echoing it as written.
10
+
11
+ ### A sub-agent's tool lists govern the hand tools too, and a factory's declared roster makes its pool listable again (#1432 / #1430; behavior narrowing; +1 optional option key, one new code) (@server @cli @client-core @test)
12
+
13
+ - **Before (v7.45.x).** A delegated child's tools come from two places: what the delegation tool HANDS it (its pool and what an `extraTools` factory injects at the spawn) and the engine's HAND tools its own run mounts over an execution environment (`Read` / `Edit` / `Write` / `NotebookEdit` / `Bash` / `Grep` / `Glob` / `RepoMap` / `EnterWorktree` / `ExitWorktree` / `TaskOutput` / `TaskStop`). Two readings answered "is this tool hidden from the agent":
14
+ - **the lists filtered the pool only.** `denyTools: ["Write"]` on a definition, a delegation tool or `TaskSpec.agents` left the child its hand `Write` while the judgment before the run called the entry effective (the parent's roster carries `Write`); `allowTools: ["Read"]` left the child every hand tool; the built-in Explore / Plan, whose lists deny `Edit` / `Write` / `NotebookEdit`, handed their children all three (their own persona says they have no file-editing tool).
15
+ - **a factory made the pool unknowable.** A delegation tool with an `extraTools` factory published no pool (`ToolSpec.agentToolPool` absent), so the judgment could not refuse a stale spelling: on a deployment whose delegation tool always carries a factory, `agents: [{ denyTools: ["bash"] }]` ran to completion with an operator advisory and the child still holding `Bash` (7.44.x refused it; measured on a real service), and the memory content-origin reader read every such delegation as reaching outside.
16
+ - **Now.** One universe, one filter, one unmount fact.
17
+ - `SubagentToolOptions.extraToolNames` — the factory's roster: every tool it may inject, `{ name, aliases?, contentOrigin? }` (absent class = external, the class an undeclared host tool reads as). Read once where the tool is built; with it the declared tools join the delegation tool's pool, so the judgment refuses `bash` / `BASH` before the run again, a declared name is effective, and the memory content-origin reader classifies the declared tools as declared. No declaration ⇒ byte-for-byte the previous behavior (the pool unknowable, a stale name told with the "cannot enumerate" sentence).
18
+ - **The declaration does not lie.** At each spawn every factory product must be a declared tool — answering to exactly one entry's names (wire name and aliases: one it adds would escape a list judged against the roster, one it lacks would let the judgment call an entry effective that the filter cannot match), its own content class no further out than the entry's (`local` < `execution` < `external`). Otherwise that spawn is refused before anything runs: the delegation tool's result is an error (`Sub-agent not started: …`, naming the tool and the roster; `details: { error: "config.extra_tool_names", reason: "undeclared_name" | "undeclared_origin" }`) and the error (same code, `detail.reason`) reaches `onExtraToolsError`. A declared tool the factory did not inject is fine (the roster is an upper bound). A value that is not a roster, one spelling in two entries, or a roster with no factory throws where the tool is built (`config.extra_tool_names`, `detail.reason: "shape"`; the sentence names the entry's place and the type it found, never the value).
19
+ - **One filter.** At the spawn the governing lists (the chosen definition's, else the tool-level pair) pass the handed tools and the hand tools' names through ONE `resolveToolSubset` reading (`resolveAgentToolSubset`, `src/agents/tool-filter.ts`; the hand names are the tool catalog's rows mounted by `hands`, `HAND_TOOLS`). The handed survivors are the child's `tools`; the withheld hand names join the child's exclusion list (`excludeTools` on the child spec — the engine's one unmount fact: the full-shell reachability check, the memory write instruction and every other "is this tool mounted" reader read it), so the judgment's "effective" and the child's roster agree for the hand tools — in every permission mode (with `denyTools: ["Write"]` the child's `Write` call used to ask a person under `default`, write the file under `acceptEdits` / `bypassPermissions` and be refused under `dontAsk`; now there is no `Write` to call). The delegation's observer sidecar gets the same exclusion list. The exclusion list unmounts by wire name, so a withheld hand name that a handed tool is mounted under stays off it — `allowTools: ["FetchLocal"]` over a caller `Read` aliased `FetchLocal` keeps that `Read` (with or without an execution environment) — while a handed tool's alias never keeps a hand tool (`allowTools: ["Scribe"]` over `Scribe` aliased `Write` still withholds the hand `Write`). A content entry (`Write(src/**)`) hides the whole hand tool with the #1377 notice; the read-only clamp and a list meet in one set whatever the order (both are per-name).
20
+ - **Who is hurt.** (1) **The built-in Explore / Plan** children lose `Edit` / `Write` / `NotebookEdit` (they keep `Read` / `Grep` / `Glob` / `Bash`) — the `(Tools: All tools except …)` line the model reads for them was false and is now true; a host or a shell that relied on an Explore / Plan child writing files (it could, silently, under `acceptEdits` / `bypassPermissions`) sees those writes stop. (2) **An allowlist that names only caller tools** (`allowTools: ["Now", "search"]`): its child had every hand tool and now has none; name the hand tools the agent needs. A deny of a hand-tool name now hides it. (3) **The withheld hand names are inherited** like any exclusion: a child's own children (its nested delegation tool) never get a hand tool its list withheld, even under a definition that allows it — narrower than the pool half, which each level's own definition governs. (4) **A deployment that declares the roster**: a stale spelling in any list is refused before the run (`config.agent.unknown_tool`) where it was told; a factory product outside the roster (a name or an alias it adds or lacks, a further-out class) refuses that spawn. (6) An observer sidecar of such a delegation no longer has the hand tools the observed agent's list withheld. (5) Not narrowed yet: an engine tool outside the hand rows (`Monitor`, `Artifact`, `ExitPlanMode`, `SendMessage`, `ListAgents`, the memory tools, `Skill` …) named in a list is still judged effective without being hidden — `allowTools: ["Read"]` leaves `Monitor` (KL-1432-a, #1433).
21
+ - **Compensation.** Name the hand tools the agent needs in its allowlist (`["Read", "Grep", "Glob", …]`). Declare every tool the factory can inject, with its aliases and its content class; a product the factory adds later must be declared before it ships. To keep a hand tool for the agent but block by path or command, write a deny rule in the caller's permission policy.
22
+ - **Consumers.** server — S-1216: declare `extraToolNames` on the deployment delegation tool (`Now` plus every deployment extra tool: name, aliases, `contentOrigin`) to get the 7.118.0 refusal of `denyTools: ["bash"]` back; S-1215 (#1430) needs no change — `denyTools: ["Write"]` now hides the hand `Write`; a refused spawn's error result carries `details.error: "config.extra_tool_names"` and `details.reason`. cli — a child's tool list shrinks as above; no new card wording. client-core — the `SubagentToolOptions` type mirror gains one optional key; the delegation tool's error result may carry `details.reason`. test — pins below.
23
+ - **Known limits.** KL-1432-a — engine tools outside the hand rows are not narrowed by a sub-agent's lists yet (#1433).
24
+ - Pins: `test/backlog1432-deny-universe.test.ts` (66 cells; 54 red on the unchanged stage tree `681d05587`, the 7 green are the old forms and the controls; group ⑧ — a hand name a kept tool is mounted under — 2 red on `cb0493a33`, its 3 controls green).
25
+
26
+ ### `renderListEntry` at the package root — the one writer of a list entry into a sentence (#1428; export +1) (@server)
27
+
28
+ - **Before (v7.45.x).** The engine's refusals and start-of-run notices rendered a list entry with every unseen character (zero-width, bidi, non-breaking space, a run of spaces) written as `\uXXXX`, but the renderer lived inside `src/core/allow-entry.ts`; a host writing its own refusal sentence echoed the entry as written, so a person could not see where the entry was wrong.
29
+ - **Now.** `renderListEntry(entry)` is exported from the package root: the entry as a JSON string with every character that prints as nothing or as a space written as `\uXXXX` (`\u{…}` beyond the BMP), a run of spaces kept space by space, capped for printing. Nothing in the engine changed.
30
+ - **Who is hurt.** No one; additive.
31
+ - **Compensation.** None needed.
32
+ - Pins: `test/backlog1428-render-list-entry-export.test.ts`.
33
+
34
+ **Closed sets (#1102 car A):** registered, not exported — `ArgRoleKind` (8 members), `DatumForm` (6), `PathValueForm` (4) (`docs/CLOSED-SETS.md`); the binary gate's declared-witness rows are a closed list pinned by `test/program-read-model.structure.test.ts`.
35
+
36
+ **Behavior (#1102 car A):** the shell read station reads what a program does with each word out of one program read model whose every row is checked against the real binary; the readings that check falsified are corrected — curl's operands are URLs, curl's own `{}` / `[]` glob is expanded (narrowing), egrep / fgrep read with grep's grammar, `diff` reads its two operands' members without `-r`, a contents walk with no operand walks `.` only where the program does, `env -0` before a command is declined, a sed spelling one implementation rejects is read by the other alone, and option clusters are read as getopt reads them.
37
+
38
+ **#1422 —** hard-breaking: none; closed sets: none (`ENGINE_BUILTIN_TOOL_NAMES` is derived from the tool catalog, the registered set); exports: +2 (`ENGINE_BUILTIN_TOOL_NAMES`, `isEngineBuiltinToolName`); no runtime behavior.
39
+
40
+ **#1383 car A —** hard-breaking: none; closed sets: `CheckpointErrorReason` +1 (`hands_coordinates_damaged`); exports: +2 types (`CallTargetReading`, `CallTargetView`); `ToolCallRequest` gains one optional member (`targetReading`), the checkpoint state one optional key (`handsRoot`; no row format raised) — wire typeKeys +2, all optional (car A's `ToolPolicy.judgesPathTargets` never ships: car B folds it into `ToolPolicy.pathRules`). **Behavior narrowing:** one — the gate's path rule lanes reach more (the tightening direction only).
41
+
42
+ **#1383 car B + #1388 —** hard-breaking: **two** (`ToolPolicy.pathDenyRules` → `ToolPolicy.pathRules(anchors)` with `toolPolicyPathDenyRules` → `toolPolicyPathRules(policies, anchors)`; `adjudicatePersistedPathRules` takes the call's views, a per-row anchor function and the leg's head resolution); closed sets: +1 new (`ChildPlacement`: `honored` / `caller_tree` / `elsewhere`, reached through `ResolvedWorkspace.placement`), `ENGINE_NOTICE_CODES` +1 (`permission.rule_row_dropped`, audience user), `CheckpointErrorReason` +3 (`installation_record_absent` / `installation_record_misaligned` / `installation_record_invalid`, on `resume.parent_constraint_mismatch`); `unsupported.path_base` narrowed (only a declared base that is unusable); exports: +3 / −1 (+`toolPolicyPathRules`, +`BoundPathRule`, +`PathRuleSpec` (types), −`toolPolicyPathDenyRules`); new optional members: `ToolPolicy.pathRules`, `ToolCallRequest.pathRuleAnchors` (engine-internal seat — present only on the copy an installed policy hands its own inner `check`; the request the gate holds, an `AskRequest`, a hook's input and a parked row never carry it), `CallTargetReading.leads`, `InheritedGate.pathRuleAnchors` / `CheckpointState.inheritedGate.pathRuleAnchors` and `InheritedGate.pathRuleInstallations` / `CheckpointState.inheritedGate.pathRuleInstallations` (data half), `ResolvedWorkspace.placement`, `RunInternals.callerRoot` / `worktreeOffered` (core delegation callers only), `CheckpointState.handsHome` (no row format raised), `EffectivePermissionRules.droppedRows`; optional params `pathRuleReachOf(…, treeFamily?)`, `directoryRuleAdmits(…, treeFamily?)`, `scopeCoversCwd(…, identity?)`. **Behavior narrowing:** yes (see the section) — rules are anchored where they are installed (some cells reach more, some reach less), the allow direction admits only when every view is admitted, a project scope root must be a complete absolute path, a worktree child its environment left in the caller's tree is refused, a resume whose re-supplied chain lacks a usable installation record for an entry whose rules depend on its installing leg is refused.
43
+
44
+
45
+ **Package exports (the whole release):** +8 / −1 against v7.45.0 (`test/export-surface.snapshot.json` 2617 → 2624): +`BoundPathRule` +`CallTargetReading` +`CallTargetView` +`PathRuleSpec` +`toolPolicyPathRules` (#1383) +`ENGINE_BUILTIN_TOOL_NAMES` +`isEngineBuiltinToolName` (#1422) +`renderListEntry` (#1428); −`toolPolicyPathDenyRules` (#1383 car B, hard-breaking). The program read model stays internal; `judgeShellRead` stays its public reader. Two exported values change content, not shape: `SHELL_WRAPPER_TABLE` (+19 keys; `env` / `strace` / `ltrace` / `nsenter` rows change — #1102 car C) and `BARE_INTERPRETER_NAMES` (+`nocorrect`).
46
+
47
+ **Closed sets (#1102 car C):** `ArgRoleKind` +1 (`binds-name`: 9 members), `PathEffect` registered (5: `read`, `names`, `write`, `create`, `edit-in-place`), `ShellWrapperName` +19 (the keys of `SHELL_WRAPPER_TABLE`: 42 → 61), `BARE_INTERPRETER_NAMES` +`nocorrect`; the gate's `NOT_RUN` list +7 spellings (`nodejs --inspect-brk` / `--test` / `--watch`, `env -P`, `grep` / `egrep` / `fgrep --exclude-from`), each with its reason; car C2: `NOT_RUN` +4 (`setsid -c` / `--ctty`, `find -fprintf`, `flock --command=`) and the gate's `SINCE` list (+2: `env -a` / `--argv0`, coreutils 9.5), each with its reason.
48
+
49
+ **Closed sets (removal guard × read model):** `OptionMode` registered (`OptionSpec.mode`, 7 members; `forced` is new — rm's `-f` / `--force`) (`docs/CLOSED-SETS.md`); equivalence classes `rm-forced-switch` (78 plan entries) and `rm-forced-switch/read|write/looser` (86 write-face entries).
50
+
51
+ **Behavior (#1102 car C):** write targets, the read-only classifier and `SHELL_WRAPPER_TABLE` read the same program rows — launchers read the options their row declares (the station judges the program behind `sudo -u x`, `xargs -0`, `strace -o t`), cp / mv / sed are read per implementation with a word only one implementation edits in place written only where it exists (#1147), curl / fish / flock writes are judged, and the rows the binary gate falsified are corrected (BSD `sed -i` takes any next word as its suffix; GNU egrep / fgrep / diff / cmp / mawk / nawk / node rows; car C2: find read per implementation — GNU's `-fprint FILE` family is a judged write — and GNU tail's `+N`).
52
+
53
+ **Closed sets (#1102 car B):** `ArgRoleKind` +2 (`mount-source`, `object-path`: 11 members); `ReadCandidateProvenance` +2 (`mount_source`, `object_path`: 12 words, both minted). `ReadCandidate` (a package type) gains one optional member, `speculative` (`containment` | `whole`). Hard-breaking: none. Package exports: none added or removed; `VARIABLE_OPERAND_BUILTINS` (the second lexer's variable-operand set, internal) +`wait`.
54
+
55
+ **Behavior (#1102 car B):** the station's three fallback rules are retired — a parenthesis or backquote no longer makes a word unestablishable (program text is a slot, the arithmetic evaluator its own), a word of a program with no row is a speculative read (a containment candidate only where it exists), and `HOME` is a name of one binding thread every name is read through (a text's own assignments and literal `for` lists are read); the station asks "where does this read" only where it guards something (a roots face or a deny row), inline and standard-input interpreter programs are unestablished there, `git grep` / object paths / container mount sources are reads, a `case` arm's command is read, and the deny refusal's note states the reach. Narrowing on guarded boundaries, loosening (open to veto) on an open face with no deny row.
56
+
57
+ **#1417 —** hard-breaking: none; closed sets: none (`OptionMode` keeps its 7 members — `interactive` now also marks a shell's `-s`); exports: none added or removed; `ShellReadSegment` gains one optional member (`stdin`). **Behavior narrowing:** one (with two loosenings open to veto: an option's value or an operand after `-s` is no shell's script file; a launcher's own words are no shell command line) — a script a shell runs from its standard input is judged as a `-c` text is (a literal one is planned; one built when the command runs is `dangerous_delete`), where `bypassPermissions` released it; the same rule reads a POSIX shell's `+` option words (`bash +x -c …`) and the shell a launcher runs with no program (`sudo -s`, `sudo -i`, `doas -s`, `su`, `chroot DIR`).
58
+
59
+ **#1419 —** hard-breaking: none; closed sets: `RECOMMENDED_SENSITIVE_PATTERNS` +1 (`.sema/config-lkg-*.json`, the read tier's mirror row); exports: +0; new optional members: `RunnerDeps.agentConfigDir` (an absolute directory) and the `agentConfigDir` option of `createSensitivePathPolicy`; one new thrown code, `config.agent_config_dir_invalid` (a value that is not the absolute path of an existing directory). **Behavior narrowing:** one, only where a host declares the key — the built-in read deny rows that name `~/.sema/…` also refuse the same files below the declared root, the always-on write protection asks about a write below it as it asks below `.sema`, and the wired deny set denies its configuration files; nothing changes where the key is absent.
60
+
61
+ **#1425 —** hard-breaking: none; closed sets: 0; exports: 0 (the program read model's `assigns` word set gains `frozen` and `unsettled`, its rows an `assigns` column — the `readonly` row's own attribute — its grammars a `plusShort` table — a declaration builtin's `+` spellings — and the declaration builtins one grammar each instead of their union; internal, not registered, not exported). **Behavior narrowing:** one — a read the 7.46.0 binding thread vouched through a name the text made read-only (or gave another attribute, or a name reference) before the assignment may run is a read-boundary question again (`probe`), as it was in 7.45.0; the integer attribute's substitution forms in a function body, a loop and a command substitution — which 7.45.0 also missed — ask too; a declaration the text does not settle (`declare -p`, a computed switch, a computed program word) reads as unsettled (one documented over-ask, KL-1425-a: a computed program word makes every later assignment unknown, the 7.45.0 shape); a declaration bash refuses (`declare -rf F=v`, `declare -Z`, `export -r`, a `local` outside a function) binds nothing the text says, and a declaration's operands are expanded before it assigns any (`readonly F=v G="$F"`). Loosenings, open to veto: `export -n` / `export -p` / `readonly -n` / `readonly -p` with `NAME=value`, and `declare +f`, `export +x`, `readonly +r` with `NAME=value`, read as the assignments bash 3.2 and 5.3 perform (they asked).
62
+
63
+ ### What a program does with each word: one read model, checked against the real binaries (#1102 car A; behavior) (@server @cli @test)
64
+
65
+ - **Before (v7.45.x).** Eight tables answered "what does this program do with its k-th word" for the shell read station and the read-only shell classifier — option slots, recursive read forms, interpreter program delivery, the launcher table, sed's option dialects, message slots, the name families (no path operand / declaration / inert builtins / base movers / block words / code-body operands) — each with its own reader, none checked against a program.
66
+ - **Now.** One model: `src/tools/fs/program-read-model.ts` (schema and readers) and `program-read-model.rows.ts` (one row per program — its option grammar per implementation, GNU and BSD where they differ; its operand roles; its tree walk; its launcher, interpreter and code-body facts). The eight tables are deleted and every reader reads the model (`test/program-read-model.structure.test.ts` keeps their names out of `src/`). Every row is run against the real program (`test/program-read-model.binary-gate.test.ts`): each command line generated from the row runs twice in a temporary directory whose words are FIFOs, and the words the binary opened must equal the row's prediction word for word; a row the gate cannot run says why (a closed list), and an implementation the host lacks is a listed skip in `test/skip-baseline.json`, never a pass. The move is pinned read-equivalent over a 16 839-command / 25 714-argument-list / 46-token-table / 141-name corpus recorded from the previous tree (`test/program-read-model.equivalence.test.ts`): every difference is an entry of one of twelve classes, each naming the old reading, the new one, the gate cell that witnessed it and why the old one was wrong (`test/fixtures/program-read-model-equivalence.allowed.json`, 4 115 entries). The classes that change a judgment — a refusal, a card, a vouch, or the candidate set a deny row is read against — are these:
67
+ - **curl's operands are URLs.** Before: an operand was a path word (`curl /v1` offered `/v1`, `curl file:///etc/passwd` the relative name `file:/etc/passwd`). Now: a `file:` URL offers the local path it names (percent-escapes decoded, any authority local) and is judged against the roots as `--url` already was; any other operand offers nothing — curl sends it to a host or rejects it. Corpus: 657 commands change their candidates, 484 their verdict (482 ask or refuse no more, 1 asks where it vouched, 1 changes only the finding). `-5` is no curl option (26 option tokens).
68
+ - **curl expands its own glob (narrowing).** Before: `curl "file:///w/{secret,x}"` handed the deny judge the literal `/w/{secret,x}`, and curl read `/w/secret` unrefused. Now: a URL (operand or `--url`) and an upload file (`-T`, `--upload-file`) are expanded the way curl does — `{a,b}` sets, `[a-z]` / `[1-9]` / `[01-10:2]` ranges, over the whole URL, scheme included — and every expansion is judged (a range step is read as curl reads it, `[1-9:+2]` and `[1-9: 2]` included); a value the station cannot list — more than 256 expansions, or a form it does not parse, a bracket curl would refuse among them — is unestablishable (a card, or a refusal under a deny table), never "opens nothing". This closes KL-844 (the transfer program's own range / list expansion inside its URL). `-g` / `--globoff` (alone, in a cluster, before or after the URL) reads every value literally — unless it stands behind an option the row does not explain (it may be that option's value); a later `--no-globoff` turns the glob back on.
69
+ - **egrep / fgrep read with grep's grammar.** Before: they had no option row — every option word was speculative, an option's value and the pattern were path candidates. Now: grep's one grammar (the recursion reading already shared it). Corpus: 335 commands change their candidates, 134 their verdict (132 ask or refuse no more — a pattern or an option value that named a denied or outside path; 2 change only the finding); readers that list egrep / fgrep only.
70
+ - **`diff` reads its two operands' members without `-r`.** Before: diff walked only under `-r`, and `-Dr` / `-Lr` read as `-r` (the recursion scan read into the option's value). Now: every diff with operands is a contents walk of them — two directories are compared member by member, one level without `-r` (read as the whole tree, KL-1102-b) — so a deny row inside a directory operand is judged. Corpus: 448 commands and 1 269 argument lists change their candidates, none its verdict in the corpus's configurations.
71
+ - **A contents walk with no operand walks `.` only where the program does.** Before: every recursive form with no operand walked the working directory. Now: grep (GNU and BSD alike), rg, ag and ack do; `diff -r` and `tar c` with no operand refuse to run and walk nothing — a working directory outside the roots no longer asks for them.
72
+ - **`env -0` / `--null` before a command is declined.** Before: the walk stepped over them to the program. Now: neither implementation runs a command beside them (exit 125), so `env -0 cat f` is a position the walk does not read past (2 commands: a card where they vouched).
73
+ - **sed's two implementations.** Before: both readings declared `-a` / `-H` / `-I` / `-s` / `-z` and every long option. Now: GNU sed has no `-a` / `-H` / `-I`, BSD sed no `-s` / `-z` and no long option; a cluster no implementation accepts leaves the script unread (unestablishable: 10 corpus readings, probed as commands, ask where they vouched); `-He` / `-Hf` read through BSD's switch to the script / script file (4 option tokens).
74
+ - **A shell's `-c` text and a builtin's code body are read with the program's own grammar.** Before: the text a shell runs was found by scanning its option words for a `c`, and `trap` / `mapfile` / `readarray` / `compgen` bodies by a scan of their own — so `bash -o noclobber -c 'cat <denied>'` read `noclobber` as a script file and the text went unread (a card where the plain `bash -c` form is refused). Now: each shell row carries its option grammar (the letters each shell on the gate host takes in front of `-c`, measured — sh, bash, zsh, dash, ksh, csh, tcsh, fish; `-o` / bash's `-O` take an option name; the POSIX letters for ash / mksh / pdksh / yash), and both readers take the shell-text value the model's walk reads; `mapfile -t` and compgen's action letters are declared switches. Such a line is refused on its deny hit (narrowing: what asked is refused — `bash -o noclobber -c 'cat <denied>'`); a letter a shell does not take still hides the text (a card).
75
+ - **Declared switches and getopt clusters.** A switch the model declares (an interpreter's `--help` / `--version`, git's global switches, `ls -R`) offers no payload candidates from the letters behind it (667 option tokens, 80 with fewer candidates; 109 commands); a value-taking letter owns the rest of its token, so `grep -Dread x f` keeps `x` the pattern (5 tokens); `sqlite3 -init FILE` reads FILE as `-init`'s value (4 readings). No verdict changes in the corpus.
76
+ - **Contract.** `shell.read_boundary.option_value_slots` moves with the table onto the model's `OptionSpec` (`docs/CONTRACT-SENTENCES.md`), restated for roles, implementations and the program's own glob.
77
+ - **Who is hurt.** A deployment that counted on a deny row to stop `curl <path>` or `egrep <denied path> f`: those programs never opened the path, and the station no longer says they did. A host whose probes pinned the old readings: `curl http://y -- -d@/etc/passwd` no longer offers `<cwd>/-d@/etc/passwd`; `env -0 npm test` no longer reads past to `npm`. A command line that names many files through one curl glob asks; one whose glob reaches a deny row is refused. `diff dirA dirB` with a deny row below a directory operand is refused or asked (one level would have sufficed — KL-1102-b).
78
+ - **Compensation.** None needed for a reading the binary witnessed; a deny row that must stop a curl transfer of a local file names the file (`file:` URL) or the upload file.
79
+ - **Consumers.** server — `judgeShellRead` / the gate answer differently for the commands above; no wire shape changes. cli — the cards and refusals above appear or disappear with no new wording. test — the corpus classes above are replayable cells (the equivalence pin's allowed entries carry each command).
80
+ - **Known limits.** KL-1102-b (diff's one-level read is read as a whole-tree walk) · c (the GNU sed / grep / diff / cmp / tar / find / xargs / time grammars are not witnessed on a macOS host; the Linux lane runs them) · d (option values declared without a role are still read speculatively — counted per row) · e (rg / ag / ack read `.` even when they would read a piped stdin) · f (spellings the gate does not run, a closed list with reasons) · g (curl's config file content is not read) · h (fish's `-C` init command is not read as shell text). KL-994i: the code-body positions are the model's rows, unchanged.
81
+ - Pins: `test/program-read-model.equivalence.test.ts` (the move, four faces, twelve classes), `test/program-read-model.binary-gate.test.ts` (every runnable row × implementation; the missing-binary pin), `test/program-read-model.structure.test.ts` (the retired names, the role set, the declared-witness list, the unmodelled-value counts), `test/backlog1102-program-facts.test.ts` (curl's glob × `-g`, the no-operand walk); re-pinned: `backlog764-verb-option-slots` G-6-d, `backlog862a-launcher-options` (`env -0`), `backlog222-recursive-read-demote` (`diff a.txt b.txt` walks both operands), and six pins that read the retired tables now read the model with unchanged assertions (945-plan, 780 ×2, 891, 502, 1114).
82
+
83
+ ### The gate's path rules judge the target's canonical view and its symlink hops beside its spelling (#1383 car A; behavior narrowing; +2 optional members, +1 checkpoint key) (@server @client-core @test)
84
+
85
+ - **Before (v7.45.x).** A path rule at the gate — a rule policy's content lane, the person's persisted lane, a skill's `disallowed-tools` path rows — compared the rule with the call's spelling joined to its base and folded (`.` / `..`), nothing else. Shapes it never reached, measured on a real lane (ask: no card, the content reached the model; deny: the gate let the call through and only a `Read` / `Edit` rule's in-tool deny table refused it):
86
+ - a rule spelled canonically × the model spelling the target through an alias of the tree (a host-spelled task root; `Read(//<real>/secret/**)` × `Read <alias>/secret/a.txt`);
87
+ - an in-tree link to the guarded directory: `link -> secret`, `Read link/a.txt`, `Grep path=link` (formerly KL-1291-a ③);
88
+ - a case variant on a case-insensitive disk: `Read SECRET/a.txt` (formerly KL-1291-a ③);
89
+ - on a canonical task root, a policy with its own base after `cd sub`: `Read ../secret/a.txt` under `Read(secret/**)` / `Read(/secret/**)`, and `Read ../.env` under `Read(.env)`.
90
+ The pre-claim recheck of an edited parked call carried no root (KL-1376-a): a policy-decided edit to a relative Grep / Glob `path` was refused (`checkpoint.invalid_outcome`) under a policy with no base.
91
+ - **Now.**
92
+ - One reader of a call's target: `readCallTarget` (`src/core/effective-path-target.ts`) reads, from the request's own `root` / `cwd` and the leg's execution environment, each reading of the call's path slot — the protective one (deny / ask) and the declared one (allow) — as its LEXICAL view, its CANONICAL view (the spelling resolved by the environment) and the HOPS of the leaf's symlink chain. The views have one producer (`readPathViews`), which the tool's own deny judgment (`resolveKey`) reads too — the tool now also judges the hops.
93
+ - `ToolCallRequest.targetReading` (optional) carries the reading; it holds no root and no cwd. Stamped by the gate where its request is minted over the final arguments, again after a policy rewrite (the lanes judge the rewritten target's views, never the old one's) and on an approved edit; by the park re-adjudication, the resume belts and the changed-files visibility probe. It describes the `args` object it was taken for and nothing else (the engine's lanes read it through `callTargetReadingOf`); every seat that rewrites the arguments and hands the request on — a composition's rewrite cascade (`combinePolicies`), the final-form re-check, an inherited station's approved-edit re-check, the lanes after a policy rewrite, an approver's edit — re-reads it for the new arguments the same way (`withArgsReading`). The pre-claim recheck reads the lexical views at the row's coordinates (no environment yet).
94
+ - Every path rule lane judges the lexical view it always judged plus every view of the protective reading: a deny / ask row any view reaches is reached; the reported rule is the lane's first (rows outer, views inner). A view the environment cannot answer is absent and takes no part. The allow direction reads the declared reading's lexical view only — the same as before (car B: every view must be admitted, K-5).
95
+ - (car A's `ToolPolicy.judgesPathTargets` — "this policy judges a path target" — is folded by car B into the presence of `ToolPolicy.pathRules`; it never ships.)
96
+ - A view the environment cannot answer — a canonicalization or a chain read that fails, throws, elapses (the shared gate deadline) or is aborted — is absent; one view's failure never takes another down.
97
+ - The reading is taken only on a leg whose lanes can hold a path rule (the caller policy chain carries `pathRules`, a rule store is wired, or a loaded skill's `disallowed-tools` holds a path entry), and only for a call with a path target: one canonicalization and one chain read per gated path call; none on any other leg.
98
+ - A write through an in-tree link into a directory an `Edit(<pattern>)` deny names (`Write <d>/lnk/new.txt`, `lnk -> secrets`) is refused at the gate (`tool_end.gate.disposition.kind: "denied"`, the rule's sentence) — one more shape the gate refuses; the tool refused it before too (`write deny list`), so what the user sees changes in the refusal's source and sentence, not in the outcome. A person's `Edit` deny ROW still meets such a Write inside the tool (its other faces are the deny table's).
99
+ - Checkpoint `state.handsRoot` (optional): the hands' root at suspend, read by the pre-claim recheck of an edited parked call; a row written before it reads as before. The resume screen judges both hands' coordinates by shape: a present `state.handsRoot` / `state.handsCwd` that is not an absolute path is refused before the claim — `checkpoint.invalid_outcome`, `detail.reason: "hands_coordinates_damaged"`, the row pending.
100
+ - Measured on the same cells (Before → Now): the link cell, the case cell, the aliased-spelling cell and the canonical-root `cd` cells (these through the reading's views of the call's own root and live working directory) give the rule's card / the gate's refusal (`denied`, attributed to the policy or the person's rule) with nothing reaching the model; a policy-decided edit to a relative Grep `path` the rule does not reach passes the pre-claim recheck and runs.
101
+ - **Who is hurt.**
102
+ - A deployment with path ask rules: a card where a link, an alias of the tree, a case variant or a `cd`-then-relative spelling (canonical root) reaches the guarded path — the content used to reach the model with no card.
103
+ - A deployment with path deny rules: such a call is refused at the gate (`tool_end.gate.disposition.kind: "denied"`, the rule's refusal sentence) instead of inside the tool (`read_path_denied`); the refusal text and the attribution change, the outcome (refused) does not.
104
+ - A leg with a rule store wired pays one canonicalization per gated path call even when the store holds no path row (the store is read per call, after the reading); a remote environment pays a round trip for it. A chain read is asked only for a target whose canonical view differs from its spelling (a link somewhere on the way, an alias of the tree, the disk's own case) or whose spelling climbs (`..`): then once at the gate (on such a leg) and once per tool resolution of the target (the Read tool resolves twice: its admission check and the read); a target with no link anywhere costs no chain read (KL-1383a-d).
105
+ - A checkpoint store that hands back a row whose `state.handsCwd` is not an absolute path (damaged storage): 7.45 resumed it with that value as the working directory; it is now refused before the claim (`hands_coordinates_damaged`), the row pending.
106
+ - A downstream test that pinned the former KL-1291-a ③ reading (no card through a link / a case variant) goes red: re-pin.
107
+ - **Compensation.** None needed for the narrowing — the rule's author named the guarded path; the new card / refusal is on a call that reaches it. Re-pin tests to the new semantics.
108
+ - **Consumers.** server / client-core — `CheckpointErrorReason` gains `hands_coordinates_damaged` (a mirror of the closed set adds the row). server — `ToolCallRequest.targetReading` is engine-stamped and passes through `wrapToolPolicy` untouched; a checkpoint store that round-trips `state` verbatim needs nothing for `handsRoot`, one that whitelists keys adds it. client-core — typeKeys +2 (`ToolCallRequest.targetReading`, `CheckpointState.handsRoot`), all optional. test — the black-box forms: a canonically spelled rule × a host-spelled absolute target; `link -> secret` with `Read link/a.txt` and `Grep path=link`; a case variant on a case-insensitive disk.
109
+ - **Known limits.** The rule's ANCHOR is not moved by this change; car B (next section) moves it and closes every cell this list named (KL-1383a-b is removed). As car A left them: on an ALIASED task root, a policy with its own base after `cd sub` reading `../secret/a.txt` (ask: no card, the content reaches the model; deny: the gate lets it through, the tool refuses); the person's `Read(.env)` after `cd sub` reading `../.env` (the persisted lane's relative pattern follows the live working directory); an Agent `cwd` child reading the parent's original under the parent's persisted `/x` row; a session started in a project subdirectory under a project-scoped row; a worktree child; a rule that names the LINK (`//proj/link/**`) read through the target; KL-1376-b (a host-spelled `//` rule × a canonically spelled target). The pre-claim recheck judges at the row's coordinates, so a workspace restored elsewhere can be refused there that the belt would not refuse — the row stays pending (KL-1383-a). A row written before `handsRoot` keeps the KL-1376-a reading. That an older worker resumes a row carrying `handsRoot` unaffected is inferred from the field being an ignorable optional key; it is not pinned in this tree.
110
+ - Pins: `test/backlog1383a-call-target-reading.test.ts` — P7 (canonical rule × host-spelled target, policy and store, ask and deny), P8 (link: Read and Grep), P8c (case variant), AR (policy with a base, `cd sub`: canonical root reached, aliased root still leaks), C1 (policy with a base reached; the person's row not), RW (a rewrite: the rewritten target's canonical view, never the old view), RWC (the same inside one caller chain), EDT (an approver's edit after a policy rewrite), HOP (a chain read that throws or never answers), SIG (one call's cancellation never reaches another call's reading), BAD (a damaged `handsRoot` / `handsCwd` refused typed), UP (a `..` spelling across a linked parent: the chain is still read), P21 (control), Z (no path rule ⇒ the target is canonicalized as often as with no policy; a path rule ⇒ once more), U (an environment that cannot answer: lexical only, no throw), ALLOW (the allow direction unchanged both ways), P18 (a same-name `Read` with no declared target: the protective floor's canonical view denied, no allow), TH (the tool's deny judgment reaches a hop), the args binding, P15 (structural: every stamp point reads the reader; the reading has no root / cwd; the lane files mint no identity), P16 / P20 (the checkpoint's `handsRoot`, an old row, a moved coordinate); `test/backlog1291-read-ask-covers-search-tools.test.ts` (the former KL-1291-a ③ cell re-pinned: asked, both rule sources); `test/backlog1132-deny-tables.test.ts` (the symlink Write: refused at the gate under the policy; the tool's own judgment kept on a person's row); `test/design383-capture-optout.test.ts` and `test/durable-checkpoint.test.ts` (the checkpoint key census and round trip).
111
+
112
+ ### A path rule's anchor is installed once; the gate and the deny tables bind rules with one function and judge them with one (#1383 car B; hard-breaking: two; behavior narrowing) (@server @client-core @cli @test)
113
+
114
+ - **Before (v7.45.x, and car A).** Three readings of "the base a path rule and its target resolve against" stood side by side, and the deny tables bound their rows a fourth way:
115
+ - a caller policy with its own `pathBases` read its rules AND the call's target under those static bases (`callBasesOf`); without them it read both under the request's `root` / `cwd` — so after `cd sub` a relative `Read ../secret/a.txt` was judged as a different file than the tool opened (on an aliased task root car A's canonical view did not help: the rule was spelled through the alias, the target canonically);
116
+ - the person's persisted lane anchored `/x` at the task root as the host spelled it and a relative pattern at the LIVE working directory: `Read(.env)` after `cd sub` no longer named `<root>/.env` (ask: no card, the content reached the model; deny: let through at the gate);
117
+ - a child leg (Agent `cwd`, a worktree, a session started in a project subdirectory) re-anchored the parent's / project's rows at its own root: the parent's original `<d>/secret/a.txt` read from an Agent `cwd` child under the persisted `Read(/secret/**)` deny ran (the row guarded `<d>/sub/secret` instead);
118
+ - the in-tool deny rows were bound when the policy was built (a policy with no base: no tree family at all — `Read(//C/proj/secrets/**)` reached none of the three spellings of that file on a win tree), and judged views-first (the refusal could name another rule than the gate's);
119
+ - a rule that names a LINK (`Read(//<d>/link/**)`) never met a call naming the real directory at the gate;
120
+ - the allow direction admitted a read whose spelling was under an allowed directory although it led out of it.
121
+ - **Now.** One law, each fact with one source (INTEGRATION-CORE "A path rule and the call it judges"):
122
+ - **The target is the call's:** every lane reads it through `callTargetViewsOf` — the Runner's reading of the call (car A), else the call's own `root` / `cwd`, else (a host's bare call) the policy's declared `pathBases`. The `callBasesOf` switch is gone.
123
+ - **The anchor is the rule's, installed once:** a caller policy is installed on the leg it enters (`installPolicyAnchors`, at prepare; declared `pathBases` word by word, else that leg's canonical root and environment home) and every child holds that installation (its own caller slot included when it names the same policy — a deployment-level policy is installed once, at the top); the person's rows anchor by scope — a PROJECT row at its own scope root, a global / session row at the chain's top leg (`InheritedGate.pathRuleAnchors`, persisted with the checkpoint), `~/` at the top leg's home; a skill's / the request's / the deployment's list rows at the leg the list is installed on. A chain handed to a child uninstalled (a cross-process resume, a host-built chain) is installed where each entry's installing leg recorded it (`InheritedGate.pathRuleInstallations`, position for position, persisted with the checkpoint; a checkpoint's records — this one and the chain top's — have their root and home moved with a restored workspace) — an entry whose rules depend on that leg (a `/x`, relative or `~/` word its declared `pathBases` do not cover) with NO usable record (absent: a chain written before 7.46.0 or built without records; misaligned: a record of another length than the chain; invalid: a member naming no absolute root; an invalid LIVE record is not replaced by the checkpoint's) is REFUSED, never installed at another leg's anchors: `resume.parent_constraint_mismatch` with `detail.reason` `installation_record_absent` / `_misaligned` / `_invalid`, before the claim on a resume (the checkpoint stays pending), at prepare on a fresh leg. An entry whose rules read the same anywhere needs no record. The pre-claim recheck of an edited parked call installs the edit-base policy by the same rule at the row's root and home (`state.handsRoot` / `state.handsHome`). A `cd`, a child or the host's spelling of the root never moves an anchor.
124
+ - **One binding, one judgment:** `bindPathRules` binds, `pathRowsReach` judges (rows outer, views inner) — the caller policy's content lane, the person's lane, the entry lists and the read / write deny tables alike; each row is also judged where its head leads in the judging leg (the reading carries the leg's resolution, `targetReading.leads`, taken once per location per leg). The gate and the table name the same rule.
125
+ - **Allow (K-5):** a stored `Read(//dir/**)` allow admits a read only when every view of the declared reading lies under the directory.
126
+ - **A project scope root is a complete absolute path** (`/…`, `X:\…` / `X:/…`, `\\host\…`) — one predicate (`isProjectScopeRoot`) for the consent face, the durable face, the sync wire's pick and the store's read; a row whose root is not one is refused on write, dropped from a sync round with a warning, and dropped on a read — the person is told once per run (`permission.rule_row_dropped`, `detail: { rows: [{ rule, scopeRoot }], count, sessionId, runId }`; the sentence names no path). `/` is a legal root: its `/secret/**` names `/secret`.
127
+ - **Which project rows apply is judged by where the roots are — for a deny or ask row:** such a row applies when its scope root covers the leg's root under either spelling — the leg's root as the host spells it, or its canonical spelling against the row's root resolved where it leads (the leg's one resolution, once per location). A deny / ask row written under a link to the project (or a leg rooted at a link to the row's directory) applies at the gate's three arms (path, command, name) and the run's deny tables. **An allow row applies only where its scope root is spelled** (the gate, the coverage table, the card's consent face and its `directoryRead` mint gate alike): a link someone can re-point never carries a standing approval to another directory — the same direction law as K-5.
128
+ - **`unsupported.path_base`** now means a DECLARED base that is not an absolute path of its tree's family; an undeclared base is no compile-time refusal (the rule is anchored where it is installed; a bare call with no base reads it unreadable — the fail-closed ask).
129
+ - **`ToolPolicy.pathRules(anchors)`** replaces `pathDenyRules` and car A's `judgesPathTargets` (one member per fact: it answers the rows, and its presence tells the Runner to read the target); an installed policy hands the anchors to its inner compiled `check` on a COPY of the request (`ToolCallRequest.pathRuleAnchors`), so a path rule keeps judging the arguments its position in a composition gives it.
130
+ - **A child placed in another filesystem namespace** (its environment did not put it where it was asked and it is not inside the caller's tree — `childPlacementOf`) takes its parents' rules again at its own anchors and is its own chain top (K-9); a workflow agent child is judged by the same function.
131
+ - The frozen spec surface of a workflow reads the call's views too.
132
+ - Measured on the real lane (Before → Now): P1 / P2 (canonical root) were already reached by car A; AR (aliased root, a policy with a base, `cd sub`), P3 (the person's `Read(.env)` after `cd sub`), P5 (the person's `/secret/**` on a host-spelled root, shell and read-only hands), P6 (a no-base policy's host-spelled `//` row × relative / canonical target; a `Grep` deny), P9 (the rule names the link), P10 (an anchor reached through a link re-pointed mid-leg: as spelled now, the twin from the next prepare), P12 (worktree child: the parent's original refused / asked), P13 / P13d (Agent `cwd` child: the parent's files refused; a deployment-level policy alike), P22 (session in `<P>/sub` × a project row), P14 (a link out of an allowed directory) and P17 (gate and table report the same rule) go from no card / let through to the rule's card / the gate's refusal with nothing reaching the model; P4, P12's copies, P13's mirrors, P23's `<P>/…` and P25's restored copy go from reached to not reached (the removed reach, below).
133
+ - **Who is hurt.**
134
+ - **Reached less (the anchor stays where the rule was installed):** a relative pattern with a `/` after `cd` (`Read(a/b/**)` no longer reaches `sub/a/b/…`; write `**/a/b/**` to reach it anywhere — a divergence from the reference, which follows the live cwd: `relative-path-rule-anchors-at-installation`); a child's mirror of a parent rule (`<child root>/guarded/…` under the parent's `/guarded/**`), a worktree's copy under the parent's `/x` (a bare-name rule such as `secret/**` still guards it); a project row in a session started in a project subdirectory now names `<P>/secret`, not `<P>/sub/secret` (a global row names the top leg's); a workspace restored INSIDE the old project scope keeps the old anchor (KL-1383-b).
135
+ - **Reached more:** every cell in the measurement above — more cards and gate refusals (`tool_end.gate.disposition.kind: "denied"`, `deniedBy` the rule's source) where a call used to run; a person's `Read(...)` deny now refuses the Read tool at the gate where its twin meets the call (the in-tool `read_path_denied` came after the gate let it through).
136
+ - **The refusal names another rule** in a table where two rows reach one call through different views (the gate's order now).
137
+ - **A project row with a root that is not a complete absolute path** (`C:`, a relative spelling) — only a foreign writer or a hand edit makes one — no longer applies; a host's own scope validator mirroring `isValidDurableScope` disagrees until it adopts the same predicate.
138
+ - **A host that compiled a no-base policy with a relative or `~/` rule expecting `unsupported.path_base`:** it compiles now.
139
+ - **A policy whose `pathBases.root` names a tree other than the task's** no longer guards the task's tree: its `/x` words name `<declared root>/x` (they used to be read against the call's base as well). To guard the task's tree, declare no root or declare the task's root.
140
+ - **A task parked before 7.46.0 under a re-supplied chain whose entry's path rules depend on the leg that installed it** is refused on resume after the upgrade (`installation_record_absent`); the way out is to let a worker of the version that parked it finish it, or let it expire and submit the task again. A host that re-supplies such a chain on a fresh leg without records is refused the same way.
141
+ - **A deny / ask project row written under another spelling of the project's directory** now applies (it did not at all): more cards and refusals where such a row exists. An allow row written that way still admits nothing there (unchanged).
142
+ - Hard-breaking readers: see the release head (`pathDenyRules`, `toolPolicyPathDenyRules`, `adjudicatePersistedPathRules`).
143
+ - **Compensation.** The rule's author named the location the rule now reaches; to follow a relative pattern into subdirectories write it with `**/`; to protect a child's own tree, a rule installed on that child (its `TaskSpec.toolPolicy`) anchors there. Re-pin tests to the new semantics (the four re-pinned families in this tree: #1376 P3, #644 / permission-rules / hardening "missing base" cells, car A's AR / C1 / ALLOW, the 7340rj / rm twin cells now refused at the gate).
144
+ - **Consumers.** server — `pathDenyRules` → `pathRules` is transparent through `wrapToolPolicy`; a no-base lane no longer needs `pathBases.root` / `.cwd` for relative or root-relative rules (the 422 arm for them can go: `unsupported.path_base` now fires only for a DECLARED unusable base); keep declaring the sandbox / device environment's `homeDir`; S-1143's cwd adoption is not read by the rule side (the target reads the engine's stamped cwd); the SQL store's durable-face mirror must adopt the complete-absolute-root predicate (`isValidDurableScope` / `isValidConsentScope` changed); a workflow agent child is placed by the same `childPlacementOf` as a delegated one; where the server assembles `pathBases`, check that a declared `root` is the task's own (a root elsewhere moves every `/x` word there); a resume that re-supplies `parentConstraints` must pass the row's `inheritedGate.pathRuleInstallations` through untouched (or none, letting the row's be read) — a pre-7.46.0 parked task under a chain with relative / `/x` / `~/` rules is refused (`resume.parent_constraint_mismatch`, reason `installation_record_absent`), drain or expire those before the upgrade (a line for the server's DEPLOY-PREREQS); `permission.rule_row_dropped` is a new user-audience notice. client-core — typeKeys: `ToolPolicy` −`pathDenyRules` +`pathRules` (optional), `ToolCallRequest` +`pathRuleAnchors` (engine-internal; never on a gate request, `AskRequest`, hook input or parked row), `CallTargetReading` +`leads`, `InheritedGate` / checkpoint `inheritedGate` +`pathRuleAnchors` +`pathRuleInstallations`, `ResolvedWorkspace` +`placement` (closed set `ChildPlacement`), `RunInternals` +`callerRoot` / `worktreeOffered`, `CheckpointState` +`handsHome`, `EffectivePermissionRules` +`droppedRows`; `ENGINE_NOTICE_CODES` +`permission.rule_row_dropped` (audience user); `CheckpointErrorReason` +3; the `unsupported.path_base` row's sentence now says "the declared base is not usable". cli — L-1085: a relative rule does not follow `cd` (the settings wire's `validatePermissionRules` no longer needs `{ root }` to accept a relative rule). test — the P-cells above as black-box forms (cd + relative ask / deny, host-spelled roots, worktree and `cwd` children, links, case variants, a project row in a subdirectory session).
145
+ - **Known limits.** KL-1383-b (a workspace restored inside the old project scope); KL-1383b-a (a background / fork delegation's launch line names the worktree before the child's placement is judged); KL-1383b-b (two namespaces sharing a path string, a factory claiming it honored); KL-1383b-e (an allow project row written under a link to the project admits nothing there: a card is raised); KL-1383b-f (a `/` project root anchors `/x` at `/`). KL-1383a-b and KL-1376-b are removed (closed); KL-1383b-c / KL-1383b-d, written during this release, are closed by it (the dropped row is announced; a chain without a usable record is refused). Not closed here: #1145 (a path ask on Edit does not reach a Bash write — a call face, not an anchor), #1271 (a `~/` row anchors at the declared home only — a second home anchor is a design question of its own), #1293 (b) (a person's deny row beyond its own tool is judged by the table, not at the gate) and (c) (RepoMap / LSP declare no path target).
146
+ - Pins: `test/backlog1383b-anchors-and-rows.test.ts` (P1, P2, AR, P3, P4, P5, P6, P7 / P8 controls, P9, P10, P12, P13, P13d, P14, P17, P19, P21, P22, P23, P25, ORD, SEAT, ID1–ID3, DR), `test/backlog1383b-seats.test.ts` (P11, P15 structure, P24, the hard break), `test/backlog1383b-chain-installations.test.ts` (R1–R6, R8, R9, H1, H2), car A's P16 / P18 / P20 in `test/backlog1383a-call-target-reading.test.ts`.
147
+
148
+ ### A worktree child its execution environment did not place in the worktree is refused, and the report says where a child ran (#1388) (@server @test)
149
+
150
+ - **Before.** An `executionEnvFactory` that ignored the worktree hint (`parentCwd = <worktree>`) and rooted the child at the caller's own root left the child reading and writing the caller's checkout while the delegation result said `worktree: <dir> (pruned — the agent made no changes)`; on a deployment with `rootPath` the child's root was the base root even when the factory did honor the hint.
151
+ - **Now.** The child's prepare judges where its environment placed it (`childPlacementOf`: `honored` / `caller_tree` / `elsewhere`, on the child's own root; `ResolvedWorkspace.placement` reports it): a worktree child placed inside the caller's tree is refused before it starts (`isolation.unavailable`; zero rounds; its environment destroyed) and the delegation tool settles the unused worktree and answers its typed "isolation unavailable" result, never a worktree line; a child placed elsewhere (another namespace) runs, and its report says where it ran instead of naming the worktree (`worktree: none — the execution environment ran the agent at <cwd>…`); an honored worktree child is unchanged and roots at the worktree whatever `rootPath` says.
152
+ - **Who is hurt.** A deployment whose factory ignores the worktree hint for local children: those children are refused (they used to run in the caller's checkout).
153
+ - **Compensation.** Honor `parentCwd` in the factory, or run the child without `isolation`.
154
+ - Pins: `test/backlog1388-worktree-placement.test.ts` (NH / EL / OK).
155
+
156
+ ### Write targets, the read-only face and the wrapper table read the same model (#1102 car C; behavior) (@server @cli @test)
157
+
158
+ - **Before (v7.45.x, and 7.46.0 car A).** Three readers kept their own copy of what a program does with its words. The write face had a list of write commands, two hand grammars for sed and for cp / mv, an operand-seat table and a "source written" table; the read-only classifier had a third option grammar (which options take a separated value), find's and sed's predicate lists, a stdin floor table and its own head / tail checks; and `SHELL_WRAPPER_TABLE` — the second lexer's wrapper peel, a package export — declared every launcher's options a second time, disagreeing with the station's launcher rows (`env` had one spelling of `-u` on one side, seven options on the other).
159
+ - **Now.** One table. A write target is a word a row's role writes (`path` with effect `write`, `create` or `edit-in-place`, at the row's seats), read under each implementation; "is this call only a read" is derived from the row (an option whose effect writes or runs a command, a writing or edit-in-place role, a follow mode, a stdin position, a byte bound) and stored nowhere; `SHELL_WRAPPER_TABLE` is the projection of the launcher rows, written out with its literal type and pinned equal to them (`test/shell-wrapper-table-projection.test.ts`), and `VARIABLE_OPERAND_BUILTINS` the rows whose operand binds a name. The retired tables are kept out of `src/` by name (`test/program-read-model.structure.test.ts`); the schema's keys are pinned, so a stored "read-only" column is red. Every difference the move makes on the 4 252 corpus commands it changes is an entry of `test/fixtures/program-read-model-equivalence-1102c.allowed.json` under a class that names the old and new reading, its direction and the gate cell or ruling that decides it (49 classes, two of them car C2's); car A's pin carries the 975 readings car C moved there (seven classes) and the 12 car C2 moved (one class). What changed:
160
+ - **Launchers read their declared options (K-11).** Before: an option the second lexer's table carried but the station's row did not (`sudo -u`, `strace -o`, `xargs -0`, `nsenter --preserve-credentials`, `env -`, …) refused at the program position — the station declined and handed every word over, the read-only face refused with the option sentence. Now: the walk reads the program behind every option the launcher row declares (binary-witnessed; declared where the gate cannot run the program — privileges, a namespace, a sandbox). The station judges the program's own operands (an in-root read is vouched where it was the ordinary ask — `sudo -u root cat src/index.ts`; a stream into a launcher that runs a shell is a mandated re-entry — `… | nsenter --preserve-credentials`); the read-only face refuses with the allowlist or stream sentence (a deployment that lists `xargs` now allows `xargs -0 wc -l` as it allowed `xargs wc -l`: 113 commands under a wide reader list); a launcher option whose value names a path (`xargs -a FILE`, `strace -o FILE`, `strace -oFILE`, `doas -C FILE`) hands that value over whole, by its role, on both faces (a file named `a=b` or `-x` is that file), wherever the option stands in the chain. The second lexer peels the 19 launchers and shells only that table lacked (`busybox`, `su`, `runuser`, `chroot`, `setpriv`, `numactl`, `prlimit`, `systemd-run`, `valgrind`, `firejail`, `bwrap`, `ash`, `mksh`, `yash`, `pdksh`, `parallel`, `alias`, `mapfile`, `readarray`) — 1 777 commands peel a layer more or stop at an unreadable word; `env -0` / `--null` and `--block-signal` (no implementation runs a command beside the first two; the third is not in the gate host's env) and strace's repeated-letter spellings leave its table. `nocorrect` (zsh's precommand modifier, a launcher in that table) is a launcher row, so it joins `BARE_INTERPRETER_NAMES`.
161
+ - **cp / mv by their rows.** BSD stops reading options at the first operand (a later `--` or `-x` is a source — a child of a directory destination: 327 commands write more), an option's value is no file (`--suffix x`, `--sparse=x`: 45 commands write fewer — each spelling, attached form and prefix witnessed writing nothing at the value word), a `-t` destination is an option value.
162
+ - **sed by its row; K-9 / #1147.** A word only one implementation edits in place is written only where the filesystem confirms it exists (`sed -i '' 1d f`: `1d` is GNU's file and BSD's script — written exactly when `1d` is there; a lexical establishment keeps it). A script or a suffix word is never written; a backup is written only by a reading that edits (`sed -i 1d f` no longer writes `f1d`). 134 commands write fewer words, 84 more (a `--follow-symlinks` backup named from where the operand lands, a backup no grammar named).
163
+ - **New write targets.** curl's writing options (`-o`, `--output`, `-c`, `--cookie-jar`, `-D`, …: 43 commands), fish's `-o` / `-p` output files, flock's lock file (created where it is not there: 213 commands), a path-prefixed `time -o FILE`, the command `complete -C` stores (a re-entry).
164
+ - **Fewer read candidates.** find's predicates are dash words (`-exec` no longer offers `xec` / `ec` / `c`: 41 commands); the launcher's own value words that name no path (`sudo -u root`, `timeout 5`) and the program word are no candidates (949 commands).
165
+ - **A call that writes a word it is handed is no read.** Before: only an option's effect made a call more than a read, so a reader list naming a writing program let `cp a b`, `mv a b`, `mkdir d`, `rm x`, `rmdir d`, `tee f`, `touch t` and `flock <lock> cmd` through as read-only. Now: the read-only face reads the write face's own reading of the call (`writeReadings`, every implementation) — a word in a writing role, or an operand of a writing program where a reading cannot place an option, refuses with "`<program>` writes a file it is handed — not a read"; a call handed nothing to write (`cp --help`, `rm --version`, a bare `tee`) is unchanged (pin `test/backlog1102-read-only-face-writes.test.ts`; none of the corpus's reader lists names a writing program, so the equivalence pins see no difference).
166
+ - **The read-only face, row by row.** sed's read-only form is derived (`-n` plus a print-only script, with the switches the gate witnesses writing and running nothing — `-u`, `-a`, `-H`, `-E`, … now allowed with `-n`: 12 commands); tail's follow reading is the row's (a spelling no implementation follows terminates — 20 commands allowed, `tail -b 5` asks); diff's `--context` / `--unified` take their value only attached and `-D` / `--ifdef` take the next word (11 allowed, 7 refused); head's byte bound accepts its GNU abbreviation (`--byte=5`); a fused `--opt=-` the row reads as a file is the stdin request; `free --sec` repeats; under a deployment that lists them, `date --set`, `tar --files-from`, curl's writing options and egrep / fgrep's stdin floor refuse (89 commands); an interpreter option the row does not explain leaves the program word possibly the program text (`mawk -V {print} f`: 101 commands refuse with the interpreter sentence instead of allowing or the allowlist sentence).
167
+ - **Rows the Linux gate falsified on its fourth run (car C2).** find is two grammars, GNU findutils and BSD find, each its measured departures from their union (`find --help` on findutils 4.9.0; `man find` and a run of every declared predicate on macOS). Before: one grammar carried BSD's `-f PATH` beside GNU's predicates, so `-fprint` / `-fprint0` / `-fls` / `-fprintf FILE` read as `-f` taking the rest of the token and the write face judged nothing — `find . -fprint /o` wrote `/o` unjudged. Now: under GNU's grammar (no `-f`, `-s` or `-x`) the predicate's value is a write target at every seat (18 corpus commands, class `find-gnu-writers/write/tighter`); BSD's keeps the `-f` reading — it has none of those predicates, nor `-context`, `-printf`, `-regextype`, `-used`, `-xtype`, `-files0-from`, `-daystart`, `-executable`, `-readable`, `-writable`, `-D`, `-O` or a long option. GNU's `-samefile` opens its file (findutils holds it open against the inode being reused) and BSD's only stats it — the read face reads the value as a checked path either way; the gate now predicts the open per implementation. The birth-time comparisons are `-newerBt` / `-newerBa` / `-newerBm` / `-newerBc` in both: the lower-case spellings the row declared are no predicate in either and now read the speculative way (12 readings of car A's pin, 16 commands of car C's; class `find-newer-birth/write/tighter`), and a `-newerB<y> FILE` reads FILE as the value it names (the predicate's own letters are no longer offered as candidates; no corpus command spells it). GNU tail reads a `+`-led traditional first word too: on a build for POSIX 1003.1-2008 (glibc) `tail +0f F1` follows, on a build for 2001 (macOS) `+0f` is a file — Before: the GNU grammar stated `-` only; Now: both signs (the read-only face already refused `tail +0f` through BSD's reading: no corpus command changes). The gate asks a host only what its build can witness: an option a build predates runs no cell there (`SINCE`, read off the binary's own `--version`; `env -a` / `--argv0` arrived in coreutils 9.5 and the Linux host carries 9.1 — a `--version` that names no release is a failure, never a skip); the GNU `+0f` termination cell follows only where the host's build reads it so (`getconf _POSIX_VERSION`); `setsid -c` / `--ctty` (sets a controlling terminal the gate's child does not have), `find -fprintf` (two words, which no arity states) and `flock --command=` (flock matches its command word whole) run no cell; flock's command-line cells carry the lock file in front — `flock FILE -c CMD` is the only form flock accepts (`-c` before the file is an invalid option, exit 64), which the old cells ran.
168
+ - **Rows the gate falsified** (each runs on the gate; macOS cells where the binary exists, the Linux lane for GNU): BSD sed's `-i` takes the NEXT word whatever it is spelled (`-i -e p f` backs up to `f-e`; `-i p f` makes `f` the script — the write face had lost the backup) · BSD rm / rmdir / touch take no long option · GNU env has no `--block-signal` on the gate host · BSD `tail +0f` follows, GNU reads `+0f` as a file · GNU grep's `--context` requires its value · GNU egrep / fgrep refuse every other matcher option (`egrep -F`, `fgrep -E`, `--perl-regexp`, …) · GNU diff and cmp read `-v` as their version banner (no file opened) · mawk and the one true awk take only POSIX awk's `-f` / `-F` / `-v` (no `-e`, `-E`, `--source`, `--file`, `--exec`) · node / nodejs `-r` / `--require` / `--import` / `--loader` / `--experimental-loader` load a module FILE before the program · node's `--inspect-brk` / `--watch` and grep's `--exclude-from` (GNU opens the file twice) and `env -P` run no cell (`NOT_RUN`, each with its reason) · a GNU binary is found under its dialect name (`gawk`, `g<name>`) and a GNU program the Linux host lacks (`awk · gnu`, `test · gnu`) is a listed absence, not a failure. Before / Now for each: the row read the old spelling; the binary refused it, opened nothing, or opened a file the row did not name.
169
+ - **Who is hurt.** A deployment that put a writing program on its read-only reader list (`cp`, `mv`, `mkdir`, `rm`, `rmdir`, `tee`, `touch`, `flock`): those calls now get a card — the list declares read-only programs; releasing a writing program is an allow rule's seat. A deployment that relied on a launcher option refusing at the program position (`sudo -u x cat f`, `strace -o t cmd`) to raise the full shell's ordinary question: the station now judges the program's reads (the classify face still refuses every launcher that is not on the reader list; bypass and accept-edits release the same questions as before). A new allow rule `Bash(nocorrect …:*)` is refused as an interpreter prefix (`invalid.bare_interpreter_prefix`) where it was accepted. A row of that shape already stored keeps its standing as every row does when the interpreter set grows: the durable partition's rows still admit at the gate (the matcher does not re-validate), a sync round moves it to the quarantine with the disclosed reason `invalid_rule_text`, and the session partition drops it with its disclosure (a row that does not survive the canonical re-projection). Probes that pinned the old readings: `xargs -a F cat` declines as `stream_fed_operands` (was `unreadable_program_position`); `env - cat f` reads `cat`; `sed -i 's/a/b/' f` writes `f` only where it exists. A run whose command writes through GNU find's `-fprint` / `-fprint0` / `-fls` / `-fprintf FILE` (car C2): FILE is now judged as a write — a card or a refusal where it lands outside the root or under a write deny, where the call ran unjudged.
170
+ - **Compensation.** None for a reading the binary witnessed. A deployment that wants a question on every launcher keeps the launcher off its reader list (the read-only face refuses it) or writes an ask rule on the launcher.
171
+ - **Consumers.** server — `judgeShellRead` and the gate answer as above for launcher, cp / mv / sed and curl commands; `SHELL_WRAPPER_TABLE` (a package export) has 19 more keys and its rows change shape for `env` (`-0`, `--null`, `--block-signal` gone), `strace`, `ltrace` and `nsenter` (`-r` / `-w` attached-only); `BARE_INTERPRETER_NAMES` +`nocorrect`. cli — `verify/_bash-gate-danger-matrix.mjs` rows for `sudo -u …`, `strace -o …`, `xargs -0 …` and `sed -i …` read the new answers; the rule card never offers a `nocorrect` prefix. test — every class of the two allowed files is a replayable cell (write targets with and without the in-place file, read-only verdicts under the default and the wide reader list).
172
+ - **Known limits.** KL-1102-c (the Linux lane runs the GNU cells; car C fixed its first eleven red groups) · d (130 option values still read speculatively, was 134) · f (`NOT_RUN` +`--exclude-from` ×3, `env -P`, the `nodejs` twins of node's three) · i (`xargs -a` declines with the standard-input sentence) · j (BSD sed's separated `-i` suffix is read two ways — the read face narrows it to suffix spellings; ticket) · k (a cp / mv backup file is no write target) · l (tar's archive and extracted members have no write role) · m (car C2: `find -fprintf FILE FORMAT` — the format word is read as an operand, a read candidate; the gate does not run it).
173
+ - Pins: `test/program-read-model.equivalence-1102c.test.ts` (four faces over car A's corpus and every spelling the moved tables declared), `test/backlog1147-sed-in-place-dialects.test.ts` (BSD / GNU × the in-place word's existence, real temporary files), `test/shell-wrapper-table-projection.test.ts`, `test/program-read-model.binary-gate.test.ts` (write cells read off the directory's own state, never the exit status — self-pinned; stdin, termination and byte-bound cells), `test/program-read-model.structure.test.ts` (car C's retired names; the schema's keys), `test/backlog1102c2-host-truths.test.ts` (car C2: find's two grammars, GNU tail's `+N`, the gate's `SINCE` / host POSIX reading / `NOT_RUN` rows, flock's command-line cell); re-pinned with their new readings: `backlog780-program-position-reader`, `backlog780-table-evidence` (`nocorrect`), `backlog502-out-of-root-read-mandate`, `backlog696-read-boundary-any-doctrine`, `backlog891-seat-keyed-declines`, `backlog945-plan` (`xargs -a`), `backlog1132-deny-tables` (`flock` writes), `permission-rule-model` (`nocorrect`), `backlog994-s1-dual-run` (four attributions).
174
+
175
+ ### The removal guard reads `rm` / `rmdir` by their rows: one reading of which words a removal removes (#1403 × #1102 car C; behavior) (@server @cli @test)
176
+
177
+ - **Before (7.45.0).** The dangerous-removal guard (`dangerous_delete` / `unverifiable_delete`, `src/tools/fs/removal-targets.ts`) cut an `rm` / `rmdir` argv with a grammar of its own (every dash word an option, read anywhere as GNU getopt permutes) and named the recursive and forced options by their spellings — beside the program read model's `rm` row, which the write face reads. rm's `-f` / `--force` were in no row, so `rm -rf x` offered the letter `f` as a read and a write candidate.
178
+ - **Now.** The guard reads the rows. A removed word is one some implementation's write reading of the row writes (`writeReadings`); every other word is an option word and is never a target. A recursive forced removal is one implementation selecting the row's traversal option and its forced option (`selectedOptions`, `--` ending the options): `rm` declares `-f` and GNU's `--force` (`OptionSpec.mode` gains `forced`). Over the 364 guard pins and the 36-row lane matrix the answers are the 7.45.0 answers; over 8,312 (command, seat) pairs that run a removal (the shell-reading corpora and the guard's own pins, four seats) the findings differ in two shapes only:
179
+ - **A dash word past the first operand is also a target** (BSD rm stops reading options at the first operand: `/bin/rm a -v` removes `a` and the file `-v`). With the working directory at `/` that word is a top-level removal: `rm /tmp/x -v` at `/` asks `dangerous_delete` naming `/-v` (narrowing; 327 pairs, all at `/`, 322 of them already asked about another target and now name the dash word first). The write face read that word as written already.
180
+ - **A cluster both rm's refuse selects nothing:** `rm -r.f "$D"` / `rm -r-f "$D"` (an illegal option to GNU and BSD rm alike — nothing is removed) is no longer an unverifiable recursive forced removal (4 pairs); it gets the read station's `probe` question.
181
+ - **Fewer candidates:** a cluster made of rm's switches only (`-rf`, `-fr`, `-Rf`) offers no letter as a candidate (78 corpus commands on the plan, 86 on the write face, one candidate fewer each).
182
+ - **Who is hurt.** A task running at `/` on a host with GNU rm that writes an option after an operand (`rm /tmp/x -f`): GNU reads it as an option, the guard asks (KL-1403-c). A deny row on `./f` no longer refuses `rm -rf x`, which never touches `./f`.
183
+ - **Compensation.** Write options before operands; the card names the word it read as a target.
184
+ - **Consumers.** server — `judgeShellRead` and the gate answer as above; no wire shape changes. cli — the cards are the 7.45.0 cards; at `/` the named target may be the dash word. test — the binary gate's rm write cells `-f an operand`, `--force an operand` (GNU), `-rf a tree` (exact: the tree and nothing else is removed — a file `f` beside it stays).
185
+ - **Known limits.** KL-1403-c (a dash word past an operand at `/` on a GNU host).
186
+ - Pins: `test/removal-targets-read-model.test.ts` (R1–R6), `test/backlog1403-dangerous-delete.test.ts` (364, unchanged), `test/program-read-model.binary-gate.test.ts` (the rm write cells), `test/program-read-model.equivalence.test.ts` / `-1102c.test.ts` (class `rm-forced-switch`).
187
+
188
+ ### A shell handed no script runs its standard input: what arrives there is its script (#1417; behavior narrowing) (@server @cli @test)
189
+
190
+ - **Before (v7.45.x, and 7.46.0 before this change).** A shell whose argv names no script (`bash`, `sh -s`, `zsh -`) reads its commands from its standard input, but the plan read that input as data: `bash <<'EOF'` with `rm -rf ~` in its body, `echo 'rm -rf ~' | bash`, `curl … | sh`, `bash < x.sh` were a shell re-entry with no text — a `probe` question under `default` and under an allow, **released with no card by `bypassPermissions`**; a here-document reading a deny-listed file (`bash <<'EOF'` / `cat ~/.ssh/id_rsa`) asked instead of being refused (KL-1403-a).
191
+ - **Now.** One rule, three sources, the two exits a `-c` text already had. The program read model says which re-entry programs run their standard input when no script is named (`reentry.stdin: "script"`: every shell row and `parallel`; not `eval`, `source`, `.`) and which option selects it (a shell's `-s`, `OptionSpec.mode: "interactive"`); `reentryText` reads it — no `-c` text and no script-file operand, or `-s`, or a lone `-`, or an option the row does not explain in front of the operand. What arrives there:
192
+ - **a here-document or here-string** — a quoted body, an unquoted body with no unescaped `$` or backquote (`\$`, `` \` ``, `\\` are read as the shell reads them), a here-string whose word is literal: the text is a **nested plan**, as `bash -c '…'`'s is (its reads, writes and removals are judged where a `-c` text's are); an unquoted body that expands, a here-string that expands: a **script built when the command runs**;
193
+ - **the pipe** — the text a literal `echo` / `printf` writes (the rows' new `prints`: `echo`'s operands after `-n` / `-e` / `-E`, `printf`'s one format; a backslash, a `%` directive, a value to fill, a word that expands or an option the row does not declare leaves it unread) is a nested plan; any other program on the pipe (`cat x.sh |`, `curl … |`, `tee … |`) is a script built when the command runs;
194
+ - **a `<` file or a `<&N` descriptor** — a script built when the command runs (the file is still the read candidate it was). A `<` file, a `<&N` descriptor or a here-string replaces the pipe on standard input (a here-document's descriptor is not read, so it does not); a shell given `-s` takes its operands as parameters, not as a script file (one reading of where the script comes from: `bash -s <file> <<<'…'` no longer reads `<file>`); a script file that names standard input (`/dev/stdin`, `/dev/fd/0`, `/proc/<pid>/fd/0`, folded lexically — `/dev/./stdin`, `/dev//fd/0`) is standard input (`echo … | bash /dev/stdin`, `source /dev/stdin <<EOF`).
195
+ A script built when the command runs is the removal guard's `runtime_script` arm: `dangerous_delete` under every mode and allow member, the card naming the source as written. A shell with a `-c` text or a script file reads its standard input as data, as before; a program that runs no shell text (`cat <<EOF`, `grep x <<EOF`, `python3 <<EOF`) is read as before.
196
+ - **A POSIX shell's `+` words are options.** Before: `bash +x -c 'rm -rf ~'` read `+x` as the script file, so the `-c` text was never planned (`bypassPermissions` released it), and `bash -o pipefail x.sh` read `pipefail` as the script file. Now: the POSIX-family shell grammars (sh, bash, dash, ksh, zsh, ash, mksh, pdksh, yash; not csh / tcsh / fish) read a `+` word as its `-` option (`Grammar.plusOptions`: `+x`, `+o NAME`), and the script file is the row walk's first operand (`scriptFileAt`) — `bash +x x.sh` and `bash -o pipefail x.sh` name `x.sh`.
197
+ - **A launcher that runs a shell with no program hands it nothing; the shell runs its standard input.** Before: `sudo -s` / `sudo -i` with no program read as a chain that runs nothing (an `ordinary` question a `Bash` allow cleared — `sudo -s <<'EOF'` holding `rm -rf ~` ran with no card under an allow and under `bypassPermissions`); `su`, `chroot DIR`, `pkexec`, `unshare`, `nsenter`, `firejail` with no program were a shell re-entry whose launcher words (`chroot`'s root, an option's value) were read as the shell's text, and its standard input as data. Now: a launcher whose row runs a shell where no program follows (`whenNoProgram: "shell"`), or whose selected option does (`sudo -s` / `-i` / `--shell` / `--login`, `doas -s` — the rows' `shells`, `OptionSpec.mode: "interactive"`), hands that shell nothing; a text-handing launcher (`su USER …`, `watch`, `script`) hands it its operands as before. The segment's program is that shell (`sh`), so a shell handed nothing runs its standard input by the same rule — a literal fed script is planned, any other is `dangerous_delete`. `sudo` takes no `-c` (`sudo -s -c '…'` is refused by sudo and read as before); `sudo ls <<'EOF'` reads its input as `ls`'s data.
198
+ - **Loosenings (open to veto).** Two readings judge fewer candidates, each because the word is no script and no command line: an option's value (`bash -o NAME`, fish's `-C` / `-d` / `-f` values) or an operand after `-s` (a positional parameter) is no longer read as a shell's script file — in the read-model corpus 59 commands judge one write-face candidate fewer and 15 judge another one instead (`bash -o V rm -r x` names `rm`, not `V`); and a launcher's own words (`chroot`'s root, `nsenter` / `unshare` / `pkexec` option values) are no longer planned as its shell's command line — 19 corpus commands no longer judge the removal that command line spelled (`chroot 'rm -r x'` never runs `rm`).
199
+ - **Who is hurt.** A deployment under `bypassPermissions` (or an allow) whose agents feed scripts to a shell on its standard input: a removal of a protected location in a fed script, and every fed script the command does not spell — `curl … | sh`, `cat x.sh | bash`, `bash < x.sh`, `cat <<'EOF' | bash` (the cat on the pipe is a program other than a printer), an expanding here-document, `printf '…\n' | sh` — now carry the mandated `dangerous_delete` card (no mode or allow clears it; headless ⇒ refused). A fed script reading a deny-listed path is refused. `bash x.sh` (a script file named) is unchanged — it does not ask where `bash < x.sh` now does.
200
+ - **Compensation.** Feed the shell a quoted here-document, or put the text in `bash -c '…'`; name a script file (`bash x.sh`) rather than redirecting it; a person confirms the card.
201
+ - **Consumers.** server — `judgeShellRead` and the gate answer as above; `ShellReadSegment` (a package type) gains one optional member, `stdin` (`{ from, literal? }[]`, a shell re-entry's standard-input sources). cli — the `dangerous_delete` card's sentence may name a standard-input source (`` `bash` runs a script built only when the command runs (<<source>) …``). test — the three sources × the two exits, and the controls.
202
+ - **Not covered.** A shell whose standard input is inherited from an enclosing command — a group's later member (`echo … | { true; bash; }`), a `-c` text's shell (`bash -c 'bash' <<EOF`) — and a script file that is a process substitution (`bash <(echo …)`) are not read as fed scripts; `su USER` reads the user name as the shell's text, so `su root <<EOF` is not read as a fed script (KL-1417-b); a command after `sudo -s` / `-i` is re-read by the shell (`sudo -s rm -rf '$HOME'` expands `$HOME` there; KL-1417-a); `xargs … bash -c '{}'` runs text from its input; a here-document's descriptor is not read (`bash 3<<EOF …` is read as feeding the shell — an over-read).
203
+ - Pins: `test/backlog1417-stdin-script.test.ts` (① – ⑭: three arms × two faces, the deny refusal, the controls, the `-c` equivalence, `+` options, the launcher shells, one reading of the script source, the standard-input paths; 232 cells), `test/program-read-model.binary-gate.test.ts` (each shell: standard input run with no operand, with `-s` and an operand, and as data beside a script file; each POSIX shell's `+x -c` text), `test/program-read-model.equivalence.test.ts` / `-1102c.test.ts` (classes `1417-*`), re-pinned `test/backlog945-plan.test.ts` (`sh <<< S` is a script); `test/backlog1403-dangerous-delete.test.ts` unchanged.
204
+
205
+ ### The read station reads what a word is, binds the names a text binds, and asks only where it guards something (#1102 car B, #1101, #1113, #923, #1017, #795, #1397; behavior; one loosening open to veto) (@server @cli @test)
206
+
207
+ - **Before (v7.45.x, and 7.46.0 cars A / C).** Where the program read model had nothing to say, three fallback rules answered. **A parenthesis rule:** a word carrying `(`, `)` or a backquote was a word the shell rewrites, quote-blind — so a jq filter (`jq '.a | map(.b)'`), an awk program, a prompt (`claude -p "fix (1)"`) and a single-quoted file name (`cat '`id`'`) left the command unestablishable (a mandated card), while an inline program WITHOUT those characters (`perl -e 'print 1'`, `ruby -e '…'`, a here-document `python3 - <<'PY'`) ran with no card. **A verb-blind rule:** every word of a program with no row (and of a rowed program the deployment did not list) was a read candidate — a non-path word was spelled `<root>/<word>` into a refusal (#1017), every word naming nothing read as a read outside the roots, and `docker -v ~/.aws:/r` was one word (`~/.aws:/r`) that matched nothing (#1113). **A HOME spelling rule:** a text spelling the name `HOME` anywhere but as a plain read withheld the declared home from every home word (`echo HOME; cat ~/x` asked), while a variable the same text set (`F=notes.txt; cat "$F"`, `for f in a b; do cat "$f"; done`) was never read through. And the station asked "could not say where this reads" whatever the boundary guarded: an open face with no deny row — nothing to protect — still raised the mandated card (29 of 29 such cards in the reference probe session).
208
+ - **Now.** The three rules are gone (0 sites), each answered by a rule of the design:
209
+ - **Program text is a slot** (the parenthesis rule's answer). A word in a program-text role is never a containment candidate; it leaves the command unestablishable exactly where its language may open a file — any spelling of the any-language interpreters (perl, ruby, python, node, php, lua, tcl, sqlite, psql, Rscript, julia, …), a program on standard input, a here-document program, and the token languages only when the text carries an opening word (jq `import` / `include`; awk `getline`, `system`, `ARGV` / `ARGC`, a call's `(`, a pipe `|`, `@`). Text in a language that may open files is also a deny-only spelling (a deny row it names refuses). Option values and program files are read by role in every language (a program file is opened; `python -m timeit`'s tail is program text, K-7). A quoted parenthesis or backquote is the literal it spells (`cat '`id`'` reads the file so named). **The arithmetic evaluator** is its own slot (K-8): a substitution in a subscript the shell evaluates — an assignment's, a `${a[…]}` expansion's, a binding builtin's NAME (`read`, `unset`, `declare`, `local`, `typeset`, `printf -v`, `test -v`, `wait -p`), `let` / `(( ))` / `[[ … -eq … ]]` operands, a `${x:off:len}` offset — quoted or not, is a nested plan the segment re-enters, and so is one in the value of a variable the text bound (`declare -i`, and bash's own integer variables: `OPTIND`, `RANDOM`, `LINENO`, `HISTCMD`, `BASHPID`, …, the reference implementation's table read whole), through any chain of names (a cycle cut, a chain past the reader's budget re-entered whole) and an array's literal elements; a literal `$((…))` body is no shell text and names no path (`$((cat ~/.env))` reads nothing — bash refuses it as arithmetic). `export` / `readonly` / `getopts` take a NAME as written (no subscript runs: bash 3.2 and 5.3).
210
+ - **A word of a program with no row is a speculative read** (the verb-blind rule's answer, K-4 A, §2.5 ⑤). Its payloads (`-x=V`, `-xV`, `NAME=V`'s value) always meet the deny rows; the word is a containment candidate only where the filesystem confirms it exists (after glob expansion) and the text writes or creates it nowhere — a known writer, a redirection, or another program with no row handed the same spelling (`ReadCandidate.speculative`). A role only another implementation reads is read the same way. A rowed program the deployment does not list is read by its row (option values by role, the script slot, the subcommand). `git grep` walks the tree it searches, `git show <rev>:<path>` / `git cat-file -p` / `git archive` object paths meet the deny rows (`object_path`, every ancestor of the working directory as a base — the repository top is not in the text, K-5), and `docker` / `podman` `run` / `create` mount sources are reads of the host path (`mount_source`; a volume name is no path, K-6) — and, since a mount hands the container the whole tree under it, traversal roots: a deny row under a mounted directory refuses (`-v ~:/h` mounts `~/.aws`).
211
+ - **The names a text binds are read through one binding thread** (the HOME rule's answer; the same candidate walk as the working directory, K-3 B). A statement or declaration assignment binds its value; a `for` loop over literal words binds the variable to them (a pattern to its matches); a binding inside a branch, a loop or a group that closed — or in an `&&` / `||` run the reader is not part of, a pipeline member, a background job — joins the earlier value; a loop body's bindings are unknown from its first beat; a function body reads the table at its call; `read`, `printf -v`, `mapfile`, `getopts`, `unset`, a name reference, `eval` / `source`, an assigning expansion, a computed name, an attribute change and an unquoted here-document `${` make names unknown; a prefix assignment is the command's environment (its own words expand first). Past a name reference (`declare -n` / `local -n` / `typeset -n`) no assignment is trusted (it may write through the reference), and an attribute that changes what is stored (`-i`, `-l`, `-u`, `-c`, `-a`, `-A`) makes later assignments to that name unknown. `HOME` is one name: unbound it is the declared home; bound, `~` reads the bound value and the declared home's rows still refuse (the floor). A `case` arm's pattern is no command: the arm's command is read (it was invisible — `case x in *) cat <deny row>;; esac` vouched on v7.45.x). The read-only classifier reads the same binding fact.
212
+ - **The station asks only where it guards something** (#1101, K-1 A, K-2 A). One predicate, `stationGuards(boundary)` = a roots face, or a deny table with rows: an unestablished read (and a stream-fed decline, #1397) lands as no question on an open face with no deny row; everywhere else it is the mandated card as before. The deadline fallback reads the same predicate with the write face's deny rows beside it.
213
+ - **The sentences say what is judged.** The deny refusal's closing note now states the reach: "The read boundary judges the paths this command names — as written and, where a filesystem can be consulted, where they resolve and what a pattern among them matches — and refuses a deny-listed one here rather than escalating it; a file the command reaches without naming it is judged only where a segment deny row (built in, or the deployment's own pattern) reaches into the tree a recursive read walks." (KL-1102-a). An unestablished read on the open face says "so it cannot be confirmed not to read a path the deny list names" (it no longer claims a confinement the open face does not have).
214
+ - Measured: the reference probe session (50 shell calls) — open face, no deny rows: 29 cards → 0; open face, built-in deny rows: 29 → 11 (4 whose loop list or value comes from a substitution or `read`, 7 here-document python programs, 6 of them carded for the first time); roots face, no deny rows: 41 → 42 in the lexical projection (15 unestablished cards become established reads outside the roots; the 6 here-document programs join; the lexical projection keeps every speculative word, a filesystem drops the ones that do not exist). The three #1113 forms on a deny-listed world: `none` / `vouch` → `deny_hit` / `refuse`. The 38 home-shadow cells: 36 unchanged, the two array-only cells (`a=([0]=~/.npmrc); echo done`, and the same printing `${a[0]}`) refused → not judged (bash reads nothing there).
215
+ - **Who is hurt.** A deployment on a roots face or with deny rows that runs inline or standard-input interpreter programs (`perl -e`, `python3 -c`, `python3 - <<'PY'`, `node -e`, `tclsh` on a pipe): each is now a mandated card (no stored rule clears it) where it ran with none, or was #862's plain clearable card — `test/backlog791-interpreter-inline-program.test.ts` pins the spread; the engine default (roots face, built-in deny tiers) is such a deployment, an open face with the built-in tiers off is not. A deployment with deny rows whose agents run `git` / `docker` / `podman`: `git grep`, an object path and a mount source naming a deny-listed file, or mounting a directory that holds one, now refuse (the rows are read whether or not the deployment lists the program); a mounted directory past the traversal bound is unestablished, as `grep -r .` is. A text that read an array element where the element spells a deny row refuses (the array assignment alone reads nothing). A text naming a computed program (`s[$(printf h)] -c …`) is unestablished. The rule-reach reader's variable-operand set gains `wait` (`VARIABLE_OPERAND_BUILTINS`). **Loosening, open to veto:** where the boundary guards nothing (open face, no deny rows) an unestablished read no longer raises a card — there is nothing for the card to protect; a speculative word that names nothing on the filesystem is no read; a name the text bound to an in-root value is placed and vouched where it asked.
216
+ - **Compensation.** A deployment that wants the card back on an open face with no deny rows turns a deny tier on (the built-in tiers are the default) or a roots face; an inline program on a guarded deployment is still released by bypass mode and refused under `dontAsk` as every mandated card is. Known limits: the shell reads the value of a variable the text did not bind (an inherited one, a positional parameter set by `set --`) as data (a known-limit candidate of this release); a program word that is a pattern (`s[h] -c …` where the working directory holds `sh`) is #1415.
217
+ - **Consumers.** server — `judgeShellRead` and the gate answer as above; the forced card's text `tool "Bash" reads where the session's read boundary could not establish what is read …` no longer appears on an open face with no deny rows (re-pin a test that expects it there); the deny refusal's closing note changed (re-pin byte-for-byte expectations); `ReadCandidateProvenance` +2 (`mount_source`, `object_path`); `ReadCandidate` gains one optional member (`speculative`). cli — the L-1100 disclosure's "the engine sentence says more" half can go (the sentence now states the reach); `verify/_bash-gate-danger-matrix.mjs` scenarios that expect an open-face unestablished card read no card. test — the probe session replay (three configurations), the knownMiss CR04 / CR06 / CR08 cells (now refused) and the home-shadow cells (④ two cells now not judged).
218
+ - Pins: `test/backlog1102-read-station-semantics.test.ts` (#1101 × two faces × with and without deny rows, #1102 ①–④ and their reverses, the binding rule, the case arm, the subscript rule, #1113 ①–③, #923, #1147, #1017, #795, K-2, K-8, the integer variables, #1397, the two sentences, the symlinked word); re-pinned to the new readings, each cell naming the rule that answers it: `backlog791-interpreter-inline-program` (K-1 / K-2; G-13 added), `backlog7300-shell-home-reassign`, `backlog7300-shell-reentry`, `backlog994-s1-readers`, `backlog994-s1-dual-run` (eight attributions; the `compound_array_elements` row retired), `backlog994-s0-deny-outranks-unresolved`, `backlog1064-7301-read-floor`, `backlog1100-tilde-assignment-deny`, `backlog1111b-read-seats`, `backlog696-read-boundary-any-doctrine`, `backlog714-home-expansion-precise`, `backlog753-whitelist-on-lexer`, `backlog758-unlisted-reader-boundary`, `backlog854-read-face-stdin-scan`, `backlog862b-narrowing-cc-alignment`, `backlog921-single-quoted-expansion`, `backlog939-deny-hit-card-names-pattern`, `backlog945-adversarial-r1`, `backlog945-plan`, `shell-wrapper-table-projection`; the two equivalence pins carry car B's classes (`1102b-*`).
219
+
220
+ ### The engine's built-in tool names are one exported set, read off the tool catalog (#1422; additive; +2 exports) (@cli @server @client-core)
221
+
222
+ - **Before.** A host that wanted a tool face with no engine built-ins had to keep its own list of the engine's tool names to feed `TaskSpec.excludeTools` — and the list could not know a tool only the engine mounts (`Monitor`), so a "no built-ins" face still carried it.
223
+ - **Now.** `ENGINE_BUILTIN_TOOL_NAMES` (package root) is the engine's own tool names, once each, derived from the tool catalog's `source: "builtin"` rows — the hands, the scaffolds, the memory tools, the background-task tools; `isEngineBuiltinToolName(name)` beside it. A host spells `excludeTools: [...ENGINE_BUILTIN_TOOL_NAMES]` and gets a face with no built-ins and every caller / MCP / skill tool kept; a tool the engine adds later lands in the set with the catalog row.
224
+ - **Who is hurt.** Nobody: additive. **Consumers.** cli — `--tools ""` and the complement of `--tools <set>` read this set for their `excludeTools` (L-1120); server / client-core — no runtime change; a mirror of the set is not needed (read it from the package).
225
+ - Pins: `test/backlog1422-engine-builtin-tool-names.test.ts` (① set = catalog builtin names, frozen, once each, includes `Monitor`; ② package root hands the module's own value; ③ predicate; ④ real lane: `excludeTools: [...ENGINE_BUILTIN_TOOL_NAMES]` leaves the model a face with only the caller's tools).
226
+
227
+ ### The host's configuration root: `RunnerDeps.agentConfigDir` — the `agent-config` rows guard the directory a host moved (#1419; behavior narrowing where declared; +1 optional key, +1 option key) (@server @cli @test)
228
+
229
+ - **Before (v7.45.x).** The `agent-config` tier of the built-in read deny table spelled the shell's settings below the literal `~/.sema` (`~/.sema/settings.json`, `~/.sema/settings.local.json`, `~/.sema/engine-data/.env`), anchored at the declared and the account home; a home entry that was a symlink was followed (#1244), a configuration directory a host moved with its own variable was not. With the directory outside `<home>/.sema`, `Read <root>/settings.json` was served — the provider keys and the last-known-good copy landed in the tool result and the session record — and nothing at the write faces named the root either (a new file below it was written with no write-protection question; the deny set's `.sema/…` runs never met a path without a `.sema` segment).
230
+ - **Now.** One seat: `RunnerDeps.agentConfigDir` (an absolute path of an existing directory, resolved by the host — the engine reads no environment variable for it). The rows of the table that open with the `.sema` directory are also placed at the same files below the root, as one more place the row's literal head leads (`readDenyConfigRootLinksOf`; the home spelling stays placed; the refusal names the row as before, so `readDenyBuiltinExclude` removes both places). A root reached through a symlink is guarded at its target too, and an alias of it that the environment's identity seat reports (a bind mount, a firmlink) is read at the root. `~/.sema.*` and the external tools' rows stay anchored at the home. The write faces read the same fact through one helper (`src/core/agent-config-dir.ts`): the always-on write-protection table judges a write below the root as it judges one below `.sema` (same row cited, the data root inside it still passthrough); `createSensitivePathPolicy({ …, agentConfigDir })` guards the root's tail of every `.sema/…` pattern (`settings.json`, `settings.local.json`, `engine-data/.env`, `hooks`, `agents`). A value that is not a string, is empty or relative, or names no existing directory refuses `new Runner` / `createSensitivePathPolicy` with `config.agent_config_dir_invalid`, the sentence naming the seat and the value's shape — it is never read as absent. Rules: +0 (the placement law accepts one more place a row leads; the write faces' twin is the same helper's tail, not a second list). The tier gains one row, `~/.sema/config-lkg-*.json`: the shell's last-known-good resolved configuration (`config-lkg-<bin>.json`, credentials included) is refused beside the settings, below the declared root and at the home alike.
231
+ - **Who is hurt.** A host that declares the key: the model can no longer read the three files below the root (the point), and a write below the root costs the question a write below `.sema` costs (bypass releases it; `dontAsk` and headless refuse it). A host that declares a root that does not exist yet: `new Runner` refuses — create the directory first. A host that wires `createSensitivePathPolicy` and does not pass the root keeps the 7.45.x deny set (the root's files are guarded by the Runner's own question only).
232
+ - **Compensation.** Declare the root once to the Runner and once to `createSensitivePathPolicy`; `readDenyBuiltinExclude: ["~/.sema/settings.json"]` removes both places of a row; `writeProtectedPaths` without the `.sema` row removes both places of the write question.
233
+ - **Consumers.** server — pass the root it resolves from `SEMA_CONFIG_DIR` to `new Runner({ agentConfigDir })` and to `createSensitivePathPolicy({ agentConfigDir })`; no derivation of `readDenyPatterns` is needed (retire the `KL-7340rg-e` compensation). cli — KL0150's retirement condition is the server release that hands the root over. test — the red-first cell: `SEMA_CONFIG_DIR=<tmp>` ⇒ `Read <tmp>/settings.json` refused, `~/.sema/settings.json` still refused.
234
+ - **Known limits.** KL-1419-a (the auto-mode classifier's deny paragraph states the table's rows as spelled, not where a declared root places them) · KL-1419-b (a library-direct `createHandsToolkit` takes the placement the Runner carries; it has no seat of its own for the root).
235
+ - Pins: `test/backlog1419-agent-config-root.test.ts` (P1–P8: the root, no home declared, a root whose own path probe fails or never answers, a Grep over the root, the last-known-good file, the default location, no root declared, the rows that stay at the home, a symlinked root, an aliased root, the six bad values, the two write faces, the shell's read station); `test/public-knob-liveness.test.ts` (the knob's row).
236
+
237
+ ### A read-only name keeps its value: an assignment after the attribute binds nothing the text says (#1425; behavior narrowing) (@server @cli @test)
238
+
239
+ - **Before (7.46.0 before this change; 7.45.0 asked).** The binding thread (#1102 car B) read `F=<outside>; readonly F; export F=./a.txt; cat "$F"` as a read of the in-root `./a.txt`. Bash refuses every assignment to a read-only name — `export` / `declare` / `typeset` / a function's `local` report the error and go on, a plain `F=…` exits — so `$F` still names the outside file. The thread read a bare `readonly F` as "unknown" once, and the next assignment bound the name again. Measured on a real lane: `default` asked `ordinary` alone, **an allow `Bash` rule cleared it with no card**, `bypassPermissions` released `ordinary`; the same for `readonly F=<outside>` / `declare -r F=<outside>` / `typeset -r` / `-rx` / a computed `readonly "$N"` followed by any assignment, for a function body defined before the attribute, for a later loop beat, for a write target (`touch "$F"`), for a removal (`D=/; readonly D; export D=./x; rm -rf "$D"` — zero card under the allow), and for a command substitution after the attribute. Bash's own read-only variables (`SHELLOPTS`, `BASHOPTS`, `BASH_VERSINFO`) were bound from text the same way. A declaration the text does not settle read as one it does: `declare -p F=./a.txt` (display mode — bash assigns nothing) bound `./a.txt`; a computed word (`o=-r; declare $o F`, `o=r; declare -$o F`, `o=-n; declare $o F=G`) and a computed program word (`r=readonly; $r F`, `d=declare; $d -n F=G`, `r=read; $r F <<< …`) bound nothing or made every name unknown for one segment only. A declaration bash refuses still bound its operands — `F=<outside>; declare -rf F=./a.txt; cat "$F"` (bash: `cannot use -f to make functions`), `declare -rZ` / `export -r` / `readonly -i` (an invalid option), bash 3.2's `declare -g` / `-I`, `declare +p`, `local` outside a function body (`can only be used in a function`) — and a declaration's operands were read in turn: `F=<outside>; readonly F=./a.txt G="$F"; cat "$G"` gave G `./a.txt`, where bash expands every operand before the builtin assigns any (G holds the outside file; `declare` / `typeset` / `export` / `local` alike). The other attributes had the same order hole: the integer attribute given after a function body or later in a loop (`f(){ F='a[$(cat <outside>)]'; }; declare -i F; f` — the substitution in the subscript runs), a case change, a name reference — and a command substitution started with none of the attributes around it (7.45.0 missed the integer forms too).
240
+ - **Now.** One rule: a declaration's attribute — integer, a case change, an array, and now **read-only** (`frozen`: the `r` switch of `declare` / `typeset` / `local`, and the `readonly` builtin's own) — holds for every assignment it may RUN before: a later one in the text, one in a function body (the body runs at its call), one in the same loop body (a later beat), one in a command substitution of a later segment (of its own segment too, on a later beat or call); such an assignment binds something other than its text (unknown — bash keeps the old value or exits). The declaration's own `NAME=value` still binds (`readonly F=./a.txt; cat "$F"` reads `./a.txt`), each value expanded against the names as they stood before the declaration (bash expands every operand first; a plain assignment chain `F=a G="$F"` stays sequential) — except where the builtin may assign none of them (`unsettled`: `declare -p` displays, `-f` / `-F` name functions, a letter bash 3.2 refuses — `declare -g` / `-I`, `export -a` — a switch word the builtin's own grammar does not carry, and `local` outside a function body): the operand binds unknown. A switch is accepted only where bash 3.2 and 5.3 both accept it, and the `+` spellings are a table of their own (both bashes take `+a` `+f` `+F` `+i` `+r` `+t` `+x` and assign, `+p` displays; `++`, `+-r`, `+1` and the bash-5-only `+n` `+A` `+c` `+l` `+u` `+g` leave the operands unsettled; `export` / `readonly` take no `+` switch — `export ++ F=v` reports an invalid name and assigns `F`). Each declaration builtin reads its own grammar, measured on bash 3.2 and 5.3 (`export -n` / `-p` and `readonly -n` / `-p` assign); a computed word of a declaration (`$o`, `-$o`) may be any switch and a computed program word (one holding no `/`) any builtin: every attribute and a name reference for every name; a name reference holds by the same order. `SHELLOPTS`, `BASHOPTS`, `BASH_VERSINFO` are names the shell keeps. The rule replaces three text-order comparisons (the attribute check, the integer check, a name reference's reach) with one order predicate.
241
+ - **Who is hurt.** Nobody the 7.45.0 reading did not already ask: the forms above are a `probe` card again under `default` and under an allow `Bash` rule, released by `bypassPermissions` (the class 7.45.0 released); the removal form is the `unverifiable_delete` card 7.45.0 raised. Over-asks (KL-1425-a, pinned as documented cells): **a command whose program word the text does not spell — `PY=echo; $PY hi; F=./a.txt; cat "$F"` — now asks about every later variable read** (the binding side reads the program word without the table and takes it as any builtin; 7.45.0 asked about every such read; the one program-word reading both sides share is #1426); a read-only attribute that ends before the assignment — given in a subshell, a pipeline member, a background job, or to a function's `local` name — is read as holding after it; `readonly -f NAME` (a function name) is read as freezing the variable `NAME`; `declare -g` / `-I`, `declare +n` / `+A` / `+c` / `+l` / `+u` / `+g` and `export -a` / `readonly -a` ask on bash 5 as well (bash 3.2 refuses them).
242
+ - **Compensation.** Write the value with the declaration (`readonly F=./a.txt`), or do not reassign a read-only name; spell the program (`echo hi`, `python3 x.py`) instead of `$PY …` where later reads should stay card-free, or call it by a path (`"$HOME"/bin/tool` names a file, never a builtin). A person confirms the card.
243
+ - **Consumers.** server / cli — re-pin a probe that expected the zero-card reading of a read-only reassignment (none known); test — the red-first cell: `F=<outside>; readonly F; export F=./a.txt; cat "$F"` under `allowedTools: ["Bash"]` ⇒ one `probe` card (7.46.0 before this change: allowed, no card).
244
+ - **Known limits.** KL-1425-a (the over-asks above).
245
+ - Pins: `test/backlog1425-readonly-binding.test.ts` (① the seven forms and a plain assignment × three arms; ② the declaration's own value; ③ a computed name; ④ the swept forms — clusters, a body's `local -r`, two words of one segment, `declare +r`, a loop beat, a body defined before, a branch / `&&` run / pipeline member, `unset` then `export`, POSIX mode, bash's read-only variables, the integer attribute in a body / a loop / a computed name / a command substitution, a case change in a body / a loop, a name reference in a body / a loop / a command substitution, a write target, a substitution of the attribute's own segment on a later beat / call; ⑤ bare `readonly` / `readonly -p`, a fresh `declare +r`, a body's own `local -r`, a plain `export`, another name, a substitution with no attribute before it or in the attribute's own segment run once, a loop's own read-only value, a program word holding `/`; ⑥ `-p`, computed switches, computed program words; ⑦ the documented over-asks of KL-1425-a; ⑧ the declarations bash refuses — `-f` / `-F` with an assignment, a letter outside the builtin's grammar, bash 3.2's letters, `+Z` / `+p`, `local` outside a function — and the operands expanded before the builtin assigns, with their controls: a plain assignment chain, a self-reference, `export -n` / `-p`, `readonly -p` / `-n`, `declare -t` / `-x`, a body's `local`; the `+` spellings — `++`, `+-r`, `-+r`, `+1`, `+rZ`, the bash-5-only letters — and their controls: `+a` `+f` `+i` `+r` `+t` `+x` `+rx`, a bare `+`, `export ++` / `+x`, `readonly +r` / `++`).
246
+
1
247
  ## 7.45.0 — 2026-10-08
2
248
 
3
249
  **#1376 —** hard-breaking: none; closed sets: none (no new code, ask class or release reason — the answer is the existing `rule_unreadable`; its reason clause gains one free-text sentence); exports: none added or removed; `ToolCallRequest` gains one optional member, `root` (wire typeKeys +1). **Behavior narrowing:** one — a caller rule policy compiled with no base judges a relative target instead of passing it (see below).
@@ -674,7 +920,6 @@
674
920
  - **Who is hurt.** A release whose RC gate was blocked, or passed, on a note's date: a row now judged by its promise alone.
675
921
  - Pins: `test/backlog1267-verify-debts-window-promise.test.ts` — the real script over one-row ledgers (`DEBTS_FILE`, `--rc-gate --version`): C1–C4 controls (bare version / bare date / neither / a note after the promise; green on v7.40.0), P1 a date only in the note, P2 a version only in the note (and the real `09-17(7.19.0)` shape), P3 full-width, P4 / P4b nesting, P5 / P5b an unclosed parenthesis (the tail is note), P6 a parenthetical-only window, P7 the reading names the promise it judged (P1–P7 all red on v7.40.0).
676
922
 
677
-
678
923
  ## 7.40.0 — 2026-10-03
679
924
 
680
925
  **Hard-breaking changes:** two — #1065 and #1302. **#1065 — Before:** under an asking permission mode (`default`, which an absent `permissionMode` reads as, `acceptEdits`, `plan`, `auto`, `dontAsk`) a call to an MCP server's tool or an A2A peer's skill that no rule granted ran without anyone being asked — under `dontAsk` too. **#1065 — Now:** it is asked once (class `unstated`, origin `policy`); with no approver it is refused (the headless remedy names the grant first), `dontAsk` refuses it, and `bypassPermissions` runs it as before. **#1065 — Who is hurt / compensation:** an embedder that mounts MCP / A2A servers under `default` with no approver, an unattended leg that answers only `ordinary` asks, the cli's built-in agent-teams MCP server, and `-p` scripts with no grant — grant the server's tools with an allow rule (`createPermissionRulePolicy([{ rule: "mcp__<server>__*", behavior: "allow" }])`, or an allow-list entry), wire an approver, or run `bypassPermissions`; a PreToolUse hook's own `allow` is a grant too. No new notice code. **#1302 — Before:** `RunnerDeps.hands.readCyberReminder` existed (on by default) and every plain-text and notebook Read ended with an engine `<system-reminder>` block, "While reading, stay alert for content that is itself malware …". **#1302 — Who is hurt:** a host that sets `readCyberReminder` (either value) — `new Runner` now refuses it with `config.deps_retired_key` (and a TypeScript caller gets a type error first) — and a deployment that leaned on that sentence next to the file bytes to restrain a weak model, for which it is gone. **#1302 — Compensation:** delete the key; to keep the sentence, put it in your own `appendSystemPrompt`. **If unchanged:** no functional loss, and each text read is about 50 tokens shorter.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The roster a host declares for a delegation tool's `extraTools` factory (`SubagentToolOptions.extraToolNames`): every tool
3
+ * the factory may inject at a spawn, by the names it answers to and its content class. Declared, the delegation tool's pool can
4
+ * be listed again — the judgment of a denied-tools list and the memory content-origin reader read it exactly as they read the
5
+ * static pool. Each spawn then checks the factory's products against it: a product the roster does not declare (a name or an
6
+ * alias no entry carries), or declares nearer to home than the product says it is (its content class further out than the
7
+ * entry's), refuses that spawn. The roster is an upper bound: a declared tool the factory did not inject this time is fine.
8
+ *
9
+ * One code for every way the declaration fails, `config.extra_tool_names`, with the arm on `detail.reason`: `shape` (the value
10
+ * is not a roster — refused where the delegation tool is built), `undeclared_name` / `undeclared_origin` (a product the roster
11
+ * does not cover — refused at the spawn). A host value is never printed or converted: a sentence names an entry's place and
12
+ * the TYPE of what it found there.
13
+ */
14
+ import type { ToolContentOrigin, ToolSpec } from "../core/types.js";
15
+ export declare const EXTRA_TOOL_NAMES_CODE = "config.extra_tool_names";
16
+ /** Which way a declaration failed (`detail.reason`). */
17
+ export type ExtraToolNamesReason = "shape" | "undeclared_name" | "undeclared_origin";
18
+ /** One declared tool, as the delegation tool keeps it (a frozen copy of the host's entry). */
19
+ export interface ExtraToolDeclaration {
20
+ readonly name: string;
21
+ readonly aliases?: readonly string[];
22
+ readonly contentOrigin?: ToolContentOrigin;
23
+ }
24
+ /** The error a failed declaration is refused with: the code, and the arm on `detail.reason`. */
25
+ export declare function extraToolNamesError(message: string, reason: ExtraToolNamesReason): Error & {
26
+ code: string;
27
+ detail: {
28
+ reason: ExtraToolNamesReason;
29
+ };
30
+ };
31
+ /**
32
+ * The host's declaration, read ONCE into frozen copies (a later change to the host's array does not reach the pool), or a
33
+ * refusal (`shape`): a roster on a delegation tool with no factory, a value that is not a list, an entry that is not an object, a
34
+ * name that is not a non-empty string, aliases that are not a list of non-empty strings, a content class outside the vocabulary,
35
+ * or one spelling in two entries.
36
+ */
37
+ export declare function readExtraToolNames(value: unknown, hasFactory: boolean): readonly ExtraToolDeclaration[];
38
+ /**
39
+ * Whether one factory product is a tool the roster declares, as declared — answering to exactly one entry's names, its class no
40
+ * further out than the entry's: undefined when it is, else the arm and the sentence (the delegation tool's name, the product's
41
+ * offending name or class, the roster). A product's content class is its own
42
+ * declaration when that is a vocabulary member, else external — the class an undeclared host tool reads as; an entry that
43
+ * states no class allows any.
44
+ */
45
+ export declare function undeclaredExtraTool(roster: readonly ExtraToolDeclaration[], product: ToolSpec, toolName: string): {
46
+ reason: Exclude<ExtraToolNamesReason, "shape">;
47
+ sentence: string;
48
+ } | undefined;
49
+ /** One row of a delegation tool's pool face (`ToolSpec.agentToolPool`): a tool's names and its declared content class. */
50
+ export interface DelegationPoolRow {
51
+ name: string;
52
+ aliases?: readonly string[];
53
+ contentOrigin?: ToolContentOrigin;
54
+ }
55
+ /**
56
+ * The pool a delegation tool's agent faces narrow FROM (`ToolSpec.agentToolPool`, read by the judgment of a denied-tools list and
57
+ * by the memory content-origin reader): what the tool hands its children — its own captured set, not the parent's roster (the two
58
+ * may differ, and a parent mounting only the delegation tool can still offer children a network tool) — plus the tools its factory
59
+ * is declared to inject. Absent when a factory has no declared roster: that pool does not exist until the call happens, and the
60
+ * readers take an absent pool as unknowable (the memory reader: external). Without an explicit pool the inherited surface is the
61
+ * face — its spawn-time narrowing only removes entries.
62
+ */
63
+ export declare function delegationPoolFace(staticPool: readonly ToolSpec[], hasFactory: boolean, roster: readonly ExtraToolDeclaration[] | undefined): DelegationPoolRow[] | undefined;
64
+ /** The first factory product the roster does not declare (see {@link undeclaredExtraTool}), or undefined when every one is declared. */
65
+ export declare function firstUndeclaredExtraTool(roster: readonly ExtraToolDeclaration[], products: readonly ToolSpec[], toolName: string): ReturnType<typeof undeclaredExtraTool>;