@sema-agent/core 7.16.0 → 7.17.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 (193) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/dist/agents/builtin-agents.d.ts +3 -3
  3. package/dist/agents/cross-session-envelope.d.ts +0 -1
  4. package/dist/agents/cross-session-envelope.js +0 -1
  5. package/dist/agents/peer-notices.d.ts +0 -9
  6. package/dist/agents/peer-notices.js +0 -63
  7. package/dist/agents/subagent.js +7 -15
  8. package/dist/brain/request-params.d.ts +2 -2
  9. package/dist/config/defaults.d.ts +0 -7
  10. package/dist/config/defaults.js +0 -2
  11. package/dist/core/artifact-host.d.ts +218 -0
  12. package/dist/core/artifact-host.js +92 -0
  13. package/dist/core/ask-origin.d.ts +29 -3
  14. package/dist/core/ask-origin.js +17 -9
  15. package/dist/core/ask-question.d.ts +25 -0
  16. package/dist/core/ask-question.js +4 -0
  17. package/dist/core/ask-unresolvable-notice.d.ts +30 -21
  18. package/dist/core/ask-unresolvable-notice.js +16 -8
  19. package/dist/core/auto-compaction.d.ts +10 -10
  20. package/dist/core/auto-mode-arming.d.ts +2 -2
  21. package/dist/core/background-agent-store.d.ts +5 -5
  22. package/dist/core/background-shell.d.ts +1 -1
  23. package/dist/core/checkpoint-execution-record.d.ts +0 -3
  24. package/dist/core/checkpoint-execution-record.js +0 -4
  25. package/dist/core/checkpoint-store.d.ts +71 -99
  26. package/dist/core/checkpoint-store.js +9 -16
  27. package/dist/core/context-edit.d.ts +36 -104
  28. package/dist/core/context-edit.js +37 -60
  29. package/dist/core/effective-path-target.d.ts +68 -8
  30. package/dist/core/effective-path-target.js +42 -30
  31. package/dist/core/engine-notice.d.ts +46 -32
  32. package/dist/core/env-budget.d.ts +31 -0
  33. package/dist/core/env-budget.js +39 -0
  34. package/dist/core/exec-output-tail.d.ts +5 -5
  35. package/dist/core/file-history-store.js +3 -6
  36. package/dist/core/fs-write-gate-policy.d.ts +1 -1
  37. package/dist/core/fs-write-gate-policy.js +3 -3
  38. package/dist/core/gate-lanes.js +9 -15
  39. package/dist/core/governance-codes.d.ts +1 -1
  40. package/dist/core/governance-codes.js +2 -0
  41. package/dist/core/hands-band.d.ts +1 -1
  42. package/dist/core/hooks.d.ts +18 -17
  43. package/dist/core/lsp-session.d.ts +1 -1
  44. package/dist/core/lsp.d.ts +1 -1
  45. package/dist/core/lsp.js +5 -4
  46. package/dist/core/mcp.d.ts +5 -5
  47. package/dist/core/mcp.js +4 -48
  48. package/dist/core/memory-recall.d.ts +0 -8
  49. package/dist/core/memory.d.ts +1 -1
  50. package/dist/core/oracle-isolation.js +2 -2
  51. package/dist/core/parent-spec-seat.d.ts +66 -0
  52. package/dist/core/parent-spec-seat.js +47 -0
  53. package/dist/core/permission-rule-model.d.ts +11 -6
  54. package/dist/core/permission-rule-model.js +53 -37
  55. package/dist/core/permission-rules.js +8 -3
  56. package/dist/core/protocol-naming.d.ts +2 -2
  57. package/dist/core/remote-env.d.ts +1 -1
  58. package/dist/core/retired-keys.d.ts +19 -0
  59. package/dist/core/retired-keys.js +15 -0
  60. package/dist/core/runner/active-skill-scope.js +3 -3
  61. package/dist/core/runner/assemble-result.d.ts +15 -13
  62. package/dist/core/runner/assemble-result.js +5 -4
  63. package/dist/core/runner/compaction-call-options.d.ts +25 -89
  64. package/dist/core/runner/contracts.d.ts +10 -9
  65. package/dist/core/runner/gate-exit.js +15 -1
  66. package/dist/core/runner/halt-attribution.d.ts +35 -0
  67. package/dist/core/runner/halt-attribution.js +16 -0
  68. package/dist/core/runner/memory-consolidation.d.ts +0 -2
  69. package/dist/core/runner/memory-consolidation.js +0 -1
  70. package/dist/core/runner/prepare-artifact.d.ts +53 -0
  71. package/dist/core/runner/prepare-artifact.js +77 -0
  72. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  73. package/dist/core/runner/prepare-caps-and-workflow.js +2 -3
  74. package/dist/core/runner/prepare-config-doors.d.ts +3 -3
  75. package/dist/core/runner/prepare-config-doors.js +5 -2
  76. package/dist/core/runner/prepare-context-lane.d.ts +2 -2
  77. package/dist/core/runner/prepare-context-lane.js +2 -13
  78. package/dist/core/runner/prepare-file-history.d.ts +1 -1
  79. package/dist/core/runner/prepare-file-history.js +3 -52
  80. package/dist/core/runner/prepare-hands-readface.d.ts +4 -7
  81. package/dist/core/runner/prepare-hands-readface.js +3 -2
  82. package/dist/core/runner/prepare-inherited-gate.d.ts +5 -2
  83. package/dist/core/runner/prepare-inherited-gate.js +7 -8
  84. package/dist/core/runner/prepare-memory.d.ts +4 -6
  85. package/dist/core/runner/prepare-memory.js +4 -2
  86. package/dist/core/runner/prepare-policy-chain.d.ts +3 -3
  87. package/dist/core/runner/prepare-policy-chain.js +15 -30
  88. package/dist/core/runner/prepare-prompt-assembly.js +1 -1
  89. package/dist/core/runner/prepare-safety-scan.d.ts +1 -1
  90. package/dist/core/runner/prepare-suspend-saga.js +0 -2
  91. package/dist/core/runner/prepare-task.js +10 -9
  92. package/dist/core/runner/prepare-wiring-manifest.js +9 -7
  93. package/dist/core/runner/resume-admission.js +2 -1
  94. package/dist/core/runner/resume-internals-and-config.js +4 -0
  95. package/dist/core/runner/retired-runner-deps.d.ts +3 -0
  96. package/dist/core/runner/retired-runner-deps.js +7 -0
  97. package/dist/core/runner/run-harness-handlers.js +2 -3
  98. package/dist/core/runner/run-settle-and-teardown.d.ts +1 -1
  99. package/dist/core/runner/run-settle-and-teardown.js +3 -1
  100. package/dist/core/runner/session-file-state-replay.js +3 -3
  101. package/dist/core/runner/session-rule-policy.js +3 -4
  102. package/dist/core/runner/stream-halt-verbs.js +1 -1
  103. package/dist/core/runner/stream-steer-verb.js +1 -4
  104. package/dist/core/runner/workspace-path.js +3 -0
  105. package/dist/core/runner-deps.d.ts +30 -49
  106. package/dist/core/safe-notify.d.ts +1 -1
  107. package/dist/core/sensitive-path-policy.d.ts +1 -1
  108. package/dist/core/sensitive-path-policy.js +4 -4
  109. package/dist/core/session.d.ts +2 -2
  110. package/dist/core/skill-tool-specifier.d.ts +1 -1
  111. package/dist/core/skills-directory.d.ts +1 -1
  112. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  113. package/dist/core/strategy-store.d.ts +8 -11
  114. package/dist/core/task-event.d.ts +20 -17
  115. package/dist/core/task-limits.d.ts +1 -1
  116. package/dist/core/task-notification.d.ts +5 -5
  117. package/dist/core/task-registry-agent.d.ts +5 -5
  118. package/dist/core/task-registry-monitor.d.ts +2 -2
  119. package/dist/core/task-registry-shared.d.ts +27 -27
  120. package/dist/core/task-registry-shared.js +3 -13
  121. package/dist/core/task-registry.d.ts +8 -8
  122. package/dist/core/task-result.d.ts +37 -37
  123. package/dist/core/task-spec.d.ts +5 -5
  124. package/dist/core/task-stream.d.ts +16 -11
  125. package/dist/core/tool-catalog-entries.d.ts +1 -0
  126. package/dist/core/tool-catalog-entries.js +5 -0
  127. package/dist/core/tool-conformance.d.ts +0 -2
  128. package/dist/core/tool-conformance.js +1 -3
  129. package/dist/core/tool-errors.d.ts +5 -0
  130. package/dist/core/tool-errors.js +1 -0
  131. package/dist/core/tool-policy.d.ts +51 -22
  132. package/dist/core/tool-policy.js +28 -5
  133. package/dist/core/tool-result-budget.d.ts +1 -1
  134. package/dist/core/tool-result-store.d.ts +11 -11
  135. package/dist/core/tool-spec.d.ts +34 -38
  136. package/dist/core/tools.d.ts +2 -2
  137. package/dist/core/trace.d.ts +12 -15
  138. package/dist/core/untrusted-text.d.ts +1 -1
  139. package/dist/core/wiring-manifest.d.ts +4 -3
  140. package/dist/core/workflow-journal-store.d.ts +11 -11
  141. package/dist/engine/execution-env/node-execution-env.d.ts +3 -2
  142. package/dist/engine/execution-env/node-execution-env.js +4 -2
  143. package/dist/engine/harness/agent-harness.d.ts +47 -4
  144. package/dist/engine/harness/agent-harness.js +52 -9
  145. package/dist/engine/loop/agent-loop.js +15 -8
  146. package/dist/engine/loop/types.d.ts +5 -0
  147. package/dist/index.d.ts +19 -14
  148. package/dist/index.js +15 -10
  149. package/dist/internal/harness.d.ts +1 -0
  150. package/dist/orchestration/run-spec.d.ts +3 -0
  151. package/dist/orchestration/run-spec.js +3 -3
  152. package/dist/orchestration/run-workflow-tool.d.ts +12 -6
  153. package/dist/orchestration/run-workflow-tool.js +23 -5
  154. package/dist/orchestration/workflow-primitives.d.ts +4 -2
  155. package/dist/orchestration/workflow-primitives.js +1 -4
  156. package/dist/orchestration/workflow-types.d.ts +34 -4
  157. package/dist/orchestration/workflow-types.js +2 -0
  158. package/dist/orchestration/workflow.d.ts +39 -7
  159. package/dist/orchestration/workflow.js +234 -113
  160. package/dist/prompt-assembly/assemble.d.ts +2 -2
  161. package/dist/prompt-assembly/assemble.js +9 -9
  162. package/dist/prompt-assembly/composer.d.ts +6 -7
  163. package/dist/prompt-assembly/composer.js +10 -10
  164. package/dist/prompt-assembly/epoch.js +1 -1
  165. package/dist/prompt-assembly/event-registry.d.ts +0 -21
  166. package/dist/prompt-assembly/packs/sema-default.js +30 -30
  167. package/dist/prompt-assembly/types.d.ts +14 -14
  168. package/dist/prompts/coordinator.d.ts +0 -13
  169. package/dist/prompts/coordinator.js +0 -3
  170. package/dist/stores/file/checkpoint-store.js +1 -5
  171. package/dist/stores/file/file-history-store.js +2 -2
  172. package/dist/stores/file/permission-rule-store.d.ts +4 -35
  173. package/dist/stores/file/permission-rule-store.js +4 -148
  174. package/dist/tools/artifact/artifact-text.d.ts +62 -0
  175. package/dist/tools/artifact/artifact-text.js +68 -0
  176. package/dist/tools/artifact/artifact-tool.d.ts +52 -0
  177. package/dist/tools/artifact/artifact-tool.js +351 -0
  178. package/dist/tools/artifact/local-stub.d.ts +26 -0
  179. package/dist/tools/artifact/local-stub.js +296 -0
  180. package/dist/tools/fs/bash-readonly-classifier.d.ts +74 -8
  181. package/dist/tools/fs/bash-readonly-classifier.js +171 -60
  182. package/dist/tools/fs/fs-bash.d.ts +14 -10
  183. package/dist/tools/fs/fs-bash.js +19 -27
  184. package/dist/tools/fs/fs-search-tools.js +5 -2
  185. package/dist/tools/fs/fs-shared.d.ts +2 -0
  186. package/dist/tools/fs/fs-shared.js +3 -11
  187. package/dist/tools/fs/index.d.ts +4 -0
  188. package/dist/tools/fs/index.js +4 -1
  189. package/dist/tools/fs/safety.d.ts +117 -1
  190. package/dist/tools/fs/safety.js +131 -31
  191. package/dist/tools/fs/search.js +8 -6
  192. package/package.json +2 -1
  193. package/test/export-surface.snapshot.json +115 -39
@@ -67,7 +67,7 @@ export declare class TransportLspSession implements LspSession {
67
67
  * it REPORTS failure instead (`{ok:false}`) — an unreadable file no longer looks like a clean sync to the
68
68
  * caller. Concurrent callers share one in-flight sync, each keeping its OWN cancellation (HRD-LSP-12). */
69
69
  private syncOne;
70
- /** RB-232: structural fallback classification for errors carrying no {@link lspFailureOf} brand (a
70
+ /** Structural fallback classification for errors carrying no {@link lspFailureOf} brand (a
71
71
  * third-party transport, e.g. the TOB WS lane) — abort signal and transport liveness are the only
72
72
  * transport-neutral facts available; everything else is a server fault. Cannot produce
73
73
  * "unsupported_operation" until that transport brands its JSON-RPC error codes (cross-repo slip). */
@@ -50,7 +50,7 @@ export interface LspSymbolInfo {
50
50
  positionUnknown?: boolean;
51
51
  }
52
52
  /**
53
- * RB-232: WHY a "none" result was "none". Absent `reason` = a GENUINE empty answer (the server
53
+ * WHY a "none" result was "none". Absent `reason` = a GENUINE empty answer (the server
54
54
  * answered null/[] — today's behavior byte-for-byte, and what an external manager returning bare
55
55
  * `{kind:"none"}` keeps getting). The failure reasons exist because one literal "No results." used to cover
56
56
  * both real emptiness and every transport-level fault — a findReferences that TIMED OUT read as "no
package/dist/core/lsp.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { uriToPath } from "./lsp-protocol.js";
2
- import { foldRedundantPathSegments, isAbsolutePathForm } from "../tools/fs/safety.js";
2
+ import { foldRedundantPathSegments, isAbsoluteForFamily, joinForFamily, pathFamilyOf } from "../tools/fs/safety.js";
3
3
  import { Type } from "typebox";
4
4
  import { defineTool, errorResult } from "./tools.js";
5
5
  import { toolFace } from "./tool-catalog-entries.js";
@@ -69,10 +69,11 @@ export function settleOnAbort(job, signal, onAbort) {
69
69
  });
70
70
  }
71
71
  export function resolveLspPath(filePath, cwd) {
72
- const isAbsForm = isAbsolutePathForm(filePath);
73
- if (isAbsForm || !cwd)
72
+ const family = cwd === undefined ? undefined : pathFamilyOf({ root: cwd });
73
+ const isAbsForm = family !== undefined && isAbsoluteForFamily(family, filePath);
74
+ if (isAbsForm || !cwd || family === undefined)
74
75
  return foldRedundantPathSegments(filePath);
75
- return foldRedundantPathSegments(cwd.replace(/[\\/]+$/, "") + (cwd.includes("\\") ? "\\" : "/") + filePath);
76
+ return foldRedundantPathSegments(joinForFamily(family, cwd, filePath));
76
77
  }
77
78
  export const MAX_LSP_FILE_BYTES = 10 * 1000 * 1000;
78
79
  function fmt(result, ctx) {
@@ -168,7 +168,7 @@ export interface MaterializedMcp {
168
168
  * A consumer (service) surfacing it as session status must understand that. `toolNames` are the namespaced
169
169
  * (`<server>__<tool>`) names exposed.
170
170
  *
171
- * RB-437-a — READ THIS AS A SNAPSHOT, NOT A HEARTBEAT. `status` is the connect-time verdict and is never
171
+ * READ THIS AS A SNAPSHOT, NOT A HEARTBEAT. `status` is the connect-time verdict and is never
172
172
  * rewritten: a server that dies mid-task still reads `connected` here, because that IS what happened at
173
173
  * materialize. The one live bit is {@link McpServerStatus.transportClosed} (set if and when core observes
174
174
  * the transport go away — see its own note for the reach). For an authoritative answer about a server's
@@ -231,7 +231,7 @@ export interface McpRefreshResult {
231
231
  */
232
232
  error?: string;
233
233
  /**
234
- * #381 — present when this entry's `tools/list` walk did NOT reach the end of pagination. `tools` is
234
+ * Present when this entry's `tools/list` walk did NOT reach the end of pagination. `tools` is
235
235
  * then a MERGE: everything the partial listing did return, plus the previously-mounted entries whose
236
236
  * absence it cannot prove (so the consumer's replace-the-whole-prefix swap withdraws nothing, and
237
237
  * `removed: []` describes a roster that really lost nothing). Absent on a complete walk and on every
@@ -267,7 +267,7 @@ export interface McpServerStatus {
267
267
  delivered?: McpDelivered;
268
268
  httpStatus?: number;
269
269
  /**
270
- * RB-437-a — set to `true` once core OBSERVES this server's transport close during the task; absent
270
+ * Set to `true` once core OBSERVES this server's transport close during the task; absent
271
271
  * otherwise (never `false`: absence means "no close was observed", which is not the same as "alive").
272
272
  * It mirrors exactly the flag core's own fail-fast path keys on, so `transportClosed === true` also
273
273
  * means every further call to this server's tools/resources is refused without dialing.
@@ -376,7 +376,7 @@ export declare function mcpStartupTimeoutMs(): number | undefined;
376
376
  * (see {@link MCP_SPEC_ERROR_CODE_NAMES}). */
377
377
  export declare function describeMcpSpecErrorCode(code: unknown): string | undefined;
378
378
  /**
379
- * RB-437-c — the SDK stamps `MCP error <code>: ` onto an McpError's message IN ITS CONSTRUCTOR, and it
379
+ * The SDK stamps `MCP error <code>: ` onto an McpError's message IN ITS CONSTRUCTOR, and it
380
380
  * does so at BOTH ends of the wire: a server built on an MCP SDK throws an McpError, its already-stamped
381
381
  * `.message` goes out as the JSON-RPC `error.message`, and the client constructs its own McpError over
382
382
  * that string — so the failure reaches the model as `MCP error -32602: MCP error -32602: …`. Collapse a
@@ -582,7 +582,7 @@ export declare function classifyDirReadInvalidParams(message: string): "not_foun
582
582
  */
583
583
  export declare function listEntryFingerprint(entry: unknown): string | undefined;
584
584
  /**
585
- * #381 — walk a paginated MCP list method to the END (`{cursor?}` → `{items, nextCursor?}`), bounded.
585
+ * Walk a paginated MCP list method to the END (`{cursor?}` → `{items, nextCursor?}`), bounded.
586
586
  *
587
587
  * Every spec-standard MCP list method is paginated the same way: absence of `nextCursor` means "that
588
588
  * was the last page". A single request therefore returns the WHOLE list only for servers that choose
package/dist/core/mcp.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { redactSecrets } from "./untrusted-egress.js";
2
- import { announceEnvKnob, __resetEnvKnobAnnouncements } from "./env-knob-announce.js";
2
+ import { __resetEnvKnobAnnouncements } from "./env-knob-announce.js";
3
+ import { resolveEnvBudget } from "./env-budget.js";
3
4
  import { MCP_NAMESPACE } from "./protocol-table.js";
4
5
  import { findNamespacePrefixCollision, mintNamespacePrefix, mintNamespacedToolName, normalizeNameSegment } from "./protocol-naming.js";
5
6
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
@@ -31,18 +32,7 @@ export function resolveMcpDeclaredResultSize(meta) {
31
32
  return Math.min(declared, MCP_META_RESULT_SIZE_CAP);
32
33
  }
33
34
  function mcpMaxOutputTokens() {
34
- const rawEnv = process.env.MAX_MCP_OUTPUT_TOKENS;
35
- if (rawEnv === undefined)
36
- return MCP_OUTPUT_TOKENS_DEFAULT;
37
- const raw = rawEnv.trim();
38
- if (raw === "")
39
- return MCP_OUTPUT_TOKENS_DEFAULT;
40
- const n = parseWholeNumber(raw);
41
- if (n === undefined || n <= 0) {
42
- announceEnvKnob(`MAX_MCP_OUTPUT_TOKENS=${rawEnv} was ignored — it is not a positive whole number of tokens. Using ${MCP_OUTPUT_TOKENS_DEFAULT} instead.`);
43
- return MCP_OUTPUT_TOKENS_DEFAULT;
44
- }
45
- return n;
35
+ return resolveEnvBudget(process.env.MAX_MCP_OUTPUT_TOKENS, { name: "MAX_MCP_OUTPUT_TOKENS", unit: "tokens", default: MCP_OUTPUT_TOKENS_DEFAULT });
46
36
  }
47
37
  function mcpTruncationNote(limitTokens) {
48
38
  return (`\n\n[OUTPUT TRUNCATED - exceeded ${limitTokens} token limit]\n\n` +
@@ -114,45 +104,11 @@ export function truncateMcpErrorText(s) {
114
104
  export const MCP_TOOL_TIMEOUT_DEFAULT_MS = 100_000_000;
115
105
  export const MCP_ENV_MS_MIN = 1_000;
116
106
  export const MCP_ENV_MS_MAX = 2_147_483_647;
117
- const ENV_SCIENTIFIC_RE = /^[+-]?(\d+(\.\d*)?|\.\d+)[eE][+-]?\d+$/;
118
- const ENV_GROUPED_RE = /^[+-]?\d{1,3}([_,\u00A0\u202F ])\d{3}(?:\1\d{3})*$/;
119
- const ENV_GROUP_SEPARATORS_RE = /[_,\u00A0\u202F ]/g;
120
- const ENV_NUMERIC_MAX_LEN = 32;
121
- function parseWholeNumber(raw) {
122
- if (/^[+-]?\d+$/.test(raw)) {
123
- const n = Number(raw);
124
- return Number.isSafeInteger(n) ? n : undefined;
125
- }
126
- if (raw.length > ENV_NUMERIC_MAX_LEN)
127
- return undefined;
128
- if (ENV_SCIENTIFIC_RE.test(raw)) {
129
- const n = Number(raw);
130
- return Number.isSafeInteger(n) ? n : undefined;
131
- }
132
- if (ENV_GROUPED_RE.test(raw))
133
- return parseInt(raw.replace(ENV_GROUP_SEPARATORS_RE, ""), 10);
134
- return undefined;
135
- }
136
107
  export function __resetMcpEnvAnnouncements() {
137
108
  __resetEnvKnobAnnouncements();
138
109
  }
139
110
  function parseEnvMs(name) {
140
- const rawEnv = process.env[name];
141
- if (rawEnv === undefined)
142
- return undefined;
143
- const raw = rawEnv.trim();
144
- if (raw === "")
145
- return undefined;
146
- const n = parseWholeNumber(raw);
147
- if (n === undefined || n <= 0) {
148
- announceEnvKnob(`${name}=${rawEnv} was ignored — it is not a positive whole number of milliseconds. Using the built-in default instead.`);
149
- return undefined;
150
- }
151
- const clamped = Math.min(Math.max(n, MCP_ENV_MS_MIN), MCP_ENV_MS_MAX);
152
- if (clamped !== n) {
153
- announceEnvKnob(`${name}=${rawEnv} is outside the range this runtime can honor (${MCP_ENV_MS_MIN}..${MCP_ENV_MS_MAX} ms). Using ${clamped}ms instead.`);
154
- }
155
- return clamped;
111
+ return resolveEnvBudget(process.env[name], { name, unit: "milliseconds", default: undefined, min: MCP_ENV_MS_MIN, max: MCP_ENV_MS_MAX });
156
112
  }
157
113
  export function mcpToolTimeoutMs() {
158
114
  return parseEnvMs("MCP_TOOL_TIMEOUT") ?? MCP_TOOL_TIMEOUT_DEFAULT_MS;
@@ -177,14 +177,6 @@ export declare function composeSelectiveBody(manifestText: string, selected: Mem
177
177
  * widen the public surface (and invite callers to parse the opaque format) for zero consumer. Treat the key
178
178
  * as an opaque token: persist it, feed it back, compare for equality — never split it. */
179
179
  export declare function encodeSurfacedKey(scope: string, id: string): string;
180
- /** A manifest header tagged with the scope it came from (design/84 Seam A decision 5). */
181
- export interface ScopedNoteHeader extends MemoryNoteHeader {
182
- scope: string;
183
- }
184
- /** A full record tagged with its scope (for cross-scope getByIds). */
185
- export interface ScopedNoteRecord extends MemoryNoteRecord {
186
- scope: string;
187
- }
188
180
  export interface LayeredRecallOptions {
189
181
  store: MemoryStore;
190
182
  /** Ordered scopes (design/84 Seam A): list order = priority; the last is highest. */
@@ -233,7 +233,7 @@ export type ConsolidationCapableStore = MemoryStore & Required<Pick<MemoryStore,
233
233
  * (design/41): {@link MemoryStore.searchScored}/{@link MemoryStore.update}/{@link MemoryStore.delete}.
234
234
  * When false, the Runner skips consolidation entirely (graceful no-op).
235
235
  *
236
- * RB-436-g: a TYPE PREDICATE, so the guard's conclusion travels to the call sites. A bare boolean left
236
+ * A TYPE PREDICATE, so the guard's conclusion travels to the call sites. A bare boolean left
237
237
  * every post-guard call site writing `store.update!(…)`, and a guard that later forgot one of the three
238
238
  * would have been invisible — the `!`s keep compiling. Now dropping a method from the check turns the
239
239
  * call sites red.
@@ -1,5 +1,5 @@
1
1
  import { isRemoteExecutionEnv, isIsolated } from "./remote-env.js";
2
- import { canonicalizeTarget } from "../tools/fs/safety.js";
2
+ import { canonicalizeTarget, pathFamilyOf } from "../tools/fs/safety.js";
3
3
  function probeCommand(canonOraclePath) {
4
4
  const q = `'${canonOraclePath.replace(/'/g, `'\\''`)}'`;
5
5
  return `touch -- ${q}/.oracle-isolation-probe 2>/dev/null`;
@@ -31,7 +31,7 @@ export async function assertOracleIsolation(workerEnv, grader, opts) {
31
31
  }
32
32
  let writable;
33
33
  try {
34
- const canon = await canonicalizeTarget(workerEnv, oraclePath, opts?.signal);
34
+ const canon = await canonicalizeTarget(workerEnv, oraclePath, opts?.signal, pathFamilyOf({ root: oraclePath }));
35
35
  if (!canon.ok) {
36
36
  return { protected: false, isolationClass, reason: `cannot canonicalize oracle path "${oraclePath}": ${canon.message}` };
37
37
  }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The parent SPEC seat a run hands its children — the projection of the fields of the parent's own REBOUND spec
3
+ * that inherit copy-at-spawn (identity, process facts, deployment facts, the frozen question face) onto ONE value
4
+ * both delegation lanes copy onto a child spec: `ToolExecuteContext.parentSpecSeat` on the Task lane,
5
+ * `RunWorkflowToolDeps.parentSpecSeat` → `RunWorkflowOptions.parentSpecSeat` on the workflow lane. Minted once per
6
+ * prepare (the inherited-gate phase); applied by ONE function ({@link applyParentSpecSeat}) so the two lanes
7
+ * cannot drift on which fields travel or on the priority between a child's own value and the parent's.
8
+ *
9
+ * The checkpoint seat (`./checkpoint-seat.ts`) is the same shape for one field; this seat carries the rest.
10
+ */
11
+ import type { OnQuestion } from "./ask-question.js";
12
+ import type { ToolExecuteContext } from "./tool-spec.js";
13
+ import type { TaskSpec } from "./types.js";
14
+ /** The fields of a spec the seat carries — one `Pick`, shared by the mint's input and the apply's target. */
15
+ export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "clientContext" | "onQuestion" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable">;
16
+ /**
17
+ * The seat a run hands its children. Every member is the parent's own value, frozen at mint (arrays copied and
18
+ * frozen; the facts object copied). Members present iff the parent carries them — an absent member is "nothing to
19
+ * inherit", never a value.
20
+ * · `principal` — the end-user identity (design/62: inherits verbatim, a worker cannot delegate under another).
21
+ * · `oneShot` — the process fact ("no later turn"): only `true` travels; it is a REMOVAL (see the apply law).
22
+ * · `clientContext` — the user's TZ/email facts (the child's environment block localizes the same way).
23
+ * · `onQuestion` — the parent's FROZEN question face (`spec.onQuestion ?? deps.onQuestion`, the value the parent's
24
+ * own AskUserQuestion resolves at): the child's question tool MOUNTS on it, so without it the child has no tool.
25
+ * · `additionalDirectories` / `additionalReadDirectories` — the fs-fence widening (deployment property).
26
+ * · `envFacts` — the sandbox facts (profile / egress / scratchpad).
27
+ * · `memoryPersistenceCapable` — the deployment's DECLARED persistence statement: `false` is a REMOVAL (floor),
28
+ * `true` a default a child's own declaration may narrow.
29
+ */
30
+ export interface ParentSpecSeat {
31
+ readonly principal?: string;
32
+ readonly oneShot?: true;
33
+ readonly clientContext?: TaskSpec["clientContext"];
34
+ readonly onQuestion?: OnQuestion;
35
+ readonly additionalDirectories?: readonly string[];
36
+ readonly additionalReadDirectories?: readonly string[];
37
+ readonly envFacts?: TaskSpec["envFacts"];
38
+ readonly memoryPersistenceCapable?: boolean;
39
+ }
40
+ /** Mint the seat off the parent's REBOUND spec and its frozen question face (spec over deps — the caller resolves it once,
41
+ * the same value the parent's own question routing reads). Absent fields stay absent; `oneShot` only when `true`. */
42
+ export declare function parentSpecSeatOf(spec: Omit<ParentSpecSeatFields, "onQuestion">, frozenOnQuestion: OnQuestion | undefined): ParentSpecSeat;
43
+ /**
44
+ * Apply the seat to a child spec — ONE law for every member, both lanes:
45
+ * a value the child spec already carries WINS; the seat FILLS an unset one; the two REMOVAL values
46
+ * (`oneShot: true`, `memoryPersistenceCapable: false`) land UNCONDITIONALLY.
47
+ * "Already carries" is the child's own authorship — a deployment baseline, a trusted script, an agent definition's
48
+ * fold (the Task lane folds the definition's `memoryPersistenceCapable` before applying; the workflow lane applies at
49
+ * the launch site AFTER the agentType fold) — so a definition may narrow a parent `true` to `false`, a baseline-pinned
50
+ * principal stands, and nothing a child can author lifts a removal. Arrays and the facts object are copied per child
51
+ * (a retained child's frozen snapshot must not alias the parent's seat). Returns a NEW object; `child` is not mutated.
52
+ * No seat (a tool running outside a Runner task) ⇒ the child spec is returned as is.
53
+ */
54
+ export declare function applyParentSpecSeat<S extends ParentSpecSeatFields>(child: S, seat: ParentSpecSeat | undefined): S;
55
+ /** The ctx keys that restate a seat member as THIS run's own fact (tools read them for scope / receipts / clamps). */
56
+ declare const SEAT_FACTS_ON_CTX: readonly ["principal", "oneShot", "onQuestion", "memoryPersistenceCapable"];
57
+ /**
58
+ * The seat a delegation boundary reads off a trusted ctx — ONE reader for both delegation tools. A Runner-filled ctx
59
+ * always carries the seat beside the run's own facts, and the two agree by construction (one mint). A HAND-BUILT ctx
60
+ * (a caller invoking a delegation tool's `execute` directly) that carries any of those facts WITHOUT the seat, or with
61
+ * a seat that disagrees with them, is REFUSED (`config.parent_spec_seat_missing`): spawning a principal-less child from a
62
+ * principal-bearing caller would offload into the shared namespace and re-key its journal — a silent tenant narrowing,
63
+ * never a fallback. A ctx with neither (a tool running outside a Runner task) hands down no seat.
64
+ */
65
+ export declare function parentSpecSeatOfCtx(ctx: Pick<ToolExecuteContext, "parentSpecSeat" | (typeof SEAT_FACTS_ON_CTX)[number]>): ParentSpecSeat | undefined;
66
+ export {};
@@ -0,0 +1,47 @@
1
+ export function parentSpecSeatOf(spec, frozenOnQuestion) {
2
+ return Object.freeze({
3
+ ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
4
+ ...(spec.oneShot === true ? { oneShot: true } : {}),
5
+ ...(spec.clientContext !== undefined ? { clientContext: spec.clientContext } : {}),
6
+ ...(frozenOnQuestion !== undefined ? { onQuestion: frozenOnQuestion } : {}),
7
+ ...(spec.additionalDirectories !== undefined ? { additionalDirectories: Object.freeze([...spec.additionalDirectories]) } : {}),
8
+ ...(spec.additionalReadDirectories !== undefined ? { additionalReadDirectories: Object.freeze([...spec.additionalReadDirectories]) } : {}),
9
+ ...(spec.envFacts !== undefined ? { envFacts: Object.freeze({ ...spec.envFacts }) } : {}),
10
+ ...(spec.memoryPersistenceCapable !== undefined ? { memoryPersistenceCapable: spec.memoryPersistenceCapable } : {}),
11
+ });
12
+ }
13
+ export function applyParentSpecSeat(child, seat) {
14
+ if (seat === undefined)
15
+ return child;
16
+ const fill = (key, value) => child[key] === undefined && value !== undefined ? { [key]: value } : {};
17
+ return {
18
+ ...child,
19
+ ...fill("principal", seat.principal),
20
+ ...fill("clientContext", seat.clientContext),
21
+ ...fill("onQuestion", seat.onQuestion),
22
+ ...fill("additionalDirectories", (seat.additionalDirectories !== undefined ? [...seat.additionalDirectories] : undefined)),
23
+ ...fill("additionalReadDirectories", (seat.additionalReadDirectories !== undefined ? [...seat.additionalReadDirectories] : undefined)),
24
+ ...fill("envFacts", (seat.envFacts !== undefined ? { ...seat.envFacts } : undefined)),
25
+ ...(seat.oneShot === true ? { oneShot: true } : {}),
26
+ ...(seat.memoryPersistenceCapable === false ? { memoryPersistenceCapable: false } : fill("memoryPersistenceCapable", seat.memoryPersistenceCapable)),
27
+ };
28
+ }
29
+ const SEAT_FACTS_ON_CTX = ["principal", "oneShot", "onQuestion", "memoryPersistenceCapable"];
30
+ export function parentSpecSeatOfCtx(ctx) {
31
+ const seat = ctx.parentSpecSeat;
32
+ const facts = SEAT_FACTS_ON_CTX.filter((k) => ctx[k] !== undefined);
33
+ if (seat === undefined) {
34
+ if (facts.length === 0)
35
+ return undefined;
36
+ throw seatRefusal(`the tool context carries ${facts.join(" / ")} but no \`parentSpecSeat\` — a delegating caller hands its children the seat it was minted (a Runner-filled ctx always carries it); spawning without it would silently drop the identity`);
37
+ }
38
+ const disagree = facts.filter((k) => (k === "oneShot" ? (ctx.oneShot === true) !== (seat.oneShot === true) : ctx[k] !== seat[k]));
39
+ if (disagree.length > 0)
40
+ throw seatRefusal(`the tool context's ${disagree.join(" / ")} disagree with its \`parentSpecSeat\` — one mint, one value`);
41
+ return seat;
42
+ }
43
+ function seatRefusal(message) {
44
+ const e = new Error(`parentSpecSeat: ${message}`);
45
+ e.code = "config.parent_spec_seat_missing";
46
+ return e;
47
+ }
@@ -73,6 +73,8 @@
73
73
  * `Bash(git status:*)`) — the last two are strict-side divergences from upstream, registered as such.
74
74
  */
75
75
  import type { AssertAllKeysHandled } from "./ask-origin.js";
76
+ import { type PathFamily, type PathIdentity } from "../tools/fs/safety.js";
77
+ import type { PathTargetReading } from "./effective-path-target.js";
76
78
  /**
77
79
  * The closed set of rule BEHAVIORS — what a matching rule says about the call. The upstream vocabulary
78
80
  * verbatim (`allow` / `deny` / `ask`, the three settings lists), in PRECEDENCE order: when rules of more
@@ -538,7 +540,7 @@ export type PathRuleBaseLabelCoversEveryBase = AssertAllKeysHandled<Exclude<Path
538
540
  export declare function ruleBasesNeeded(rule: Pick<PersistedRule, "match" | "command">): readonly PathRuleBase[];
539
541
  /** Is this base usable by {@link resolvePathPattern} — an absolute path with a lexical normal form? A rule
540
542
  * compiler asks this at compile time so a pattern is refused rather than compiled inert. */
541
- export declare function isUsablePathBase(base: string | undefined): boolean;
543
+ export declare function isUsablePathBase(base: string | undefined, family: PathFamily | undefined): boolean;
542
544
  /**
543
545
  * The PATH family's tightening reach — three-valued like the command family's ({@link programRunReachOf}):
544
546
  * does this deny/ask rule reach the call's target path? The target is the caller's ALREADY-RESOLVED
@@ -555,7 +557,7 @@ export declare function isUsablePathBase(base: string | undefined): boolean;
555
557
  * defect (a `~/` deny falling back to the engine host's home guarded the wrong directory; a `/…` deny with
556
558
  * no root reached nothing at all).
557
559
  */
558
- export declare function pathRuleReachOf(rule: Pick<PersistedRule, "match" | "command" | "behavior">, target: string, bases: PathRuleBases): ProgramRunReachOutcome;
560
+ export declare function pathRuleReachOf(rule: Pick<PersistedRule, "match" | "command" | "behavior">, target: PathIdentity, bases: PathRuleBases): ProgramRunReachOutcome;
559
561
  /**
560
562
  * Does this rule's command pattern admit `command`?
561
563
  *
@@ -692,7 +694,7 @@ export declare function pathWithinRoot(path: string, root: string): boolean;
692
694
  * bodies never come through here (their canonical spelling is unique and non-normal spellings are
693
695
  * refused at the validator); this is for the comparison INPUT a caller derives from a live call.
694
696
  */
695
- export declare function lexicalNormalAbsolutePathOf(path: string): string | undefined;
697
+ export declare function lexicalNormalAbsolutePathOf(path: string, family: PathFamily | undefined, anchor?: string): PathIdentity | undefined;
696
698
  /**
697
699
  * design/382 §2.5 — the ONE admission predicate of the Read directory rule family, beside
698
700
  * {@link ruleAdmitsCommand} as its path-lane sibling. Four consumers share it (the minting round
@@ -715,7 +717,10 @@ export declare function lexicalNormalAbsolutePathOf(path: string): string | unde
715
717
  * there is nothing here for it to clear). As a DENY/ASK the same containment refuses or questions every
716
718
  * path under the directory, on whichever path tool the rule names.
717
719
  */
718
- export declare function directoryRuleAdmits(rule: Pick<PersistedRule, "match" | "command">, path: string): boolean;
720
+ export declare function directoryRuleAdmits(rule: Pick<PersistedRule, "match" | "command">, path: PathIdentity, bases: PathRuleBases): boolean;
721
+ /** The identity of a directory SPELLING (the cd resolver's `/`-rooted output) under the call's tree, for the
722
+ * consumers that hand {@link directoryRuleAdmits} a spelling; a rule minted from it stores this form. */
723
+ export declare function directoryIdentityOf(spelling: string, bases: PathRuleBases): PathIdentity | undefined;
719
724
  /** Does a rule's scope cover a task running in `cwd` (and, for the session dimension, in the session
720
725
  * named by `sessionId`)? A project rule needs a cwd to compare against; without one it covers nothing
721
726
  * (fail-closed). A session rule (design/382 §4.3) covers a call iff the call's `sessionId` equals the
@@ -872,8 +877,8 @@ export declare function adjudicatePersistedPathRules(rules: readonly PersistedRu
872
877
  cwd: string | undefined;
873
878
  sessionId?: string;
874
879
  }, targets: {
875
- readonly tighten?: string;
876
- readonly allow?: string;
880
+ readonly tighten?: PathTargetReading;
881
+ readonly allow?: PathTargetReading;
877
882
  }, bases: PathRuleBases): PersistedRuleVerdict | undefined;
878
883
  /**
879
884
  * design/375 §5.2 — the per-segment coverage table for `command`: which segments an eligible rule
@@ -4,6 +4,7 @@ import { compileReadDeny } from "../tools/fs/read-deny.js";
4
4
  import { carriesShellRedirection, parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
5
5
  import { inlineUntrusted } from "./untrusted-text.js";
6
6
  import { pathTargetOf } from "./tool-registry.js";
7
+ import { carriesDotSegment, isAbsoluteForFamily, isIdentityDir, isPathIdentity, isShellRootedSpellingUnmapped, isUncIdentity, pathFamilyOf, pathIdentityOf } from "../tools/fs/safety.js";
7
8
  import { readShellCommand } from "./shell-lexer.js";
8
9
  export const RULE_BEHAVIORS = ["deny", "ask", "allow"];
9
10
  const RULE_BEHAVIOR_SET = new Set(RULE_BEHAVIORS);
@@ -364,12 +365,18 @@ function baseValueOf(base, bases) {
364
365
  }
365
366
  }
366
367
  function resolvePathPattern(pattern, bases, behavior) {
367
- if (pattern.startsWith("//"))
368
- return { resolved: "/" + pattern.slice(2) };
368
+ const family = pathFamilyOf(bases);
369
+ const tree = bases.root ?? bases.cwd;
370
+ if (pattern.startsWith("//")) {
371
+ const abs = pathIdentityOf("/" + pattern.slice(2), family, tree);
372
+ return abs.ok ? { resolved: abs.identity } : { missingBase: "root" };
373
+ }
369
374
  const needed = ruleBasesNeeded({ match: "path", command: pattern })[0];
370
- const normal = lexicalNormalAbsolutePathOf(baseValueOf(needed, bases) ?? "");
371
- if (normal === undefined)
375
+ const baseValue = baseValueOf(needed, bases);
376
+ const base = baseValue !== undefined && isAbsoluteForFamily(family, baseValue) ? pathIdentityOf(baseValue, family, tree) : undefined;
377
+ if (base === undefined || !base.ok)
372
378
  return { missingBase: needed };
379
+ const normal = base.identity;
373
380
  const rest = pattern.startsWith("~/") ? pattern.slice(2) : pattern.startsWith("/") ? pattern.slice(1) : pattern.startsWith("./") ? pattern.slice(2) : pattern;
374
381
  const peeled = rest.endsWith("/**") ? rest.slice(0, -3) : rest;
375
382
  const bare = needed === "cwd" && peeled !== "" && !peeled.includes("/");
@@ -377,8 +384,8 @@ function resolvePathPattern(pattern, bases, behavior) {
377
384
  const body = anyDepth ? "**/" + peeled : rest;
378
385
  return { resolved: (normal === "/" ? "" : normal) + "/" + body };
379
386
  }
380
- export function isUsablePathBase(base) {
381
- return base !== undefined && lexicalNormalAbsolutePathOf(base) !== undefined;
387
+ export function isUsablePathBase(base, family) {
388
+ return base !== undefined && isAbsoluteForFamily(family, base) && pathIdentityOf(base, family, base).ok;
382
389
  }
383
390
  function segmentGlobMatches(glob, segment) {
384
391
  if (!glob.includes("*"))
@@ -422,15 +429,18 @@ function globSegmentsReach(globs, target) {
422
429
  }
423
430
  export function pathRuleReachOf(rule, target, bases) {
424
431
  if (rule.match === "subpath")
425
- return directoryRuleAdmits(rule, target) ? REACHED : NOT_REACHED;
432
+ return directoryRuleAdmits(rule, target, bases) ? REACHED : NOT_REACHED;
426
433
  if (rule.match !== "path")
427
434
  return NOT_REACHED;
428
- if (!isLexicalNormalAbsoluteDir(target) && target !== "/")
435
+ const family = pathFamilyOf(bases);
436
+ if (!isPathIdentity(target, family))
429
437
  return NOT_REACHED;
430
438
  const r = resolvePathPattern(rule.command, bases, rule.behavior);
431
439
  if ("missingBase" in r) {
432
- return { reach: "unreadable", reason: `the rule is relative to ${PATH_RULE_BASE_LABEL[r.missingBase]} and this call supplies no absolute \`${r.missingBase}\` base to resolve it against` };
440
+ return { reach: "unreadable", reason: `the rule is relative to ${PATH_RULE_BASE_LABEL[r.missingBase]} and this call supplies no absolute \`${r.missingBase}\` base of this tree's family to resolve it against` };
433
441
  }
442
+ if (isUncIdentity(r.resolved) !== isUncIdentity(target))
443
+ return NOT_REACHED;
434
444
  const globs = r.resolved.split("/").filter((sg) => sg !== "");
435
445
  const segments = target.split("/").filter((sg) => sg !== "");
436
446
  return globSegmentsReach(globs, segments) ? REACHED : NOT_REACHED;
@@ -587,21 +597,9 @@ export function pathWithinRoot(path, root) {
587
597
  const base = root.endsWith("/") ? root : root + "/";
588
598
  return path.startsWith(base);
589
599
  }
590
- export function lexicalNormalAbsolutePathOf(path) {
591
- if (typeof path !== "string" || !path.startsWith("/"))
592
- return undefined;
593
- const out = [];
594
- for (const seg of path.split("/")) {
595
- if (seg === "" || seg === ".")
596
- continue;
597
- if (seg === "..") {
598
- if (out.length > 0)
599
- out.pop();
600
- continue;
601
- }
602
- out.push(seg);
603
- }
604
- return out.length === 0 ? "/" : "/" + out.join("/");
600
+ export function lexicalNormalAbsolutePathOf(path, family, anchor) {
601
+ const r = pathIdentityOf(path, family, anchor);
602
+ return r.ok ? r.identity : undefined;
605
603
  }
606
604
  function isLexicalNormalAbsoluteDir(path) {
607
605
  if (typeof path !== "string" || !path.startsWith("/") || path.startsWith("//"))
@@ -609,14 +607,22 @@ function isLexicalNormalAbsoluteDir(path) {
609
607
  const segments = path.split("/").slice(1);
610
608
  return segments.length > 0 && segments.every((s) => s !== "" && s !== "." && s !== "..");
611
609
  }
612
- export function directoryRuleAdmits(rule, path) {
610
+ export function directoryRuleAdmits(rule, path, bases) {
613
611
  if (rule.match !== "subpath")
614
612
  return false;
615
- if (!isLexicalNormalAbsoluteDir(rule.command))
613
+ const family = pathFamilyOf(bases);
614
+ if (carriesDotSegment(rule.command, family))
615
+ return false;
616
+ const dir = pathIdentityOf(rule.command, family, bases.root ?? bases.cwd);
617
+ if (!dir.ok || !isIdentityDir(dir.identity, family))
616
618
  return false;
617
- if (!isLexicalNormalAbsoluteDir(path))
619
+ if (!isPathIdentity(path, family) || !isIdentityDir(path, family))
618
620
  return false;
619
- return pathWithinRoot(path, rule.command);
621
+ return pathWithinRoot(path, dir.identity);
622
+ }
623
+ export function directoryIdentityOf(spelling, bases) {
624
+ const r = pathIdentityOf(spelling, pathFamilyOf(bases), bases.root ?? bases.cwd);
625
+ return r.ok ? r.identity : undefined;
620
626
  }
621
627
  const CD_TARGET_REJECT_CHARS = /[*?[\]{}$`\\'"<>\n\r]/;
622
628
  function cdSegmentDirectoryOf(segment, opts) {
@@ -711,7 +717,8 @@ function resolveCdSegmentDirectories(segments, cwd) {
711
717
  const dir = cdSegmentDirectoryOf(segment, { cwd, priorCd });
712
718
  if (!priorCd && movesShellCursor(segment))
713
719
  priorCd = true;
714
- return dir;
720
+ const bases = basesOfCwd(cwd);
721
+ return dir === undefined || isShellRootedSpellingUnmapped(pathFamilyOf(bases), dir) ? undefined : directoryIdentityOf(dir, bases);
715
722
  });
716
723
  }
717
724
  export function scopeCoversCwd(scope, cwd, sessionId) {
@@ -767,11 +774,14 @@ function firstEligibleDirectoryRule(rules, directory, call) {
767
774
  for (const rule of rules) {
768
775
  if (!eligiblePersisted(rule, { tool: "Read", cwd: call.cwd, sessionId: call.sessionId }, "allow"))
769
776
  continue;
770
- if (directoryRuleAdmits(rule, directory))
777
+ if (directoryRuleAdmits(rule, directory, basesOfCwd(call.execCwd ?? call.cwd)))
771
778
  return rule;
772
779
  }
773
780
  return undefined;
774
781
  }
782
+ function basesOfCwd(cwd) {
783
+ return cwd === undefined ? {} : { root: cwd };
784
+ }
775
785
  export function adjudicatePersistedRules(rules, call) {
776
786
  for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
777
787
  if (behavior === "allow") {
@@ -798,23 +808,29 @@ export function adjudicatePersistedRules(rules, call) {
798
808
  export function adjudicatePersistedPathRules(rules, call, targets, bases) {
799
809
  for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
800
810
  if (behavior === "allow") {
801
- if (targets.allow === undefined || call.tool !== READ_RULE_TOOL)
811
+ if (targets.allow?.kind !== "path" || call.tool !== READ_RULE_TOOL)
802
812
  continue;
803
813
  for (const rule of rules) {
804
814
  if (!eligiblePersisted(rule, call, behavior))
805
815
  continue;
806
- if (directoryRuleAdmits(rule, targets.allow))
816
+ if (directoryRuleAdmits(rule, targets.allow.identity, bases))
807
817
  return { behavior, rules: [rule] };
808
818
  }
809
819
  continue;
810
820
  }
811
- if (targets.tighten === undefined)
821
+ const tighten = targets.tighten;
822
+ if (tighten === undefined || tighten.kind === "refused")
812
823
  continue;
813
824
  let unreadable;
814
825
  for (const rule of rules) {
815
826
  if (!eligiblePersisted(rule, call, behavior))
816
827
  continue;
817
- const outcome = pathRuleReachOf(rule, targets.tighten, bases);
828
+ if (tighten.kind === "unreadable") {
829
+ if (rule.match === "path" || rule.match === "subpath")
830
+ unreadable ??= { behavior: "ask", rules: [rule], unreadable: tighten.reason };
831
+ continue;
832
+ }
833
+ const outcome = pathRuleReachOf(rule, tighten.identity, bases);
818
834
  if (outcome.reach === "reached")
819
835
  return { behavior, rules: [rule] };
820
836
  if (outcome.reach === "unreadable")
@@ -843,7 +859,7 @@ export function segmentCoverageOf(command, rules, call) {
843
859
  proposed.some((p) => p.rule.tool === "Read"
844
860
  ? dir !== undefined &&
845
861
  eligibleContext({ tool: p.rule.tool, scope: p.scope }, { tool: "Read", cwd: call.cwd, sessionId: call.sessionId }) &&
846
- directoryRuleAdmits(p.rule, dir)
862
+ directoryRuleAdmits(p.rule, dir, basesOfCwd(call.execCwd ?? call.cwd))
847
863
  : eligibleContext({ tool: p.rule.tool, scope: p.scope }, call) && ruleAdmitsCommand(p.rule, segment)),
848
864
  };
849
865
  });
@@ -889,7 +905,7 @@ export function suggestRulesForCommand(command, ctx) {
889
905
  const parsed = parseRuleText(formatRuleText(directory, "subpath", READ_RULE_TOOL), "allow");
890
906
  if (!("rule" in parsed) || parsed.rule.match !== "subpath")
891
907
  return undefined;
892
- if (!directoryRuleAdmits(parsed.rule, directory))
908
+ if (!directoryRuleAdmits(parsed.rule, directory, basesOfCwd(ctx?.execCwd ?? ctx?.cwd)))
893
909
  return undefined;
894
910
  return { kind: "directoryRead", rule: parsed.rule.rule, directory: parsed.rule.command, segment };
895
911
  };
@@ -963,7 +979,7 @@ export function suggestRulesForCommand(command, ctx) {
963
979
  if (parsedBatch.some((p) => p.tool !== "Read" && ruleAdmitsCommand(p, segment)))
964
980
  continue;
965
981
  const dir = cdDirs[i];
966
- if (dir !== undefined && parsedBatch.some((p) => directoryRuleAdmits(p, dir)))
982
+ if (dir !== undefined && parsedBatch.some((p) => directoryRuleAdmits(p, dir, basesOfCwd(ctx?.execCwd ?? ctx?.cwd))))
967
983
  continue;
968
984
  const reason = carriesShellRedirection(segment) ? "redirection" : mintSegmentRule(segment) === undefined ? "no_rule_form" : "cap_overflow";
969
985
  uncoveredDetail.push({ segment, reason });
@@ -2,6 +2,7 @@ import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
2
2
  import { catalogRuleFaceOf, pathTargetOf } from "./tool-registry.js";
3
3
  import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule } from "./permission-rule-syntax.js";
4
4
  import { PATH_RULE_BASE_LABEL, isUsablePathBase, parseRuleText, pathRuleReachOf, programRunReachOf, ruleBasesNeeded } from "./permission-rule-model.js";
5
+ import { pathFamilyOf } from "../tools/fs/safety.js";
5
6
  import { effectivePathTargetOf } from "./effective-path-target.js";
6
7
  import { protectivePathTargetOf } from "./tool-registry.js";
7
8
  export { parsePermissionRule };
@@ -188,7 +189,7 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
188
189
  }
189
190
  for (const base of ruleBasesNeeded(parsedPath.rule)) {
190
191
  const value = base === "cwd" ? (pathBases.cwd ?? pathBases.root) : pathBases[base];
191
- if (!isUsablePathBase(value)) {
192
+ if (!isUsablePathBase(value, pathFamilyOf(pathBases))) {
192
193
  bad(text, "unsupported.path_base", `"${text}" is relative to ${PATH_RULE_BASE_LABEL[base]}, and this policy was given no absolute \`pathBases.${base}\`${base === "cwd" ? " (or `root`)" : ""} to resolve it against — the rule could not be judged on any call`);
193
194
  }
194
195
  }
@@ -321,9 +322,13 @@ export function createPermissionRulePolicy(rules, opts) {
321
322
  let unreadable;
322
323
  for (const r of lane) {
323
324
  if (r.parsed.match === "subpath" || r.parsed.match === "path") {
324
- if (target === undefined)
325
+ if (target === undefined || target.kind === "refused")
325
326
  continue;
326
- const outcome = pathRuleReachOf(r.parsed, target, pathBases);
327
+ if (target.kind === "unreadable") {
328
+ unreadable ??= { rule: r, unreadable: target.reason };
329
+ continue;
330
+ }
331
+ const outcome = pathRuleReachOf(r.parsed, target.identity, pathBases);
327
332
  if (outcome.reach === "reached")
328
333
  return { rule: r };
329
334
  if (outcome.reach === "unreadable")
@@ -18,13 +18,13 @@ export declare const MINTED_TOOL_SEGMENT_MIN_CHARS = 16;
18
18
  * live domain key here (the refresh splice). That case is decided by
19
19
  * {@link findNamespacePrefixCollision} and refused at the mount, loudly; the mint stays total.
20
20
  *
21
- * RB-83 (2026-07-25, red probe): the pattern this enforces is `{1,64}`, and only the charset half was
21
+ * The pattern this enforces is `{1,64}`, and only the charset half was
22
22
  * enforced. Both other halves matter for the same reason the charset does — an over-long or empty segment
23
23
  * produces a name the provider rejects, which 400s the WHOLE model request, the exact failure the intake
24
24
  * gate exists to contain ("a single misconfigured server never bricks the whole task").
25
25
  */
26
26
  export declare function normalizeNameSegment(name: string): string;
27
- /** RB-83: force a normalized segment into `{1,max}`, keeping long names distinct via a stable digest. */
27
+ /** Force a normalized segment into `{1,max}`, keeping long names distinct via a stable digest. */
28
28
  export declare function clampNameSegment(seg: string, max?: number): string;
29
29
  /**
30
30
  * The registered-name prefix for `peer` in `ns` — i.e. the set `name.startsWith(prefix)` that a consumer