@sema-agent/core 7.33.8 → 7.35.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 (228) hide show
  1. package/CHANGELOG.md +462 -3
  2. package/dist/agents/cross-session-judge.d.ts +16 -7
  3. package/dist/agents/cross-session-judge.js +18 -15
  4. package/dist/agents/peer-session-drain.d.ts +4 -0
  5. package/dist/agents/peer-session-drain.js +5 -2
  6. package/dist/agents/repair-loop.d.ts +1 -1
  7. package/dist/agents/subagent.d.ts +3 -0
  8. package/dist/agents/subagent.js +12 -3
  9. package/dist/brain/anthropic.js +1 -0
  10. package/dist/brain/errors.d.ts +23 -1
  11. package/dist/brain/errors.js +19 -2
  12. package/dist/brain/open-responses.js +1 -0
  13. package/dist/brain/openai.js +1 -0
  14. package/dist/brain/status-sink.d.ts +7 -1
  15. package/dist/brain/stream-engine.d.ts +3 -0
  16. package/dist/brain/stream-engine.js +2 -1
  17. package/dist/brain/stream-shared.d.ts +3 -1
  18. package/dist/brain/stream-shared.js +16 -4
  19. package/dist/core/a2a.d.ts +3 -1
  20. package/dist/core/a2a.js +1 -1
  21. package/dist/core/ask-origin.d.ts +379 -154
  22. package/dist/core/ask-origin.js +224 -45
  23. package/dist/core/ask-question.js +2 -1
  24. package/dist/core/ask-unresolvable-notice.d.ts +5 -2
  25. package/dist/core/ask-unresolvable-notice.js +4 -3
  26. package/dist/core/auto-mode-arming.d.ts +17 -4
  27. package/dist/core/auto-mode-arming.js +31 -6
  28. package/dist/core/auto-mode-defaults.d.ts +16 -7
  29. package/dist/core/auto-mode-defaults.js +2 -1
  30. package/dist/core/auto-mode-directive.d.ts +18 -30
  31. package/dist/core/auto-mode-directive.js +9 -18
  32. package/dist/core/auto-mode-prompt.d.ts +62 -1
  33. package/dist/core/auto-mode-prompt.js +42 -4
  34. package/dist/core/auto-mode-rebuild.d.ts +2 -1
  35. package/dist/core/auto-mode-rebuild.js +1 -0
  36. package/dist/core/auto-mode.d.ts +32 -3
  37. package/dist/core/auto-mode.js +80 -41
  38. package/dist/core/checkpoint-store.d.ts +54 -35
  39. package/dist/core/checkpoint-store.js +58 -3
  40. package/dist/core/child-clamp-seats.js +1 -0
  41. package/dist/core/classifier-unavailable-arm.d.ts +57 -0
  42. package/dist/core/classifier-unavailable-arm.js +17 -0
  43. package/dist/core/effective-path-target.d.ts +12 -0
  44. package/dist/core/effective-path-target.js +18 -7
  45. package/dist/core/engine-notice.d.ts +53 -2
  46. package/dist/core/fs-write-gate-policy.d.ts +5 -1
  47. package/dist/core/fs-write-gate-policy.js +4 -1
  48. package/dist/core/gate-fold.js +19 -3
  49. package/dist/core/gate-lanes.js +116 -50
  50. package/dist/core/gate-outcome.d.ts +40 -7
  51. package/dist/core/gate-outcome.js +26 -4
  52. package/dist/core/governance-codes.d.ts +18 -2
  53. package/dist/core/governance-codes.js +23 -1
  54. package/dist/core/hooks.d.ts +53 -29
  55. package/dist/core/hooks.js +1 -3
  56. package/dist/core/mcp.js +1 -1
  57. package/dist/core/message-utils.d.ts +2 -1
  58. package/dist/core/message-utils.js +2 -1
  59. package/dist/core/mode-policy.d.ts +185 -0
  60. package/dist/core/mode-policy.js +184 -0
  61. package/dist/core/oracle-isolation.d.ts +3 -3
  62. package/dist/core/permission-rule-model.d.ts +20 -3
  63. package/dist/core/permission-rule-model.js +40 -3
  64. package/dist/core/permission-rule-provider.d.ts +22 -11
  65. package/dist/core/permission-rule-provider.js +17 -7
  66. package/dist/core/permission-rules.d.ts +23 -1
  67. package/dist/core/permission-rules.js +56 -9
  68. package/dist/core/persisted-rule-arms.d.ts +14 -10
  69. package/dist/core/persisted-rule-arms.js +13 -6
  70. package/dist/core/present-plan-tool.d.ts +1 -1
  71. package/dist/core/present-plan-tool.js +3 -3
  72. package/dist/core/read-only-shell.d.ts +19 -19
  73. package/dist/core/read-only-shell.js +38 -12
  74. package/dist/core/retired-keys.d.ts +9 -3
  75. package/dist/core/retired-keys.js +5 -1
  76. package/dist/core/runner/active-skill-scope.js +5 -4
  77. package/dist/core/runner/assemble-result.js +2 -2
  78. package/dist/core/runner/classifier-ladder.d.ts +43 -1
  79. package/dist/core/runner/classifier-ladder.js +29 -0
  80. package/dist/core/runner/contracts.d.ts +62 -38
  81. package/dist/core/runner/denial-limit-arms.d.ts +35 -7
  82. package/dist/core/runner/denial-limit-arms.js +15 -9
  83. package/dist/core/runner/env-capability-census.js +1 -1
  84. package/dist/core/runner/file-history-request.d.ts +26 -0
  85. package/dist/core/runner/file-history-request.js +90 -0
  86. package/dist/core/runner/gate-exit.d.ts +30 -4
  87. package/dist/core/runner/gate-exit.js +28 -7
  88. package/dist/core/runner/permission-rule-lanes.d.ts +11 -6
  89. package/dist/core/runner/permission-rule-lanes.js +55 -48
  90. package/dist/core/runner/persisted-path-rows.d.ts +64 -0
  91. package/dist/core/runner/persisted-path-rows.js +89 -0
  92. package/dist/core/runner/prepare-artifact.d.ts +5 -1
  93. package/dist/core/runner/prepare-artifact.js +9 -3
  94. package/dist/core/runner/prepare-ask-lane.d.ts +6 -4
  95. package/dist/core/runner/prepare-ask-lane.js +3 -3
  96. package/dist/core/runner/prepare-caps-and-workflow.d.ts +39 -13
  97. package/dist/core/runner/prepare-caps-and-workflow.js +58 -29
  98. package/dist/core/runner/prepare-file-history.d.ts +9 -3
  99. package/dist/core/runner/prepare-file-history.js +4 -90
  100. package/dist/core/runner/prepare-gate-stations.d.ts +37 -9
  101. package/dist/core/runner/prepare-gate-stations.js +46 -29
  102. package/dist/core/runner/prepare-hands-readface.d.ts +40 -17
  103. package/dist/core/runner/prepare-hands-readface.js +64 -53
  104. package/dist/core/runner/prepare-inherited-gate.d.ts +30 -23
  105. package/dist/core/runner/prepare-inherited-gate.js +45 -25
  106. package/dist/core/runner/prepare-park-ask.d.ts +6 -4
  107. package/dist/core/runner/prepare-park-ask.js +7 -5
  108. package/dist/core/runner/prepare-policy-chain.d.ts +25 -4
  109. package/dist/core/runner/prepare-policy-chain.js +203 -180
  110. package/dist/core/runner/prepare-protocol-tools.d.ts +28 -0
  111. package/dist/core/runner/prepare-protocol-tools.js +13 -3
  112. package/dist/core/runner/prepare-run-refs.d.ts +6 -6
  113. package/dist/core/runner/prepare-run-refs.js +9 -4
  114. package/dist/core/runner/prepare-safety-scan.d.ts +5 -0
  115. package/dist/core/runner/prepare-safety-scan.js +19 -3
  116. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -5
  117. package/dist/core/runner/prepare-suspend-saga.js +6 -6
  118. package/dist/core/runner/prepare-task.js +56 -24
  119. package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -6
  120. package/dist/core/runner/prepare-wiring-manifest.js +5 -3
  121. package/dist/core/runner/read-root-grant.d.ts +9 -0
  122. package/dist/core/runner/read-root-grant.js +12 -0
  123. package/dist/core/runner/retired-runner-deps.js +1 -0
  124. package/dist/core/runner/run-brain-sinks.js +3 -1
  125. package/dist/core/runner/run-identity-wiring.js +1 -1
  126. package/dist/core/runner/run-leg.js +29 -18
  127. package/dist/core/runner/run-notification-lane.d.ts +3 -0
  128. package/dist/core/runner/run-notification-lane.js +3 -1
  129. package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
  130. package/dist/core/runner/run-telemetry-and-budget-seats.js +1 -1
  131. package/dist/core/runner/run-turn-boundary.js +1 -1
  132. package/dist/core/runner/runtask.d.ts +1 -0
  133. package/dist/core/runner/runtask.js +8 -11
  134. package/dist/core/runner/session-mode-seats.d.ts +48 -0
  135. package/dist/core/runner/session-mode-seats.js +73 -0
  136. package/dist/core/runner/session-rule-policy.js +3 -2
  137. package/dist/core/runner/stream-mode-verb.d.ts +27 -0
  138. package/dist/core/runner/stream-mode-verb.js +19 -0
  139. package/dist/core/runner/turn-attachments.d.ts +3 -3
  140. package/dist/core/runner/turn-attachments.js +1 -1
  141. package/dist/core/runner/write-protection-station.d.ts +7 -1
  142. package/dist/core/runner/write-protection-station.js +6 -3
  143. package/dist/core/runner-deps.d.ts +31 -34
  144. package/dist/core/runtime-caps.d.ts +12 -3
  145. package/dist/core/safety-axis-vocab.d.ts +2 -2
  146. package/dist/core/safety-axis-vocab.js +1 -1
  147. package/dist/core/secret-env.d.ts +30 -58
  148. package/dist/core/secret-env.js +149 -586
  149. package/dist/core/sensitive-path-policy.d.ts +4 -2
  150. package/dist/core/sensitive-path-policy.js +41 -31
  151. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
  152. package/dist/core/task-event.d.ts +1 -1
  153. package/dist/core/task-result.d.ts +1 -1
  154. package/dist/core/task-spec.d.ts +41 -89
  155. package/dist/core/task-stream.d.ts +16 -0
  156. package/dist/core/terminal-cause.js +1 -0
  157. package/dist/core/tighten-task-spec.d.ts +2 -2
  158. package/dist/core/tighten-task-spec.js +12 -14
  159. package/dist/core/tool-catalog-entries.js +4 -3
  160. package/dist/core/tool-policy.d.ts +84 -30
  161. package/dist/core/tool-policy.js +76 -16
  162. package/dist/core/tool-registry.d.ts +38 -0
  163. package/dist/core/tool-registry.js +25 -1
  164. package/dist/core/tool-roster.d.ts +17 -2
  165. package/dist/core/tool-roster.js +11 -1
  166. package/dist/core/tool-spec.d.ts +8 -9
  167. package/dist/core/trace.d.ts +18 -6
  168. package/dist/core/types.d.ts +2 -1
  169. package/dist/core/wiring-manifest.d.ts +31 -5
  170. package/dist/core/wiring-manifest.js +28 -1
  171. package/dist/core/write-protect.d.ts +11 -1
  172. package/dist/core/write-protect.js +26 -15
  173. package/dist/engine/execution-env/node-execution-env.d.ts +6 -1
  174. package/dist/engine/execution-env/node-execution-env.js +16 -3
  175. package/dist/engine/harness/agent-harness.js +2 -1
  176. package/dist/engine/harness/types.d.ts +6 -7
  177. package/dist/engine/llm/types.d.ts +2 -4
  178. package/dist/engine/loop/types.d.ts +7 -0
  179. package/dist/engine/lsp/node-lsp-manager.js +1 -1
  180. package/dist/index.d.ts +18 -8
  181. package/dist/index.js +15 -6
  182. package/dist/orchestration/workflow-governance.js +1 -1
  183. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  184. package/dist/orchestration/workflow-size-guideline.js +2 -2
  185. package/dist/stores/file/index.d.ts +5 -3
  186. package/dist/stores/file/session-store.d.ts +8 -2
  187. package/dist/stores/file/session-store.js +5 -5
  188. package/dist/tools/artifact/artifact-tool.d.ts +17 -0
  189. package/dist/tools/artifact/artifact-tool.js +16 -1
  190. package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
  191. package/dist/tools/fs/bash-readonly-classifier.js +88 -0
  192. package/dist/tools/fs/containment-face.d.ts +9 -3
  193. package/dist/tools/fs/containment-face.js +2 -0
  194. package/dist/tools/fs/file-read-check.d.ts +50 -0
  195. package/dist/tools/fs/file-read-check.js +79 -0
  196. package/dist/tools/fs/fs-bash.d.ts +3 -3
  197. package/dist/tools/fs/fs-read.d.ts +1 -0
  198. package/dist/tools/fs/fs-search-tools.d.ts +2 -2
  199. package/dist/tools/fs/fs-search-tools.js +4 -4
  200. package/dist/tools/fs/fs-shared.js +20 -6
  201. package/dist/tools/fs/fs-write.d.ts +4 -3
  202. package/dist/tools/fs/fs-write.js +9 -9
  203. package/dist/tools/fs/index.d.ts +39 -3
  204. package/dist/tools/fs/index.js +38 -15
  205. package/dist/tools/fs/read-deny-home.d.ts +93 -0
  206. package/dist/tools/fs/read-deny-home.js +225 -0
  207. package/dist/tools/fs/read-deny.d.ts +176 -34
  208. package/dist/tools/fs/read-deny.js +268 -61
  209. package/dist/tools/fs/safety.d.ts +6 -1
  210. package/dist/tools/fs/safety.js +18 -5
  211. package/dist/tools/fs/search.d.ts +3 -0
  212. package/dist/tools/fs/search.js +47 -9
  213. package/dist/tools/fs/shell-read-check.d.ts +52 -13
  214. package/dist/tools/fs/shell-read-check.js +199 -20
  215. package/dist/tools/fs/shell-read-establish.d.ts +7 -3
  216. package/dist/tools/fs/shell-read-establish.js +134 -21
  217. package/dist/tools/fs/shell-read-identity.d.ts +7 -0
  218. package/dist/tools/fs/shell-read-identity.js +21 -0
  219. package/dist/tools/fs/shell-read-judge.d.ts +92 -32
  220. package/dist/tools/fs/shell-read-judge.js +43 -18
  221. package/dist/tools/fs/shell-read-plan.d.ts +4 -0
  222. package/dist/tools/fs/shell-read-plan.js +26 -4
  223. package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
  224. package/dist/tools/fs/shell-read-write-targets.js +173 -0
  225. package/package.json +1 -1
  226. package/test/export-surface.snapshot.json +117 -11
  227. package/wire-changes/core@7.34.0.json +2621 -0
  228. package/wire-changes/core@7.35.0.json +584 -0
@@ -11,7 +11,9 @@ import type { ToolPolicy } from "./tool-policy.js";
11
11
  *
12
12
  * @contract sensitive_paths.agent_config_tier_is_shared — the write deny set's agent-configuration
13
13
  * block IS the read face's `agent-config` tier, row for row: one table, two judges, and the two faces
14
- * cannot drift into disagreeing about which files are an agent's own configuration. The CONFIGURATION
14
+ * cannot drift into disagreeing about which files are an agent's own configuration. The block carries each
15
+ * row's RUN: the read rows are home-anchored (a project's same-named file reads), while this write set judges
16
+ * each run anywhere, so a project's own configuration file stays a refused write here. The CONFIGURATION
15
17
  * DIRECTORIES (`.claude`, `.sema`) are on neither face's deny side — their write guard is ASK
16
18
  * severity, where an approver or a persisted allow rule can clear it. Their `hooks/` and `agents/`
17
19
  * sub-directories are the two exceptions and they are NOT part of this mirror: a hook script is an executable
@@ -30,7 +32,7 @@ export declare function agentConfigDenyPatterns(): readonly string[];
30
32
  * globs): each pattern is a `/`-separated run of path SEGMENTS; `*` matches any run of non-slash chars
31
33
  * within one segment and is the ONLY metacharacter (everything else, `?` included, is literal); the run
32
34
  * may match ANYWHERE in the canonical path (so `.ssh` covers `~/.ssh/…` and any other `.ssh` directory —
33
- * home-anchoring like `~/` is NOT part of the syntax, do not prefix it). A pattern that reduces to zero
35
+ * home-anchoring like `~/` is NOT part of the syntax: a pattern opening with it is refused at wiring). A pattern that reduces to zero
34
36
  * segments is a wiring ERROR (it would guard nothing) and throws rather than being dropped.
35
37
  * Remember this policy guards WRITES only; reads are out of scope by design.
36
38
  *
@@ -1,12 +1,13 @@
1
1
  import { homedir } from "node:os";
2
2
  import { isAbsolute, join, resolve } from "node:path";
3
3
  import { canonicalizeTarget, expandHomeTilde, isAbsoluteForFamily, isWinFormPath, pathFamilyOf, writeTargetPath } from "../tools/fs/safety.js";
4
- import { compileSegmentPattern, matchSegmentPatterns, READ_FACE_BUILTIN_DENY_TABLE } from "../tools/fs/read-deny.js";
4
+ import { compileSegmentPattern, matchSegmentPatterns, READ_FACE_BUILTIN_DENY_TABLE, readDenyPatternRun } from "../tools/fs/read-deny.js";
5
5
  import { isProtectedWrite, protectivePathTargetOf } from "./tool-registry.js";
6
6
  import { pathTargetBaseOf } from "./effective-path-target.js";
7
7
  import { symlinkChainHops } from "./path-identity.js";
8
+ import { toolNameSetHas } from "./tool-roster.js";
8
9
  export function agentConfigDenyPatterns() {
9
- return READ_FACE_BUILTIN_DENY_TABLE.filter((row) => row.tier === "agent-config").map((row) => row.pattern);
10
+ return READ_FACE_BUILTIN_DENY_TABLE.filter((row) => row.tier === "agent-config").map((row) => readDenyPatternRun(row.pattern).run);
10
11
  }
11
12
  export const RECOMMENDED_SENSITIVE_PATTERNS = [
12
13
  ".env",
@@ -95,6 +96,11 @@ export function relativeUnderRoot(targetKey, rootKey) {
95
96
  function compilePatterns(patterns) {
96
97
  const out = [];
97
98
  for (const raw of patterns) {
99
+ const { homeAnchored, run } = readDenyPatternRun(raw);
100
+ if (homeAnchored) {
101
+ throw new Error(`createSensitivePathPolicy: pattern ${JSON.stringify(raw)} opens with the home word "~", which this write list does not place — its patterns are judged anywhere in the real target. ` +
102
+ `Spell the segments without it (${JSON.stringify(run)}), which covers the home's file and every same-named one.`);
103
+ }
98
104
  const compiled = compileSegmentPattern(raw, CASE_INSENSITIVE_FS ? "unicode" : "none");
99
105
  if (compiled === null) {
100
106
  throw new Error(`createSensitivePathPolicy: pattern ${JSON.stringify(raw)} contains no path segments and would guard nothing. ` +
@@ -111,55 +117,59 @@ export function createSensitivePathPolicy(opts) {
111
117
  const compiled = compilePatterns(opts.patterns);
112
118
  const dataRootPaths = dataRootSpellings(opts.dataRoot);
113
119
  const guardedByName = opts.tools !== undefined ? new Set(opts.tools) : undefined;
120
+ const family = (cwd) => pathFamilyOf({ root: opts.rootPath, cwd: cwd ?? opts.env.cwd });
121
+ const judgeTarget = async (path, fam, base, signal) => {
122
+ const canon = await canonicalizeTarget(opts.env, path, signal, fam, base);
123
+ if (!canon.ok)
124
+ return { kind: "unresolved", unresolvedSymlink: canon.unresolvedSymlink === true, cause: canon.refused ?? canon.message };
125
+ let relative = null;
126
+ for (const rootKey of await canonicalDataRootKeys(opts.env, dataRootPaths, signal)) {
127
+ relative = relativeUnderRoot(canon.key, rootKey);
128
+ if (relative !== null)
129
+ break;
130
+ }
131
+ if (relative === "")
132
+ return { kind: "clear" };
133
+ const hit = matchSensitive(relative ?? canon.key, compiled, canon.aliasResolved === true);
134
+ if (hit)
135
+ return { kind: "hit", pattern: hit, via: "target" };
136
+ if (relative === null) {
137
+ for (const hop of await symlinkChainHops(opts.env, path, signal, fam, base)) {
138
+ const viaHop = matchSensitive(hop, compiled, false);
139
+ if (viaHop)
140
+ return { kind: "hit", pattern: viaHop, via: "chain" };
141
+ }
142
+ }
143
+ return { kind: "clear" };
144
+ };
114
145
  return {
146
+ ...(compiled.length > 0 ? { writeTargetGuard: async (target, signal) => (await judgeTarget(target, family(undefined), undefined, signal)).kind !== "clear" } : {}),
115
147
  async check(req, signal) {
116
148
  if (compiled.length === 0)
117
149
  return { action: "allow" };
118
- if (guardedByName !== undefined ? !guardedByName.has(req.toolName) : !isProtectedWrite(req))
150
+ if (guardedByName !== undefined ? !toolNameSetHas(guardedByName, req) : !isProtectedWrite(req))
119
151
  return { action: "allow" };
120
152
  const protective = protectivePathTargetOf(req);
121
153
  const path = writeTargetPath(req, protective);
122
154
  if (typeof path !== "string" || path.length === 0)
123
155
  return { action: "allow" };
124
- const canon = await canonicalizeTarget(opts.env, path, signal, pathFamilyOf({ root: opts.rootPath, cwd: req.cwd ?? opts.env.cwd }), pathTargetBaseOf(protective, { root: opts.rootPath, cwd: req.cwd }));
125
- if (!canon.ok) {
156
+ const judged = await judgeTarget(path, family(req.cwd), pathTargetBaseOf(protective, { root: opts.rootPath, cwd: req.cwd }), signal);
157
+ if (judged.kind === "unresolved") {
126
158
  return {
127
159
  action: "deny",
128
- message: canon.unresolvedSymlink
160
+ message: judged.unresolvedSymlink
129
161
  ? `write to "${path}" is blocked: it is a symlink whose real target could not be resolved (it could point onto a guarded sensitive path)`
130
- : `write to "${path}" is blocked: its target could not be resolved on this tree (${canon.refused ?? canon.message}), so the sensitive-path guard cannot judge it`,
162
+ : `write to "${path}" is blocked: its target could not be resolved on this tree (${judged.cause}), so the sensitive-path guard cannot judge it`,
131
163
  decisionReason: "safety",
132
164
  };
133
165
  }
134
- let relative = null;
135
- for (const rootKey of await canonicalDataRootKeys(opts.env, dataRootPaths, signal)) {
136
- relative = relativeUnderRoot(canon.key, rootKey);
137
- if (relative !== null)
138
- break;
139
- }
140
- const judged = relative ?? canon.key;
141
- if (relative === "")
142
- return { action: "allow" };
143
- const hit = matchSensitive(judged, compiled, canon.aliasResolved === true);
144
- if (hit) {
166
+ if (judged.kind === "hit") {
145
167
  return {
146
168
  action: "deny",
147
- message: `write to "${path}" is blocked: its real target resolves onto the guarded sensitive path "${hit}"`,
169
+ message: judged.via === "target" ? `write to "${path}" is blocked: its real target resolves onto the guarded sensitive path "${judged.pattern}"` : `write to "${path}" is blocked: its symlink chain passes through the guarded sensitive path "${judged.pattern}"`,
148
170
  decisionReason: "safety",
149
171
  };
150
172
  }
151
- if (relative === null) {
152
- for (const hop of await symlinkChainHops(opts.env, path, signal, pathFamilyOf({ root: opts.rootPath, cwd: req.cwd ?? opts.env.cwd }), pathTargetBaseOf(protective, { root: opts.rootPath, cwd: req.cwd }))) {
153
- const viaHop = matchSensitive(hop, compiled, false);
154
- if (viaHop) {
155
- return {
156
- action: "deny",
157
- message: `write to "${path}" is blocked: its symlink chain passes through the guarded sensitive path "${viaHop}"`,
158
- decisionReason: "safety",
159
- };
160
- }
161
- }
162
- }
163
173
  return { action: "allow" };
164
174
  },
165
175
  };
@@ -301,7 +301,7 @@ export async function checkpointStoreContract(make, runAssertion) {
301
301
  const stored = (await store.get(bitless.token)).pendingAction;
302
302
  assert.equal("hasBidiControls" in stored, false, "projection backfill must not write back to the row");
303
303
  });
304
- run("design/388 B18 × B6 identity + face group: a row's seven optional members (capabilityId/contractId/shapeDigest/implementationRevision/rosterDigest + family/pathTarget) survive the round-trip verbatim; an absent group stays absent", async () => {
304
+ run("design/388 B18 × B6 identity + face group: a row's eight optional members (capabilityId/contractId/shapeDigest/implementationRevision/rosterDigest + family/pathTarget/names) survive the round-trip verbatim; an absent group stays absent", async () => {
305
305
  const store = make();
306
306
  const carried = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "id-carried" });
307
307
  const group = {
@@ -312,6 +312,7 @@ export async function checkpointStoreContract(make, runAssertion) {
312
312
  rosterDigest: "sha256:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
313
313
  family: "file-write",
314
314
  pathTarget: { param: "destination", aliases: ["path"], access: "create", skillScopeEligible: true },
315
+ names: ["AcmeWrite", "acme_write"],
315
316
  };
316
317
  Object.assign(carried.pendingAction, group);
317
318
  const bare = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "id-bare" });
@@ -370,7 +370,7 @@ export type TaskEvent = ({
370
370
  * document/worktree/todo/task/task-list/task-output/task-stop/agent/fork/workflow-run/web-fetch/
371
371
  * web-search/cron-create/cron-delete/cron-list/tool-search/repo-map/monitor-start/
372
372
  * enter-plan-mode/exit-plan-mode/report-findings/schedule-wakeup/send-message/agent-transcript/
373
- * a2a/path_not_in_root/readonly_out_of_root), surfaced on the live wire so a CC-faithful shell
373
+ * a2a/path_not_in_root/readonly_out_of_root/read_unestablished), surfaced on the live wire so a CC-faithful shell
374
374
  * renders rich cards (red/green diff, stdout panes, file cards) without re-parsing the
375
375
  * model-facing string. Absent when the tool carries no card shape or when the serialized card
376
376
  * exceeds the size bound (omitted whole; a consumer falls back to `output`).
@@ -622,7 +622,7 @@ export interface TaskResult {
622
622
  * posture rides every leg that continues this work, not just the delegation subtree.
623
623
  *
624
624
  * **In-presence condition:** present iff non-empty. Built-in entries are never listed — they are the
625
- * DEPLOYMENT's own selection (off by default), re-compiled per leg and reported on its wiring
625
+ * DEPLOYMENT's own selection (the default tiers unless it says otherwise), re-compiled per leg and reported on its wiring
626
626
  * manifest ({@link import("./wiring-manifest.js").WiringManifest.readDeny}). Same terminal coverage as
627
627
  * {@link effectiveReadFace}: rides any terminal of a leg that ran; absent on prepare failures.
628
628
  *
@@ -4,6 +4,7 @@
4
4
  * beside it in `task-limits.ts`; the terminal record it produces in `task-result.ts`. Layer 0
5
5
  * vocabulary; `types.ts` re-exports the three, so no consumer's import changes.
6
6
  */
7
+ import type { PermissionMode } from "./ask-origin.js";
7
8
  import type { TSchema } from "typebox";
8
9
  import type { ThinkingLevel } from "../internal/harness.js";
9
10
  import type { ActorAssertion, Model, ResilienceOptions } from "../internal/llm.js";
@@ -31,15 +32,12 @@ export type ImageInput = {
31
32
  url: string;
32
33
  };
33
34
  /** A single self-contained task with its full configuration. */
34
- /** The shell-gate doctrine vocabulary — {@link TaskSpec.shellGate}'s value set, and the word the engine persists on a
35
- * shell-gated ask (`RiskDescriptor.shellGateDoctrine`) and folds across a delegation chain (max-rank: `off` <
36
- * `classify` < `always`). One closed set; every seat that names a doctrine spells it through this alias. */
37
- export type ShellGateDoctrine = "off" | "classify" | "always";
38
- /** The approval POSTURE a host declares for a task — {@link TaskSpec.approverPosture}'s value set: a
39
- * statement about who answers this run's approval cards, never an instruction to the gate. One word
40
- * today (`"bypass"` — the host answers every card itself, automatically). The engine only RENDERS it,
41
- * so adding a word here changes what the model is told and nothing else. */
42
- export type ApproverPosture = "bypass";
35
+ /** The shell-gate FLOOR vocabulary — {@link TaskSpec.shellGate}'s value set, the word the engine persists on a
36
+ * shell-gated ask (`RiskDescriptor.shellGateDoctrine`) and carries down a delegation chain. One value: `"always"`
37
+ * (every shell command is confirmed, under every permission mode — an operator's mandate). The shape tier itself
38
+ * is installed on every full shell regardless; what a permission mode does with the resulting ask is the gate's
39
+ * arbitration, never a doctrine word. */
40
+ export type ShellGateDoctrine = "always";
43
41
  export interface TaskSpec {
44
42
  taskId?: string;
45
43
  /** The instruction / latest user message for this turn. */
@@ -791,8 +789,8 @@ export interface TaskSpec {
791
789
  /**
792
790
  * Interactive read-boundary whitelist (ruled 2026-08-05, both-arms ruling, arm A): extra directories
793
791
  * the READ faces may access — and nothing else. Joins the containment allowlist of the read-only
794
- * tools (read_file / grep / glob / repo_map / `bash_readonly`) and the `shellGate:"classify"`
795
- * read-boundary (so a provably read-only command like `head <dir>/settings.json` auto-allows instead
792
+ * tools (read_file / grep / glob / repo_map / `bash_readonly`) and the shell read station's
793
+ * boundary (so a provably read-only command like `head <dir>/settings.json` auto-allows instead
796
794
  * of asking on every call), but is NEVER admitted by the write faces (edit_file / write_file /
797
795
  * notebook_edit refuse these paths exactly as before). Use `additionalDirectories` when the task
798
796
  * should also write there. Same canonicalization and fail-closed skip rules as
@@ -805,7 +803,7 @@ export interface TaskSpec {
805
803
  * design/199 件B — TASK-layer ADDITIONS to the built-in sensitive-path READ deny set
806
804
  * ({@link import("../tools/fs/read-deny.js").READ_FACE_DEFAULT_DENY_ENTRIES}). Judged by the
807
805
  * structured read faces (Read/Grep/Glob/RepoMap, their traversals, the compaction attachment reader) and
808
- * by the shell READ STATION under every doctrine — the full shell (`Bash`, `Monitor`, whatever
806
+ * by the shell READ STATION under every mode — the full shell (`Bash`, `Monitor`, whatever
809
807
  * {@link shellGate} says) and the `bash_readonly` shell mounted by {@link handsReadOnly} alike: a
810
808
  * deny-listed shell read is REFUSED at the gate (`deniedBy: "read_boundary"`), never asked, with the same
811
809
  * sentence the structured faces refuse with — one deny judge for every read face — in BOTH containment
@@ -814,8 +812,8 @@ export interface TaskSpec {
814
812
  * {@link RunnerDeps.readDenyPatterns}; `[]` ≡ absent (union identity); NOTHING at the task layer
815
813
  * can remove a built-in — the one removal channel is the DEPLOYMENT's built-in configuration
816
814
  * ({@link RunnerDeps.readDenyBuiltinTiers} / {@link RunnerDeps.readDenyBuiltinExclude}, #245
817
- * revision of D-4), which no TaskSpec key reaches — and that half is OFF by default, so on an
818
- * unconfigured deployment the entries here are the WHOLE judged set. Bad entry shapes refuse loudly at prepare
815
+ * revision of D-4), which no TaskSpec key reaches (an unconfigured deployment judges the default tiers,
816
+ * {@link import("../tools/fs/read-deny.js").READ_DENY_DEFAULT_TIERS}). Bad entry shapes refuse loudly at prepare
819
817
  * (#123). The write faces are untouched.
820
818
  */
821
819
  readDenyPatterns?: readonly import("../tools/fs/read-deny.js").ReadDenyEntry[];
@@ -890,87 +888,41 @@ export interface TaskSpec {
890
888
  */
891
889
  enableFork?: boolean;
892
890
  /**
893
- * design/80 D-2 (part-1): gate the injected `bash` hand tool, since a full shell can run egress (`curl`,
894
- * `git push`) and irreversible (`rm`) commands that the per-tool egress/irreversibility marks otherwise miss.
895
- * The §4-OQ4 doctrine: shell-in-presence ⇒ always-gate UNLESS a real parsed classifier is wired.
896
- * - `"off"` (default): `bash` is unmarked — the current behavior (subject only to the deployment's policy).
897
- * - `"always"`: every `bash` command tightens to an `irreversible_ask` durable suspend (fail-closed; the
898
- * correct default for an unattended/supervisor deployment with no classifier).
899
- * - `"classify"`: mark `bash` `irreversibility:"maybe"` with {@link import("../tools/fs/index.js").bashReversibilityProbe}
900
- * — the SHAPE classifier alone: a provably-benign single allowlisted read-only command (no operators)
901
- * auto-allows; everything else (writes, egress, pipes, unknown commands) tightens to ask. WHERE a command
902
- * reads is not this doctrine's question under any value: the gate's shell read station judges every
903
- * shell call's read candidates against the deny set and the containment roots (a deny-listed read is
904
- * refused, an out-of-root read is a mandated ask) whether `shellGate` is `"off"`, `"classify"` or
905
- * `"always"`. An egress command (e.g. `curl`) suspends under
906
- * the IRREVERSIBLE axis (the shell mark is irreversibility, not egress) — the kind is non-budgetable either
907
- * way, so the budget resolver still never auto-approves it; the axis label is informational.
908
- * NOT unconditional: a POSITIVE per-tool mark on the shell tool keeps its seat — an explicit
909
- * `irreversibility:"always"` is never downgraded (the tier combine is tighten-only) and an explicit
910
- * `"maybe"` keeps its own probe (a probe-less explicit `"maybe"` stays fail-closed ask; the doctrine's
911
- * generic probe installs only on doctrine-owned seats). An explicit `"never"` is NOT a mark — the
912
- * doctrine governs that seat as if unmarked.
913
- * No effect under `handsReadOnly` (that mounts `bash_readonly`, already allowlisted) or with no execution env.
914
- * **Re-supply on resume:** like `toolPolicy`/`tools`/`durableApproval`, `shellGate` is part of the resume
915
- * task config — a resume that omits it leaves the resumed run's SUBSEQUENT `bash` calls ungated (the approved
916
- * pending call itself runs once, having been adjudicated). Re-pass the same value the original task used.
891
+ * The OPERATOR's shell floor: `"always"` ⇒ every `bash` / `Monitor` command is a confirmed ask (an
892
+ * `operator_forced` ask — kept under `bypassPermissions`, not settled by `dontAsk`; a standing allow rule never
893
+ * clears it). Absent ⇒ no floor: the shape tier the engine installs on every full shell (a provably read-only
894
+ * single command runs, everything else asks) is arbitrated by the task's {@link permissionMode}. The retired
895
+ * values `"off"` / `"classify"` are refused at the door (`config.shell_gate_value_retired`): the tier is no longer a
896
+ * doctrine a caller chooses, and the read boundary is judged on every shell call under every mode. A positive
897
+ * per-tool mark keeps its seat (an explicit `irreversibility:"always"` is never downgraded). No effect under
898
+ * `handsReadOnly` or with no execution env. Re-supplied on resume like `toolPolicy` (a checkpoint carries the
899
+ * chain's floor). Delegation: the floor travels the inherited-gate lane; a child cannot lift it.
917
900
  */
918
901
  shellGate?: ShellGateDoctrine;
919
902
  /**
920
- * The caller's AUTO-MODE INTENT for this task — the "user turned auto on" half of the classifier
921
- * arming (CC 2.1.250 polarity: auto mode is something the USER enables at the permission-mode
922
- * seat, and something an ORGANIZATION may only DENY; it is never something an organization grants
923
- * on a user's behalf). The engine arms its per-run classifier only when ALL THREE hold:
924
- * - this seat is `true` (intent),
925
- * - {@link RunnerDeps.autoMode} is present (the deployment is classifier-capable — the trust gate;
926
- * classifier RULES still enter ONLY through that face, never through this seat), and
927
- * - {@link RuntimeCaps.autoMode} is not `false` (the per-principal DENY bit; ABSENT is NOT a denial).
903
+ * The task's PERMISSION MODE — the ONE key that says how the engine answers a surviving ask (six words,
904
+ * {@link import("./ask-origin.js").PERMISSION_MODES}; spelled as the wire spells it). ROOT task absent ⇒
905
+ * `"default"`; an engine-spawned CHILD absent ⇒ its parent's effective mode (a child's own declaration is
906
+ * adopted only where the reference implementation's rank table admits it under the parent's — a folded
907
+ * declaration is disclosed on `wiring_manifest.permissionMode.foldedBy: "inherited_ceiling"` and announced,
908
+ * never silent). A value outside the set is refused at the door (`config.permission_mode_invalid`).
928
909
  *
929
- * **Ownership** — same split as {@link shellGate}: the service layer TRANSLATES the effective
930
- * permission mode into this seat (at the same point it translates the mode into `shellGate`);
931
- * core only CONSUMES it. Absent ⇒ not an auto-mode task (byte-identical to the pre-seat shape:
932
- * asks flow the original chain). Only the literal `true` is a value here: any other present value
933
- * (`false`, `"true"`, `1`) is refused at the door (`config.auto_mode_requested_invalid`) rather than
934
- * read as either polarity — a marshalled string must never silently arm, or silently disarm, a
935
- * classifier.
910
+ * What the mode DOES is one table and one predicate ({@link import("./ask-origin.js").MODE_ARBITRATION},
911
+ * {@link import("./ask-origin.js").arbitrateByMode}) read after every rule, tighten and classifier station:
912
+ * `bypassPermissions` releases every ask but the immune classes (an explicit ask rule, a hook's ask, the
913
+ * question tool, an unverifiable recursive delete, an org rule, an operator's forced confirmation, unreadable
914
+ * governance, the classifier's denial-limit fallback) and discloses each release; `dontAsk` refuses every ask
915
+ * but a hook's or an operator authority's; `auto` arms the classifier (`RunnerDeps.autoMode` present and
916
+ * `RuntimeCaps.autoMode` not `false`, else folded to `default` and announced); `plan` reads only and mounts the
917
+ * plan pair; `acceptEdits` writes inside the root without asking; `default` asks. The mode also installs its own
918
+ * seats ({@link import("./mode-policy.js").modePolicyOf}).
936
919
  *
937
- * **Inheritance**: the intent is session-wide, like a permission mode — an engine-spawned child of
938
- * an auto-mode task inherits it through the trusted constraint chain
939
- * (`InheritedGate.autoModeRequested`), never through a model-authored argument; the child's own
940
- * deny bit and deployment face are still evaluated for the child. **On resume:** the intent the
941
- * suspend leg resolved (this seat, or the bit its chain carried) is recorded on the checkpoint's
942
- * data half (`CheckpointState.inheritedGate.autoModeRequested`) and read back as one more intent
943
- * source, so a redemption in another process arms as the suspend leg did without re-passing the
944
- * seat; re-passing it is still honoured (the sources fold by OR). Intent only — the resuming
945
- * deployment's face and the resuming principal's deny bit are judged afresh on every leg.
946
- */
947
- autoModeRequested?: true;
948
- /**
949
- * The host's declared APPROVER POSTURE for this task — a typed fact seat, the same ownership split
950
- * as {@link shellGate} and {@link autoModeRequested}: the service layer declares it (it is the layer
951
- * that knows who sits at the cards), core only CONSUMES it. `"bypass"` says the host answers every
952
- * approval card itself, automatically.
953
- *
954
- * **The gate does not read it.** Cards are raised, routed and recorded byte-identically with and
955
- * without this seat — no rule is relaxed, no ask is skipped, no decision is pre-empted. The seat's
956
- * one effect is that the ENGINE tells the model which posture the run is under, in its own marked
957
- * `<system-reminder>` on the first turn. That is the whole point of the seat: a posture is a fact of
958
- * the run, and a host that instead splices the sentence into the objective hands the model
959
- * reminder-shaped text with no engine mark, which the mark declaration tells it to treat as data and
960
- * flag. One fact, one seat, one mint.
961
- *
962
- * Absent ⇒ no posture declared, zero bytes rendered (byte-identical to the pre-seat shape). Only the
963
- * literal `"bypass"` is a value: any other present value is refused at the door
964
- * (`config.approver_posture_invalid`) rather than folded back to absence — a marshalled typo must not
965
- * silently un-declare a posture the host meant to declare.
966
- *
967
- * **Inheritance / resume:** session-wide, exactly like {@link autoModeRequested} — an engine-spawned
968
- * child inherits it through the trusted constraint chain (`InheritedGate.approverPosture`), and the
969
- * resolved value is recorded on the checkpoint's data half so a redemption in another process still
970
- * knows the posture without the seat being re-passed. Re-passing it is honoured; the sources fold
971
- * seat-first.
920
+ * `bypassPermissions` is a REQUEST: it takes effect only where the deployment does not deny it
921
+ * ({@link RuntimeCaps.bypassPermissions} not `false`); a denied request runs in `default`, announced
922
+ * (`config.permission_mode_unavailable`). A running leg changes mode through `TaskStream.setPermissionMode`.
923
+ * Persists on the checkpoint's data half (`CheckpointState.inheritedGate.permissionMode`), re-judged on every leg.
972
924
  */
973
- approverPosture?: ApproverPosture;
925
+ permissionMode?: PermissionMode;
974
926
  /** Task-scoped MCP servers, materialized into tools then disposed. */
975
927
  mcp?: McpServerSpec[];
976
928
  /** Task-scoped A2A peers (remote agents), whose advertised skills mount as `a2a__<peer>__<skill>`
@@ -275,6 +275,22 @@ export interface TaskStream extends AsyncIterable<TaskEvent> {
275
275
  }): Promise<{
276
276
  outcome: "created" | "existed";
277
277
  }>;
278
+ /**
279
+ * Change the running leg's PERMISSION MODE (`TaskSpec.permissionMode`'s live half — a shell's mode toggle, a serving
280
+ * layer's `permission_mode` control frame). The requested word is rewritten on the leg's mode seat; the deployment's
281
+ * fold is re-judged (`bypassPermissions` where `RuntimeCaps.bypassPermissions` is `false`, or `auto` with no
282
+ * classifier face / a `false` deny bit, runs `default` and is announced `config.permission_mode_unavailable`); a leg
283
+ * that became `auto` arms its classifier now — an arming that fails rolls the steer back (the word it replaced stands)
284
+ * and the verb rejects with the arming's code; the NEXT tool call reads the new mode at the gate's arbitration point
285
+ * (a tightening and a loosening both take effect one call later, on one seat), and a child leg derives its own
286
+ * effective mode from this seat on its next read. Resolves the same three-member receipt `wiring_manifest.permissionMode`
287
+ * carries — `{ requested, effective, foldedBy? }` — on EVERY call, moved or not, so a caller renders what the engine
288
+ * says and never what it sent. Typed refusals: `steering.invalid_content` (a word outside the closed set) and the
289
+ * steer-family `steering.not_running` once the task has finished. AUTHORITY (steer-lane law): holding this stream IS
290
+ * the capability handle; a service exposing the verb owns the principal check at its own face — and a deployment that
291
+ * must never let a caller reach `bypassPermissions` says so with the deny bit, which this verb honours.
292
+ */
293
+ setPermissionMode(mode: import("./ask-origin.js").PermissionMode): Promise<import("./mode-policy.js").PermissionModeReceipt>;
278
294
  /**
279
295
  * **RE-DIAL one MCP server** on the running task, the action `RefreshMcpTools` deliberately
280
296
  * is not (its contract is "never dials or re-dials"). Core holds no MCP connection across task
@@ -4,6 +4,7 @@ export const TERMINAL_CAUSE_IS_REPLAYABLE = {
4
4
  blocked: false,
5
5
  paused: false,
6
6
  };
7
+ Object.freeze(TERMINAL_CAUSE_IS_REPLAYABLE);
7
8
  export function isTerminalCauseKind(kind) {
8
9
  return typeof kind === "string" && Object.hasOwn(TERMINAL_CAUSE_IS_REPLAYABLE, kind);
9
10
  }
@@ -13,8 +13,8 @@ export declare class TaskSpecTightenError extends Error {
13
13
  * would drop the base policy entirely.
14
14
  * - **`handsReadOnly`** — `true` is stricter. `base:true` + `override:false` ⇒ **throw** (loosening). Otherwise
15
15
  * the stricter (`true` if either is true) wins.
16
- * - **`shellGate`** — `off < classify < always`. `override` may only equal-or-raise the rank; lowering it ⇒
17
- * **throw**. The stricter of the two wins.
16
+ * - **`shellGate`** — one value (`"always"`, the operator floor). Either side set ⇒ the floor stands; it cannot
17
+ * be loosened because absence is the only other state and an override cannot unset a base value.
18
18
  * - **`onAsk`** — no safe automatic merge (the approver and the policy must be coordinated). Both set to
19
19
  * DIFFERENT values ⇒ **throw**; equal ⇒ keep; one set ⇒ that one.
20
20
  * - **`hooks`** — no standard composition (stacking hook results is caller-specific). Both set ⇒ **throw**;
@@ -1,9 +1,5 @@
1
1
  import { combinePolicies } from "./tool-policy.js";
2
- const SHELL_GATE_RANK = {
3
- off: 0,
4
- classify: 1,
5
- always: 2,
6
- };
2
+ import { clampPermissionMode } from "./ask-origin.js";
7
3
  export class TaskSpecTightenError extends Error {
8
4
  constructor(message) {
9
5
  super(message);
@@ -51,16 +47,18 @@ export function tightenTaskSpec(base, overrides) {
51
47
  if (base.handsReadOnly !== undefined || overrides.handsReadOnly !== undefined) {
52
48
  merged.handsReadOnly = base.handsReadOnly === true || overrides.handsReadOnly === true;
53
49
  }
54
- if (base.shellGate !== undefined || overrides.shellGate !== undefined) {
55
- const baseGate = base.shellGate ?? "off";
56
- const overrideGate = overrides.shellGate;
57
- if (overrideGate !== undefined && SHELL_GATE_RANK[overrideGate] < SHELL_GATE_RANK[baseGate]) {
58
- throw new TaskSpecTightenError(`tightenTaskSpec: cannot loosen shellGate from "${baseGate}" to "${overrideGate}" (override may only tighten)`);
50
+ if (base.permissionMode !== undefined && overrides.permissionMode !== undefined) {
51
+ const { effective, clamped } = clampPermissionMode(overrides.permissionMode, base.permissionMode);
52
+ if (clamped) {
53
+ throw new TaskSpecTightenError(`tightenTaskSpec: permissionMode override "${overrides.permissionMode}" would loosen the base "${base.permissionMode}" (the override may only tighten; a bypass override needs a bypass base, acceptEdits and auto never adopt each other)`);
59
54
  }
60
- merged.shellGate =
61
- overrideGate !== undefined && SHELL_GATE_RANK[overrideGate] > SHELL_GATE_RANK[baseGate]
62
- ? overrideGate
63
- : (base.shellGate ?? overrideGate);
55
+ merged.permissionMode = effective;
56
+ }
57
+ else if (base.permissionMode !== undefined || overrides.permissionMode !== undefined) {
58
+ merged.permissionMode = overrides.permissionMode ?? base.permissionMode;
59
+ }
60
+ if (base.shellGate !== undefined || overrides.shellGate !== undefined) {
61
+ merged.shellGate = base.shellGate ?? overrides.shellGate;
64
62
  }
65
63
  if (base.onAsk !== undefined && overrides.onAsk !== undefined && base.onAsk !== overrides.onAsk) {
66
64
  throw new TaskSpecTightenError("tightenTaskSpec: conflicting onAsk on base and overrides — onAsk cannot be merged automatically " +
@@ -56,7 +56,7 @@ export const TOOL_CATALOG_ENTRIES = [
56
56
  },
57
57
  {
58
58
  id: "bash", name: "Bash", source: "builtin", definedIn: "src/tools/fs/fs-bash.ts", mountedBy: ["hands"],
59
- cards: ["bash", "readonly_out_of_root", "bash_invalid_timeout"],
59
+ cards: ["bash", "readonly_out_of_root", "read_unestablished", "bash_invalid_timeout"],
60
60
  face: {
61
61
  family: "shell", effect: "write", contentOrigin: "execution", contract: c(FULL_SHELL_CONTRACT_ID),
62
62
  ruleFace: SHELL_RULE_FACE,
@@ -66,7 +66,7 @@ export const TOOL_CATALOG_ENTRIES = [
66
66
  },
67
67
  {
68
68
  id: "bash-readonly", name: "Bash", source: "builtin", definedIn: "src/tools/fs/fs-bash.ts", mountedBy: ["hands"],
69
- cards: ["bash", "readonly_out_of_root", "bash_invalid_timeout"],
69
+ cards: ["bash", "readonly_out_of_root", "read_unestablished", "bash_invalid_timeout"],
70
70
  face: {
71
71
  family: "shell", effect: "read", contract: c(READONLY_SHELL_CONTRACT_ID),
72
72
  ruleFace: SHELL_RULE_FACE,
@@ -108,7 +108,7 @@ export const TOOL_CATALOG_ENTRIES = [
108
108
  { id: "task-stop-hands", name: "TaskStop", source: "builtin", definedIn: "src/tools/fs/fs-bash.ts", mountedBy: ["hands"], cards: [], face: { family: "delegate", effect: "write", contract: TASK_STOP_CONTRACT } },
109
109
  { id: "task-output", name: "TaskOutput", source: "builtin", definedIn: "src/core/task-registry.ts", mountedBy: ["delegation"], cards: ["task-output"], face: { family: "delegate", effect: "read", contract: TASK_OUTPUT_CONTRACT } },
110
110
  { id: "task-stop", name: "TaskStop", source: "builtin", definedIn: "src/core/task-registry.ts", mountedBy: ["delegation"], cards: ["task-stop"], face: { family: "delegate", effect: "write", contract: TASK_STOP_CONTRACT } },
111
- { id: "monitor", name: "Monitor", source: "builtin", definedIn: "src/tools/monitor.ts", mountedBy: ["backgroundTasks"], cards: ["monitor-start", "readonly_out_of_root"], face: { family: "other", effect: "write", contract: c("core.monitor@1"), offloadThresholdChars: 10_000 } },
111
+ { id: "monitor", name: "Monitor", source: "builtin", definedIn: "src/tools/monitor.ts", mountedBy: ["backgroundTasks"], cards: ["monitor-start", "readonly_out_of_root", "read_unestablished"], face: { family: "other", effect: "write", contract: c("core.monitor@1"), offloadThresholdChars: 10_000 } },
112
112
  { id: "send-message", name: "SendMessage", source: "builtin", definedIn: "src/agents/send-message-tool.ts", mountedBy: ["delegation", "hostInternals", "peerLane"], cards: ["send-message"], face: { family: "protocol", effect: "write", contract: c("core.send_message@1") } },
113
113
  { id: "agent-transcript", name: "AgentTranscript", source: "builtin", definedIn: "src/agents/agent-transcript-tool.ts", mountedBy: ["delegation"], cards: ["agent-transcript"], face: { family: "delegate", effect: "read", contract: c("core.agent_transcript@1") } },
114
114
  { id: "list-agents", name: "ListAgents", source: "builtin", definedIn: "src/agents/list-agents-tool.ts", mountedBy: ["delegation", "peerLane"], cards: ["list-agents"], face: { family: "protocol", effect: "read", aliases: ["ListPeers"], contract: c("core.list_agents@1") } },
@@ -156,6 +156,7 @@ export const ENGINE_CARDS = Object.freeze({
156
156
  path_not_in_root: "src/tools/fs/safety.ts",
157
157
  read_path_denied: "src/tools/fs/safety.ts",
158
158
  readonly_out_of_root: "src/tools/fs/shell-read-check.ts",
159
+ read_unestablished: "src/tools/fs/shell-read-check.ts",
159
160
  });
160
161
  const byId = new Map();
161
162
  for (const e of TOOL_CATALOG_ENTRIES) {