@sema-agent/core 7.53.0 → 7.54.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 (244) hide show
  1. package/CHANGELOG.md +277 -1
  2. package/LICENSE +3 -1
  3. package/dist/agents/peer-session-drain.d.ts +0 -1
  4. package/dist/agents/peer-session-drain.js +0 -2
  5. package/dist/agents/send-message-tool.js +3 -4
  6. package/dist/agents/subagent.d.ts +3 -5
  7. package/dist/agents/subagent.js +18 -11
  8. package/dist/agents/tool-filter.d.ts +7 -4
  9. package/dist/agents/tool-filter.js +7 -3
  10. package/dist/core/a2a.js +4 -2
  11. package/dist/core/allow-entry.d.ts +1 -0
  12. package/dist/core/allow-entry.js +1 -0
  13. package/dist/core/approval-object.d.ts +87 -0
  14. package/dist/core/approval-object.js +85 -0
  15. package/dist/core/arg-summary.d.ts +3 -2
  16. package/dist/core/arg-summary.js +17 -11
  17. package/dist/core/ask-origin.d.ts +43 -27
  18. package/dist/core/ask-origin.js +20 -11
  19. package/dist/core/ask-question.d.ts +0 -1
  20. package/dist/core/ask-question.js +4 -67
  21. package/dist/core/ask-unresolvable-notice.d.ts +3 -2
  22. package/dist/core/auto-compaction.d.ts +49 -50
  23. package/dist/core/auto-compaction.js +41 -51
  24. package/dist/core/auto-mode-prompt.d.ts +4 -2
  25. package/dist/core/auto-mode-prompt.js +4 -3
  26. package/dist/core/canonical-json.d.ts +69 -0
  27. package/dist/core/canonical-json.js +130 -8
  28. package/dist/core/checkpoint-requirements.d.ts +13 -7
  29. package/dist/core/checkpoint-requirements.js +4 -1
  30. package/dist/core/checkpoint-store.d.ts +33 -18
  31. package/dist/core/checkpoint-store.js +5 -4
  32. package/dist/core/child-clamp-seats.d.ts +45 -6
  33. package/dist/core/child-clamp-seats.js +56 -4
  34. package/dist/core/consolidate-scope.d.ts +1 -1
  35. package/dist/core/cross-boundary-taint.d.ts +213 -0
  36. package/dist/core/cross-boundary-taint.js +196 -0
  37. package/dist/core/decision-receipt.d.ts +44 -1
  38. package/dist/core/decision-receipt.js +16 -0
  39. package/dist/core/effective-path-target.d.ts +3 -0
  40. package/dist/core/effective-path-target.js +4 -0
  41. package/dist/core/engine-notice.d.ts +34 -2
  42. package/dist/core/engine-notice.js +17 -1
  43. package/dist/core/engine-storage-backend.d.ts +73 -0
  44. package/dist/core/engine-storage-backend.js +1 -0
  45. package/dist/core/file-history-store.d.ts +6 -2
  46. package/dist/core/file-history-store.js +1 -0
  47. package/dist/core/gate-fold.js +10 -2
  48. package/dist/core/gate-lanes.js +30 -21
  49. package/dist/core/gate-outcome.d.ts +7 -3
  50. package/dist/core/governance-codes.d.ts +7 -2
  51. package/dist/core/governance-codes.js +6 -0
  52. package/dist/core/hooks.d.ts +14 -14
  53. package/dist/core/hooks.js +1 -2
  54. package/dist/core/locked-config.d.ts +28 -1
  55. package/dist/core/locked-config.js +30 -3
  56. package/dist/core/lsp.d.ts +1 -1
  57. package/dist/core/mcp-server-spec.d.ts +66 -9
  58. package/dist/core/mcp-server-spec.js +52 -19
  59. package/dist/core/mcp.js +5 -4
  60. package/dist/core/memory-engine/engine.js +2 -1
  61. package/dist/core/memory-engine/export-bundle.d.ts +1 -1
  62. package/dist/core/memory-engine/export-bundle.js +1 -1
  63. package/dist/core/memory-engine/file-backend.d.ts +2 -6
  64. package/dist/core/memory-engine/file-backend.js +1 -12
  65. package/dist/core/memory.d.ts +3 -3
  66. package/dist/core/mode-policy.d.ts +13 -8
  67. package/dist/core/mode-policy.js +3 -2
  68. package/dist/core/mods/kernel/receive.d.ts +2 -1
  69. package/dist/core/mods/kernel/receive.js +7 -2
  70. package/dist/core/parent-spec-seat.d.ts +10 -13
  71. package/dist/core/parent-spec-seat.js +14 -6
  72. package/dist/core/pause-registry.d.ts +35 -1
  73. package/dist/core/pause-registry.js +10 -0
  74. package/dist/core/permission-rule-consent.d.ts +18 -5
  75. package/dist/core/permission-rule-consent.js +13 -8
  76. package/dist/core/permission-rule-model.d.ts +29 -6
  77. package/dist/core/permission-rule-model.js +6 -1
  78. package/dist/core/permission-rule-org.d.ts +37 -0
  79. package/dist/core/permission-rule-org.js +34 -42
  80. package/dist/core/permission-rule-session.js +2 -2
  81. package/dist/core/permission-rule-store.d.ts +8 -0
  82. package/dist/core/permission-rule-store.js +1 -0
  83. package/dist/core/permission-rule-sync.js +3 -2
  84. package/dist/core/read-only-shell.d.ts +1 -1
  85. package/dist/core/read-only-shell.js +30 -9
  86. package/dist/core/remote-env.d.ts +41 -1
  87. package/dist/core/remote-env.js +18 -0
  88. package/dist/core/resume-startup-cause.d.ts +18 -0
  89. package/dist/core/resume-startup-cause.js +12 -0
  90. package/dist/core/runner/approval-feedback.d.ts +46 -0
  91. package/dist/core/runner/approval-feedback.js +41 -0
  92. package/dist/core/runner/compaction-call-options.d.ts +3 -3
  93. package/dist/core/runner/compaction-call-options.js +2 -2
  94. package/dist/core/runner/contracts.d.ts +33 -41
  95. package/dist/core/runner/cross-boundary-lane.d.ts +87 -0
  96. package/dist/core/runner/cross-boundary-lane.js +190 -0
  97. package/dist/core/runner/decide-continuation.d.ts +4 -4
  98. package/dist/core/runner/decide-continuation.js +3 -1
  99. package/dist/core/runner/gate-exit.d.ts +32 -12
  100. package/dist/core/runner/gate-exit.js +75 -33
  101. package/dist/core/runner/inherited-ask-grants.js +2 -2
  102. package/dist/core/runner/person-path-rows.d.ts +2 -2
  103. package/dist/core/runner/prepare-acquire-reconcile.d.ts +3 -4
  104. package/dist/core/runner/prepare-acquire-reconcile.js +3 -3
  105. package/dist/core/runner/prepare-ask-lane.js +3 -3
  106. package/dist/core/runner/prepare-caps-and-workflow.d.ts +6 -5
  107. package/dist/core/runner/prepare-caps-and-workflow.js +3 -3
  108. package/dist/core/runner/prepare-config-doors.d.ts +12 -15
  109. package/dist/core/runner/prepare-config-doors.js +8 -3
  110. package/dist/core/runner/prepare-context-lane.js +6 -3
  111. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
  112. package/dist/core/runner/prepare-delegation-surface.js +2 -3
  113. package/dist/core/runner/prepare-execution-env.d.ts +1 -2
  114. package/dist/core/runner/prepare-execution-env.js +2 -1
  115. package/dist/core/runner/prepare-gate-stations.d.ts +2 -0
  116. package/dist/core/runner/prepare-gate-stations.js +4 -3
  117. package/dist/core/runner/prepare-hands-readface.d.ts +2 -2
  118. package/dist/core/runner/prepare-hands-readface.js +1 -1
  119. package/dist/core/runner/prepare-inherited-gate.d.ts +2 -0
  120. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  121. package/dist/core/runner/prepare-park-ask.d.ts +1 -1
  122. package/dist/core/runner/prepare-park-ask.js +9 -3
  123. package/dist/core/runner/prepare-policy-chain.js +2 -7
  124. package/dist/core/runner/prepare-safety-scan.d.ts +7 -2
  125. package/dist/core/runner/prepare-safety-scan.js +34 -27
  126. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -2
  127. package/dist/core/runner/prepare-suspend-saga.js +6 -2
  128. package/dist/core/runner/prepare-task.js +13 -10
  129. package/dist/core/runner/prepare-turn-wiring.d.ts +10 -5
  130. package/dist/core/runner/prepare-turn-wiring.js +14 -49
  131. package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
  132. package/dist/core/runner/prepare-wiring-manifest.js +2 -2
  133. package/dist/core/runner/prepare-workspace-restore.js +2 -2
  134. package/dist/core/runner/resume-admission.js +9 -9
  135. package/dist/core/runner/resume-apply.js +4 -4
  136. package/dist/core/runner/resume-policy-outcome.d.ts +2 -2
  137. package/dist/core/runner/resume-policy-outcome.js +18 -15
  138. package/dist/core/runner/resume-preflight.js +2 -2
  139. package/dist/core/runner/resume-review-outcome.js +3 -3
  140. package/dist/core/runner/run-leg.js +57 -33
  141. package/dist/core/runner/run-settle-and-teardown.js +2 -0
  142. package/dist/core/runner/run-terminal-adoption.js +15 -4
  143. package/dist/core/runner/session-file-state-replay.d.ts +5 -2
  144. package/dist/core/runner/session-file-state-replay.js +3 -2
  145. package/dist/core/runner/stream-steer-verb.js +2 -2
  146. package/dist/core/runner/tool-face-overlay.d.ts +24 -6
  147. package/dist/core/runner/tool-face-overlay.js +24 -15
  148. package/dist/core/runner-deps.d.ts +9 -9
  149. package/dist/core/session-store.d.ts +35 -16
  150. package/dist/core/session-store.js +20 -12
  151. package/dist/core/{shell-lexer.d.ts → shell-run-candidates.d.ts} +36 -12
  152. package/dist/core/{shell-lexer.js → shell-run-candidates.js} +145 -225
  153. package/dist/core/shell-wrapper-table.d.ts +5 -3
  154. package/dist/core/shell-wrapper-table.js +1 -1
  155. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -2
  156. package/dist/core/store-contracts/session-store-contract.d.ts +49 -0
  157. package/dist/core/store-contracts/session-store-contract.js +639 -0
  158. package/dist/core/store-contracts/session-store-incarnation-contract.d.ts +12 -0
  159. package/dist/core/store-contracts/session-store-incarnation-contract.js +5 -2
  160. package/dist/core/task-event.d.ts +6 -4
  161. package/dist/core/task-registry-agent.js +1 -0
  162. package/dist/core/task-registry-shared.d.ts +5 -0
  163. package/dist/core/task-registry.d.ts +7 -0
  164. package/dist/core/task-registry.js +17 -0
  165. package/dist/core/task-result.d.ts +1 -1
  166. package/dist/core/task-spec.d.ts +6 -6
  167. package/dist/core/tighten-task-spec.d.ts +6 -4
  168. package/dist/core/tighten-task-spec.js +2 -2
  169. package/dist/core/tool-catalog-entries.d.ts +1 -1
  170. package/dist/core/tool-catalog-entries.js +52 -52
  171. package/dist/core/tool-face-reader.d.ts +49 -0
  172. package/dist/core/tool-face-reader.js +94 -0
  173. package/dist/core/tool-face.d.ts +17 -8
  174. package/dist/core/tool-face.js +1 -0
  175. package/dist/core/tool-policy.d.ts +64 -23
  176. package/dist/core/tool-policy.js +62 -56
  177. package/dist/core/tool-roster.d.ts +46 -28
  178. package/dist/core/tool-roster.js +91 -14
  179. package/dist/core/tool-spec-members.js +2 -2
  180. package/dist/core/tool-spec.d.ts +20 -4
  181. package/dist/core/tools.js +2 -0
  182. package/dist/core/trace.d.ts +24 -2
  183. package/dist/core/types.d.ts +2 -2
  184. package/dist/core/types.js +1 -1
  185. package/dist/core/untrusted-text.d.ts +7 -0
  186. package/dist/core/untrusted-text.js +3 -8
  187. package/dist/core/user-attachment.d.ts +43 -0
  188. package/dist/core/user-attachment.js +92 -15
  189. package/dist/engine/compaction/compaction.d.ts +2 -2
  190. package/dist/engine/compaction/utils.d.ts +2 -2
  191. package/dist/engine/harness/types.d.ts +7 -4
  192. package/dist/engine/llm/types.d.ts +7 -2
  193. package/dist/engine/llm/types.js +2 -1
  194. package/dist/engine/session/repo-utils.d.ts +5 -0
  195. package/dist/engine/session/repo-utils.js +9 -0
  196. package/dist/engine/session/storage-base.js +2 -2
  197. package/dist/index.d.ts +23 -9
  198. package/dist/index.js +19 -8
  199. package/dist/internal/llm.d.ts +1 -1
  200. package/dist/internal/llm.js +1 -1
  201. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  202. package/dist/orchestration/run-workflow-tool.js +1 -0
  203. package/dist/orchestration/workflow-governance.d.ts +1 -1
  204. package/dist/orchestration/workflow.d.ts +3 -0
  205. package/dist/orchestration/workflow.js +2 -1
  206. package/dist/prompt-assembly/catalog/schema.d.ts +3 -0
  207. package/dist/prompt-assembly/catalog/schema.js +3 -0
  208. package/dist/prompt-assembly/overlay.js +2 -2
  209. package/dist/prompt-assembly/select.js +2 -2
  210. package/dist/prompts/default.d.ts +1 -1
  211. package/dist/shell/lex.d.ts +87 -8
  212. package/dist/shell/lex.js +283 -49
  213. package/dist/stores/file/index.d.ts +8 -6
  214. package/dist/tools/artifact/artifact-tool.js +10 -0
  215. package/dist/tools/fs/bash-program-position.d.ts +3 -15
  216. package/dist/tools/fs/bash-program-position.js +3 -10
  217. package/dist/tools/fs/bash-readonly-classifier.js +2 -0
  218. package/dist/tools/fs/fs-bash.d.ts +3 -3
  219. package/dist/tools/fs/fs-bash.js +72 -101
  220. package/dist/tools/fs/fs-read.d.ts +12 -0
  221. package/dist/tools/fs/fs-read.js +10 -9
  222. package/dist/tools/fs/fs-shared.d.ts +27 -31
  223. package/dist/tools/fs/fs-shared.js +12 -15
  224. package/dist/tools/fs/fs-write.js +1 -1
  225. package/dist/tools/fs/index.js +2 -2
  226. package/dist/tools/fs/read-deny-home.d.ts +1 -1
  227. package/dist/tools/fs/read-deny-home.js +5 -4
  228. package/dist/tools/fs/read-deny.d.ts +16 -3
  229. package/dist/tools/fs/read-deny.js +12 -9
  230. package/dist/tools/fs/safety.d.ts +7 -6
  231. package/dist/tools/fs/shell-read-bindings.d.ts +5 -3
  232. package/dist/tools/fs/shell-read-bindings.js +15 -16
  233. package/dist/tools/fs/shell-read-device.js +1 -1
  234. package/dist/tools/fs/shell-read-plan.js +36 -47
  235. package/dist/tools/fs/shell-read-word.d.ts +1 -6
  236. package/dist/tools/fs/shell-read-word.js +2 -20
  237. package/dist/tools/fs/shell-reentry-text.js +1 -1
  238. package/dist/tools/web.d.ts +2 -2
  239. package/package.json +4 -1
  240. package/test/export-surface.snapshot.json +5654 -5520
  241. package/wire-changes/core@7.53.0.json +30 -8
  242. package/wire-changes/core@7.54.0.json +2870 -0
  243. package/dist/core/shell-scan.d.ts +0 -60
  244. package/dist/core/shell-scan.js +0 -183
@@ -55,6 +55,8 @@ export interface McpServerSpec {
55
55
  * override is AUTHORITATIVE and may LOWER an effect (vouch a tool is `read`/`idempotent`) as well as raise it
56
56
  * (`egress` / irreversible). This is the ONLY trusted way to drop an MCP tool below the fail-closed `write`
57
57
  * default (design F). Folds over the server hints in prepare-task (caller > server hint > fail-closed write).
58
+ * `boundaryFlow: "engine"` vouches that a call's effect stays inside the engine: the tool's roster row states no boundary legs
59
+ * (`boundaryLegs: []`) instead of the legs its effect implies — it needs `egress: false` beside it (refused otherwise).
58
60
  *
59
61
  * A DIFFERENT AXIS from {@link contentOrigin}: this one is about what a call DOES (repeat-safety,
60
62
  * blast radius, reversibility — it feeds the approval gate); the content class is about what a call
@@ -75,16 +77,19 @@ export interface McpServerSpec {
75
77
  effect?: ToolEffect;
76
78
  egress?: boolean;
77
79
  irreversibility?: "always" | "never";
80
+ boundaryFlow?: "engine";
78
81
  }>;
79
82
  /**
80
83
  * design/388 §1.1 / §2.7 — the OPTIONAL per-tool FACE overlay for this server's tools (keyed by the server's
81
84
  * RAW tool name, like {@link allowTools} / {@link toolAxes}): a deployment lets an MCP tool enter a specialised
82
85
  * approval card, be confirmed by path, carry a display name / summary, or narrow its rule vocabulary. TRUSTED
83
- * (the deployment writes it), but validated against the MATERIALIZED schema at every listing (first mount and
84
- * every refresh): a `pathTarget.param` that is not a top-level property, an `approvalCard` whose keys the schema
85
- * lacks, a `ruleFace` naming a non-scalar key ⇒ the tool mounts WITHOUT the overlay and the leg announces
86
- * `config.tool_face_invalid` once; a key naming a tool the listing does not carry announces
87
- * `config.tool_face_undeclared` once. Applied whole or not at all.
86
+ * (the deployment writes it). It is read once by `readToolFaces` at the preparation door and at `materializeMcpTools`: a
87
+ * declaration that does not fit on its own (a member the types do not have, a member of the wrong kind, a word outside a
88
+ * closed table, a reserved approval card, an empty tool name) refuses the entry under `config.tool_face_invalid` — the whole
89
+ * prepare at the door. What only the schema can say is judged at every listing (first mount and every refresh): a
90
+ * `pathTarget.param` that is not a top-level property, an `approvalCard` whose keys the schema lacks, a `ruleFace` naming a
91
+ * non-scalar key ⇒ that tool is NOT mounted and the leg announces `config.tool_face_invalid` once; a key naming a tool the
92
+ * listing does not carry announces `config.tool_face_undeclared` once. Applied whole or not at all.
88
93
  */
89
94
  toolFaces?: Record<string, {
90
95
  family?: ToolFamily;
@@ -185,6 +190,12 @@ export type ToolAxesRead = {
185
190
  ok: false;
186
191
  problem: string;
187
192
  };
193
+ /** A name the declarer wrote, quoted and bounded for a sentence (the sentence names; it never carries a value). The bound holds on the
194
+ * QUOTED form — escaping a quote, a backslash or a control character lengthens a name — so a sentence's budget is spent the same way
195
+ * whatever the name holds. */
196
+ export declare function quotedName(name: string): string;
197
+ /** The kind of a value that is plain data, for a sentence that names what was declared without carrying it. */
198
+ export declare function dataKind(value: unknown): string;
188
199
  /**
189
200
  * Where a declaration stops being one: the path from the declaration's root — a list index as a number, a member name as a string — and
190
201
  * the clause that says what is there ("it contains itself; …"): a kind and a remedy, never a value the declarer wrote.
@@ -220,6 +231,9 @@ export interface DeclarationPathRule {
220
231
  * surface's row: it is a member the surface does not read.
221
232
  */
222
233
  export declare const DECLARATION_PATH_RULES: Readonly<Record<ToolAxesSurface, readonly DeclarationPathRule[]>>;
234
+ /** The rules of a declaration that fixes no path beyond plain data and lists of names (a `toolAxes` record, a lock declaration): the one
235
+ * empty table, passed on purpose — {@link receiveDeclaration} takes its rules as an argument and refuses an absent one. */
236
+ export declare const NO_DECLARATION_PATH_RULES: readonly DeclarationPathRule[];
223
237
  /**
224
238
  * The one reading of a declaration a caller hands the engine — an MCP server's or an A2A peer's entry (the snapshot), a `toolAxes`
225
239
  * record ({@link readToolAxes}), a lock declaration: the engine's one receipt of a caller's object in its declaration mode, so the
@@ -227,20 +241,62 @@ export declare const DECLARATION_PATH_RULES: Readonly<Record<ToolAxesSurface, re
227
241
  * vocabulary over the copy (a list holds names; no member is named `__proto__`; each path `rules` names holds what its rule says — an
228
242
  * entry's rules are its surface's {@link DECLARATION_PATH_RULES}). The copy is the answer — the caller's object is never handed on and
229
243
  * never read again. A copy that is not a record is answered as it is, for the reader that expected a record to word.
244
+ *
245
+ * `rules` is required: a surface's {@link DECLARATION_PATH_RULES} row, or an empty list ({@link NO_DECLARATION_PATH_RULES} is the engine's
246
+ * own) for a declaration that fixes no path. A value that is not a list (an absent one — what an `at` outside the surfaces indexes to — `null`, a string, an inherited
247
+ * member of the table) is a caller's mistake about which declaration it holds, refused loudly (`config.invalid_argument`) rather than
248
+ * read as "no path rule", which would judge the declaration by less than its surface fixes.
249
+ */
250
+ export declare function receiveDeclaration(declared: unknown, rules: readonly DeclarationPathRule[]): DeclarationRead;
251
+ /**
252
+ * The per-tool declarations a spec carries as ONE record keyed by the remote tool (or skill) name — what each is called in a
253
+ * sentence about it. Every reader of such a record ({@link readToolAxes}, `readToolFaces`) opens it through
254
+ * {@link receivePerToolDeclaration} and words a refusal through the two sentence functions below, so the one receipt, the one
255
+ * fault vocabulary and the one sentence shape are not written twice.
256
+ */
257
+ declare const PER_TOOL_DECLARATIONS: {
258
+ readonly toolAxes: {
259
+ readonly plural: "axes";
260
+ readonly singular: "axis";
261
+ };
262
+ readonly toolFaces: {
263
+ readonly plural: "faces";
264
+ readonly singular: "face";
265
+ };
266
+ };
267
+ type PerToolDeclaration = keyof typeof PER_TOOL_DECLARATIONS;
268
+ /** {@link receivePerToolDeclaration}'s answer: the receipt's copy as a record keyed by name, or the one sentence that says why not. */
269
+ type PerToolDeclarationRead = {
270
+ readonly ok: true;
271
+ readonly perTool: Readonly<Record<string, unknown>>;
272
+ } | {
273
+ readonly ok: false;
274
+ readonly problem: string;
275
+ };
276
+ /**
277
+ * A per-tool declaration's receipt: {@link receiveDeclaration} (plain data held to the engine's one depth, every own member read once,
278
+ * lists of names, no `__proto__`), then the copy must be a record of names. A refusal names the tool (or skill) and the member where
279
+ * reading stopped, never a value. `unit` is what the keys are called (`tool` / `skill`).
230
280
  */
231
- export declare function receiveDeclaration(declared: unknown, rules?: readonly DeclarationPathRule[]): DeclarationRead;
281
+ export declare function receivePerToolDeclaration(declared: unknown, field: PerToolDeclaration, unit: string): PerToolDeclarationRead;
282
+ /** The sentence for one tool's (or skill's) declaration that does not fit: `detail` says which member and why. */
283
+ export declare function perToolEntryRefusal(field: PerToolDeclaration, unit: string, key: string, detail: string): string;
284
+ /** The sentence for a per-tool declaration that names a tool (or skill) with an empty name. */
285
+ export declare function perToolEmptyName(field: PerToolDeclaration, unit: string): string;
232
286
  /**
233
287
  * The one reader of a raw `toolAxes` declaration — {@link McpServerSpec.toolAxes} and {@link A2aServerSpec.toolAxes} are the same
234
288
  * declaration, and the engine's preparation door, its materialization of an MCP server and of an A2A peer, and every host that
235
289
  * assembles the field from JSON ask this same function. `at` names the surface the declaration sits on, because that decides what its
236
290
  * keys are called in the sentence: `"mcp"` — the server's remote tools; `"a2a"` — the peer's remote skills. Absent ⇒ `{ ok: true }`
237
291
  * (nothing declared). Anything else must be an object of per-tool objects whose members are exactly `effect` /
238
- * `egress` / `irreversibility` with legal values; a declaration of any other shape, an empty tool name, an
292
+ * `egress` / `irreversibility` / `boundaryFlow` with legal values; a declaration of any other shape, an empty tool name, an
239
293
  * unknown member, a word outside the vocabulary or a non-boolean `egress` on ANY one tool refuses the whole
240
294
  * declaration (`ok: false`) — the good tools are not kept and nothing is defaulted, since a caller axis is
241
295
  * two-way (it can vouch a tool down as well as mark it up) and a silently lost half is a trust root that is
242
- * not in force. The reader judges SHAPE only; whether the axes contradict each other (`egress` over a
243
- * non-write effect) is the ingestion's own arithmetic. The declaration is read once, as plain data, by {@link receiveDeclaration} — a
296
+ * not in force. `boundaryFlow: "engine"` (the call's effect stays inside the engine — the tool's roster row states no
297
+ * boundary legs) needs `egress: false` in the same entry — `egress: true` contradicts it and an absent one leaves a server hint
298
+ * or the peer default marking it an external write — refused here; whether the other axes contradict each other (`egress`
299
+ * over a non-write effect) is the ingestion's own arithmetic. The declaration is read once, as plain data, by {@link receiveDeclaration} — a
244
300
  * Proxy, a map, a class instance, a record built on a prototype or in another realm, a getter that throws, a structure that contains
245
301
  * itself, a tool named `__proto__` are refusals that say where they sit (never the thrown text), not a throw — and judged on that copy.
246
302
  * `at` is the caller's own word, not the declarer's: one outside {@link ToolAxesSurface} throws `config.invalid_argument` naming the
@@ -325,6 +381,7 @@ export interface A2aServerSpec {
325
381
  effect?: ToolEffect;
326
382
  egress?: boolean;
327
383
  irreversibility?: "always" | "never";
384
+ boundaryFlow?: "engine";
328
385
  }>;
329
386
  }
330
387
  /**
@@ -18,7 +18,7 @@ function toolAxesSurfaceOf(at) {
18
18
  throw invalidArgument(`toolAxes is read for a surface, and the surfaces are ${Object.keys(TOOL_AXES_SURFACES).map((s) => JSON.stringify(s)).join(" | ")} ` +
19
19
  `(an MCP server's tools, an A2A peer's skills); the surface named is not one of them.`);
20
20
  }
21
- const TOOL_AXIS_MEMBERS = { effect: true, egress: true, irreversibility: true };
21
+ const TOOL_AXIS_MEMBERS = { effect: true, egress: true, irreversibility: true, boundaryFlow: true };
22
22
  const TOOL_AXIS_MEMBER_NAMES = Object.keys(TOOL_AXIS_MEMBERS);
23
23
  const TOOL_EFFECT_WORDS = SAFETY_AXIS_VOCABULARY.toolEffect;
24
24
  const ENGINE_DERIVED_IRREVERSIBILITY = "maybe";
@@ -27,9 +27,16 @@ const irreversibilityPolesLockstep = true;
27
27
  void irreversibilityPolesLockstep;
28
28
  const toolAxesDeclarationsAreOne = true;
29
29
  void toolAxesDeclarationsAreOne;
30
- function quotedName(name) {
31
- return JSON.stringify(name.length > 80 ? `${name.slice(0, 80)}…` : name);
30
+ export function quotedName(name) {
31
+ const whole = JSON.stringify(name);
32
+ if (name.length <= 80 && whole.length <= QUOTED_NAME_MAX)
33
+ return whole;
34
+ let cut = Math.min(name.length, 80);
35
+ while (cut > 0 && JSON.stringify(`${name.slice(0, cut)}…`).length > QUOTED_NAME_MAX)
36
+ cut--;
37
+ return JSON.stringify(`${name.slice(0, cut)}…`);
32
38
  }
39
+ const QUOTED_NAME_MAX = 83;
33
40
  function memberNames(path) {
34
41
  let rendered = "";
35
42
  for (const [index, name] of path.entries()) {
@@ -39,7 +46,7 @@ function memberNames(path) {
39
46
  }
40
47
  return rendered;
41
48
  }
42
- function dataKind(value) {
49
+ export function dataKind(value) {
43
50
  return value === null ? "null" : value === undefined ? "undefined" : Array.isArray(value) ? "an array" : typeof value === "object" ? "a record" : `a ${typeof value}`;
44
51
  }
45
52
  const RECEIPT_FAULT_PROBLEMS = {
@@ -63,12 +70,13 @@ export const DECLARATION_PATH_RULES = Object.freeze({
63
70
  { path: ["name"], holds: "a string", required: true },
64
71
  { path: ["transport", "env"], holds: "names to strings" },
65
72
  { path: ["transport", "headers"], holds: "names to strings" },
66
- ]),
73
+ ].map((rule) => Object.freeze({ ...rule, path: Object.freeze([...rule.path]) }))),
67
74
  a2a: Object.freeze([
68
75
  { path: ["name"], holds: "a string", required: true },
69
76
  { path: ["headers"], holds: "names to strings" },
70
- ]),
77
+ ].map((rule) => Object.freeze({ ...rule, path: Object.freeze([...rule.path]) }))),
71
78
  });
79
+ export const NO_DECLARATION_PATH_RULES = Object.freeze([]);
72
80
  const PROTO_MEMBER = `it is a member named "__proto__", which a copy by assignment would turn into a prototype — not a name a declaration can carry`;
73
81
  function declarationVocabularyFault(root, rules) {
74
82
  const asked = new Set();
@@ -148,7 +156,11 @@ function declarationVocabularyFault(root, rules) {
148
156
  }
149
157
  return problem === undefined ? undefined : { path, problem };
150
158
  }
151
- export function receiveDeclaration(declared, rules = []) {
159
+ export function receiveDeclaration(declared, rules) {
160
+ if (!Array.isArray(rules)) {
161
+ throw invalidArgument(`a declaration is received with the path rules of its surface (${Object.keys(DECLARATION_PATH_RULES).map((s) => `DECLARATION_PATH_RULES[${JSON.stringify(s)}]`).join(" | ")}), ` +
162
+ `or an empty list when it fixes no path; what was passed is ${dataKind(rules)}, not a list of rules.`);
163
+ }
152
164
  const received = receive(declared, DECLARATION_RECEIPT_MODE);
153
165
  if (!received.ok)
154
166
  return { ok: false, fault: declarationFaultOf(received.fault) };
@@ -175,38 +187,59 @@ function readToolAxisEntry(raw) {
175
187
  return `member ${quotedName(member)} is a hidden member: its name answers a value the declaration does not list as its own`;
176
188
  declared[member] = value;
177
189
  }
178
- const { effect, egress, irreversibility } = declared;
190
+ const { effect, egress, irreversibility, boundaryFlow } = declared;
179
191
  if (effect !== undefined && !isToolEffectWord(effect))
180
192
  return `member "effect" is not one of ${TOOL_EFFECT_WORDS.join(" | ")}`;
181
193
  if (egress !== undefined && typeof egress !== "boolean")
182
194
  return `member "egress" is not a boolean`;
183
195
  if (irreversibility !== undefined && !isIrreversibilityPoleWord(irreversibility))
184
196
  return `member "irreversibility" is not one of ${IRREVERSIBILITY_POLE_WORDS.join(" | ")}`;
197
+ if (boundaryFlow !== undefined && boundaryFlow !== "engine")
198
+ return `member "boundaryFlow" is not "engine"`;
199
+ if (boundaryFlow === "engine" && egress !== false)
200
+ return `member "boundaryFlow": "engine" needs "egress": false in the same entry — add it, or drop "boundaryFlow" (${egress === true ? '"egress": true contradicts it' : 'with no "egress", the server hint or the peer default stays in force'})`;
185
201
  return {
186
202
  ...(effect !== undefined ? { effect } : {}),
187
203
  ...(egress !== undefined ? { egress } : {}),
188
204
  ...(irreversibility !== undefined ? { irreversibility } : {}),
205
+ ...(boundaryFlow !== undefined ? { boundaryFlow } : {}),
189
206
  };
190
207
  }
191
- export function readToolAxes(declared, at) {
192
- const unit = toolAxesSurfaceOf(at).key;
193
- if (declared === undefined)
194
- return { ok: true };
195
- const received = receiveDeclaration(declared);
208
+ const PER_TOOL_DECLARATIONS = {
209
+ toolAxes: { plural: "axes", singular: "axis" },
210
+ toolFaces: { plural: "faces", singular: "face" },
211
+ };
212
+ export function receivePerToolDeclaration(declared, field, unit) {
213
+ const received = receiveDeclaration(declared, NO_DECLARATION_PATH_RULES);
196
214
  if (!received.ok) {
197
215
  const [key, ...member] = received.fault.path;
198
216
  if (key === undefined)
199
- return { ok: false, problem: `toolAxes cannot be read: ${received.fault.problem}` };
217
+ return { ok: false, problem: `${field} cannot be read: ${received.fault.problem}` };
200
218
  const where = member.length > 0 ? `member ${memberNames(member)}: ` : "";
201
- return { ok: false, problem: `the axes declared for ${unit} ${quotedName(String(key))} do not fit: ${where}${received.fault.problem}` };
219
+ return { ok: false, problem: perToolEntryRefusal(field, unit, String(key), `${where}${received.fault.problem}`) };
202
220
  }
203
221
  const perTool = received.value;
204
222
  if (!isPlainRecord(perTool))
205
- return { ok: false, problem: `toolAxes is ${dataKind(perTool)}, not an object of per-${unit} axis declarations` };
223
+ return { ok: false, problem: `${field} is ${dataKind(perTool)}, not an object of per-${unit} ${PER_TOOL_DECLARATIONS[field].singular} declarations` };
224
+ return { ok: true, perTool };
225
+ }
226
+ export function perToolEntryRefusal(field, unit, key, detail) {
227
+ return `the ${PER_TOOL_DECLARATIONS[field].plural} declared for ${unit} ${quotedName(key)} do not fit: ${detail}`;
228
+ }
229
+ export function perToolEmptyName(field, unit) {
230
+ return `${field} has an empty ${unit} name`;
231
+ }
232
+ export function readToolAxes(declared, at) {
233
+ const unit = toolAxesSurfaceOf(at).key;
234
+ if (declared === undefined)
235
+ return { ok: true };
236
+ const received = receivePerToolDeclaration(declared, "toolAxes", unit);
237
+ if (!received.ok)
238
+ return { ok: false, problem: received.problem };
206
239
  const entries = [];
207
- for (const [key, raw] of Object.entries(perTool)) {
240
+ for (const [key, raw] of Object.entries(received.perTool)) {
208
241
  if (key.length === 0)
209
- return { ok: false, problem: `toolAxes has an empty ${unit} name` };
242
+ return { ok: false, problem: perToolEmptyName("toolAxes", unit) };
210
243
  let entry;
211
244
  try {
212
245
  entry = readToolAxisEntry(raw);
@@ -215,7 +248,7 @@ export function readToolAxes(declared, at) {
215
248
  entry = "a member threw while it was read";
216
249
  }
217
250
  if (typeof entry === "string")
218
- return { ok: false, problem: `the axes declared for ${unit} ${quotedName(key)} do not fit: ${entry}` };
251
+ return { ok: false, problem: perToolEntryRefusal("toolAxes", unit, key, entry) };
219
252
  if (Object.keys(entry).length > 0)
220
253
  entries.push([key, entry]);
221
254
  }
package/dist/core/mcp.js CHANGED
@@ -25,9 +25,10 @@ import { MARKED_BLOCK_AFTER_DATA } from "./reminder-mint.js";
25
25
  import { resolveMcpDeclaredResultSize, resolveMcpRequiresUserInteraction } from "./mcp-tool-meta.js";
26
26
  export { resolveMcpDeclaredResultSize } from "./mcp-tool-meta.js";
27
27
  import { withContentOrigin } from "./memory-engine/content-origin.js";
28
- import { judgeEntryToolAxes, snapshotMcpEntries } from "./locked-config.js";
28
+ import { judgeMcpEntryDeclarations, snapshotMcpEntries } from "./locked-config.js";
29
29
  import { validateJsonSchemaShape } from "./runner/strict-output-schema.js";
30
30
  import { schemaKeyBoundProblem } from "./tool-roster.js";
31
+ import { boundaryFlowFace } from "./cross-boundary-taint.js";
31
32
  import { readMember, thrownIs, thrownName, thrownText } from "./thrown-value.js";
32
33
  export const MCP_PREFIX = MCP_NAMESPACE.prefix;
33
34
  const MCP_OUTPUT_TOKENS_DEFAULT = 25_000;
@@ -161,7 +162,7 @@ export function mcpStartupTimeoutMs(server) {
161
162
  return fromEnv;
162
163
  if (typeof own === "number" && Number.isSafeInteger(own) && own >= MCP_ENV_MS_MIN && own <= MCP_ENV_MS_MAX)
163
164
  return own;
164
- throw Object.assign(new Error(`MCP server "${inlineUntrusted(server.name, 160)}": startupTimeoutMs must be a whole number of milliseconds from ${MCP_ENV_MS_MIN} to ${MCP_ENV_MS_MAX} (got ${typeof own === "number" ? String(own) : own === null ? "null" : typeof own}) — not dialed`), { code: "config.mcp_startup_timeout_invalid" });
165
+ throw Object.assign(new Error(`MCP server ${typeof server.name === "string" ? `"${inlineUntrusted(server.name, 160)}"` : "(unnamed)"}: startupTimeoutMs must be a whole number of milliseconds from ${MCP_ENV_MS_MIN} to ${MCP_ENV_MS_MAX} (got ${typeof own === "number" ? String(own) : own === null ? "null" : typeof own}) — not dialed`), { code: "config.mcp_startup_timeout_invalid" });
165
166
  }
166
167
  function markMcpFailure(e, details) {
167
168
  e.details = details;
@@ -666,7 +667,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
666
667
  const snapshot = snapshotMcpEntries(specs);
667
668
  const axesRefused = new WeakMap();
668
669
  const entries = snapshot.map((spec) => {
669
- const judged = judgeEntryToolAxes(spec, "mcp");
670
+ const judged = judgeMcpEntryDeclarations(spec);
670
671
  if (judged.ok)
671
672
  return judged.entry;
672
673
  axesRefused.set(spec, judged.error);
@@ -1704,7 +1705,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
1704
1705
  ...(mcpMaxResultSizeChars !== undefined ? { mcpMaxResultSizeChars } : {}),
1705
1706
  ...(mcpAlwaysLoad ? { mcpAlwaysLoad: true } : {}),
1706
1707
  ...(interaction.declared ? { mcpRequiresUserInteraction: true } : {}),
1707
- mcpRemoteName: remoteName, ...(t.annotations?.readOnlyHint === true ? { mcpReadOnlyHint: true } : {}), permissionSeat: "passthrough",
1708
+ mcpRemoteName: remoteName, ...boundaryFlowFace(spec.toolAxes?.[remoteName]), ...(t.annotations?.readOnlyHint === true ? { mcpReadOnlyHint: true } : {}), permissionSeat: "passthrough",
1708
1709
  execute: async (_toolCallId, params, signal) => {
1709
1710
  const what = `The call to tool ${inlineUntrusted(remoteName)}`;
1710
1711
  if (isServerRevoked?.(spec.name) === true) {
@@ -4,6 +4,7 @@ import { dirname, join, relative, sep } from "node:path";
4
4
  import { uuidv7 } from "../../internal/harness.js";
5
5
  import { MAX_MEMORY_BYTES, composeMemoryBlock, firstSentence } from "../memory.js";
6
6
  import { inlineUntrusted } from "../untrusted-text.js";
7
+ import { canonicalJsonStringify } from "../canonical-json.js";
7
8
  import { mintSystemReminder } from "../reminder-mint.js";
8
9
  import { formatMemoryAge } from "../memory-recall.js";
9
10
  import { committedDistilledOf, committedOriginOf, computeEntryRev, consolidationExposedFrontmatter, distilledEquals, hasDistilledFormExtra, hasOriginFormExtra, isValidEntryId, originEquals, parseEntryFile, serializeEntryFile, stripModelWrittenDistilled, stripModelWrittenOrigin, UNCLOSED_MEMORY_FILE_REASON } from "./frontmatter.js";
@@ -16,7 +17,7 @@ import { noReplaceRestore, retireStagingToCustody, stagingPathBeside } from "./d
16
17
  import { sessionWriteHomeOf } from "./session-write-home.js";
17
18
  import { landedUndecided, layerDigest, layerObservationLookup, readOnlyLayerCandidates, readOnlyLayerCensus, recordLayerObservations, settleUndecidedLayerObservations, observationBacked, sameObservation, undecidedLayerObservations } from "./layer-observations.js";
18
19
  import { markOriginClearanceTombstoned, openOriginClearance, readOriginClearances, settleOriginClearance } from "./origin-clearance.js";
19
- import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, canonicalJsonStringify, captureErasureInput, erasureRequestInvalid, erasureSelectHash, scanEntryFiles, } from "./file-backend.js";
20
+ import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, captureErasureInput, erasureRequestInvalid, erasureSelectHash, scanEntryFiles, } from "./file-backend.js";
20
21
  import { assembleMemoryExportBundle, computeMemoryBundleHash, memoryBundleInvalid, } from "./export-bundle.js";
21
22
  import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, restoreContainedBaseline, indexLineIsStale, readIndexRevs, writeIndexRevs, bumpScanFuse, scanFuseKey, canonicalize, anchorStoreRoot, landingPathOf, landsOutsideStore, storeActRefusal, storeRootMovedRefusal, adoptRootScopeLayout, clearScanFuse, effectiveControlRootOf, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, markSessionPolluted, readSessionPollution, fileSessionCaptureRecordStore, recordRetrievedAccount, writeFileNoFollow, withPermissionRemedy, permissionRemedyOf, readRetrievedAccount, registerScope, registeredScopes, scopeDirFor, appendChallengeEvents, appendLineageAudit, rebuildStrictControlPlaneLedger, isStrictControlPlaneLedgerCorrupt, CHALLENGE_LEDGER_MAX_EVENTS, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageAccountOfEntry, lineageContributionsOfSession, lineageLatchedIds, promoteLineagePending, readChallengeEvents, readChallengedHistory, readLineageRecord, recordChallengedHistory, recordLineageCredential, reconcileLineage, readProjectionDebts, resolveChallengeEvent, settleProjectionDebts, stageLineagePending, stageProjectionDebts, } from "./layout.js";
22
23
  import { createSessionIncarnationSeat, recalledForAnyIdentity, recalledForIdentity, rememberForIdentity } from "./session-incarnation.js";
@@ -1,4 +1,4 @@
1
- import { type TransferEvidence } from "./file-backend.js";
1
+ import type { TransferEvidence } from "./file-backend.js";
2
2
  import { type MemoryEntry } from "./types.js";
3
3
  /** One exported challenge event (unresolved generations only). Generations are DESTINATION-minted:
4
4
  * the bundle deliberately carries no generation number — the importing store re-mints events under
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { canonicalJsonStringify } from "./file-backend.js";
2
+ import { canonicalJsonStringify } from "../canonical-json.js";
3
3
  import { computeEntryRev } from "./frontmatter.js";
4
4
  import { MEMORY_ORIGIN_CAUSES } from "./types.js";
5
5
  import { ambiguousOriginRepresentation, committedOriginOf } from "./frontmatter.js";
@@ -126,10 +126,6 @@ export interface MemoryErasureAttestation {
126
126
  propagation: "local-store-only";
127
127
  };
128
128
  }
129
- /** Canonical JSON (recursively sorted keys, no whitespace) — the erasure protocol's ONE comparison
130
- * and hash basis: `selectHash` complains bind to it, and evidence rows are written/compared in this
131
- * form so a re-serialized import (key order changed by a middle layer) still compares equal. */
132
- export declare function canonicalJsonStringify(value: unknown): string;
133
129
  /** sha256 hex over the selector's canonical JSON + LF — the replay-identity judge (§4.6) and the
134
130
  * anchor validator's recomputation basis (a 64-hex shape check alone would let a forged anchor swap
135
131
  * the selector under a kept hash). */
@@ -373,13 +369,13 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
373
369
  /**
374
370
  * §3.1 (r3-④) — the migration retry judge: the PERSISTED schema form, set at every real disk read
375
371
  * of the ledger and flipped only by the locked migration. Deliberately NOT derived from the
376
- * in-memory ledger shape: the v1 compat arm upgrades rows in memory (§3.2), so the memory shape
372
+ * in-memory ledger shape: the v1 compat arm (RL-1654-ledger-v1) upgrades rows in memory (§3.2), so the memory shape
377
373
  * reads "already v2" after the first stand-down and the retry would die. `"v1"` arms the
378
374
  * per-entry opportunistic retry (construction / reads / checkControlPlane / every locked
379
375
  * mutation); undefined = the disk form has not been observed yet.
380
376
  */
381
377
  private persistedSchemaVersion;
382
- /** §3.1 — the v1-compat window is loud ONCE per instance (per-row unbound noise stays suppressed;
378
+ /** §3.1 — the v1-compat window (RL-1654-ledger-v1) is loud ONCE per instance (per-row unbound noise stays suppressed;
383
379
  * the OVERALL state does not). */
384
380
  private announcedV1Compat;
385
381
  /** §3.4 — O(1) retry judge for the READ entries' unbound-convergence attempts: recomputed at
@@ -1,6 +1,7 @@
1
1
  import { existsSync, fstatSync, linkSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, openSync, fsyncSync, closeSync, utimesSync } from "node:fs";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import { dirname, join, relative } from "node:path";
4
+ import { canonicalJsonStringify } from "../canonical-json.js";
4
5
  import { jaccardDistance, termSet } from "../memory-vector.js";
5
6
  import { MAX_MEMORY_BYTES } from "../memory.js";
6
7
  import { inlineUntrusted } from "../untrusted-text.js";
@@ -14,18 +15,6 @@ import { scanMemoryFileName, scanMemoryWrite } from "./scan.js";
14
15
  import { admittingLandings, cutPerLanding, LandingsNeedAdmission, requireLandingsAdmitted } from "./landing-admission.js";
15
16
  import { thrownText } from "../thrown-value.js";
16
17
  export { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles } from "./entry-scan.js";
17
- export function canonicalJsonStringify(value) {
18
- if (Array.isArray(value))
19
- return `[${value.map((v) => (v === undefined || typeof v === "function" || typeof v === "symbol" ? "null" : canonicalJsonStringify(v))).join(",")}]`;
20
- if (value !== null && typeof value === "object") {
21
- const rec = value;
22
- const keys = Object.keys(rec)
23
- .filter((k) => rec[k] !== undefined)
24
- .sort();
25
- return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJsonStringify(rec[k])}`).join(",")}}`;
26
- }
27
- return JSON.stringify(value);
28
- }
29
18
  export function erasureSelectHash(select) {
30
19
  return createHash("sha256").update(`${canonicalJsonStringify(select)}\n`, "utf8").digest("hex");
31
20
  }
@@ -16,7 +16,7 @@ export interface ScoredMemory {
16
16
  /** design/84 Seam B (前置 BLOCKER): true when this note was CREATED by a consolidation ADD (not the
17
17
  * `remember` tool / a caller). Periodic/incremental consolidation EXCLUDES these from its candidate set so
18
18
  * it never re-consolidates its own output (infinite re-merge). Stores that don't track it leave it absent
19
- * (treated as not-generated — back-compat: the inline task-end path already excludes this-task notes by id). */
19
+ * (treated as not-generated — back-compat, RL-1654-memorystore: the inline task-end path already excludes this-task notes by id). */
20
20
  consolidationGenerated?: boolean;
21
21
  }
22
22
  /**
@@ -86,7 +86,7 @@ export interface MemoryStore {
86
86
  */
87
87
  searchScored?(scope: string, query: string, limit?: number): Promise<ScoredMemory[]> | ScoredMemory[];
88
88
  /** The vector-ranking rung this store achieves (design/81 Slice 5). Absent ⇒ treated as `"lexical"`
89
- * (back-compat). A runtime observable — a deployment reads it to decide index/metrics, NOT to decide
89
+ * (back-compat, RL-1654-memorystore). A runtime observable — a deployment reads it to decide index/metrics, NOT to decide
90
90
  * whether to inject an embedder (injection is config-driven; this is the RESULT). See {@link MemoryVectorMode}. */
91
91
  readonly vectorMode?: MemoryVectorMode;
92
92
  /** Optional: replace a note's text by id (consolidation UPDATE). The id stays stable. Must be safe
@@ -437,7 +437,7 @@ export declare function expandLexicalTerms(term: string): string[];
437
437
  * Matching rule: tokenize `text` on word boundaries, then for each whitespace-separated `query` term the
438
438
  * text must match AT LEAST ONE of that term's {@link expandLexicalTerms} variants (OR within a term), and
439
439
  * EVERY query term must match (AND across terms). The ORIGINAL query term keeps its pre-Slice-2b SUBSTRING
440
- * semantics (back-compat); the EXPANDED synonym/stem variants are matched on WORD BOUNDARIES (set membership
440
+ * semantics (back-compat, RL-1654-memorystore); the EXPANDED synonym/stem variants are matched on WORD BOUNDARIES (set membership
441
441
  * over the text's token set) so a short injected synonym like "key"/"db" cannot substring-match an unrelated
442
442
  * word ("keyboard"/"double"). An empty/whitespace-only query never matches.
443
443
  */
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import type { AgentTool, ExecutionEnv } from "../internal/harness-types.js";
17
17
  import type { ContainmentFace } from "../tools/fs/containment-face.js";
18
- import type { PermissionResult, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
18
+ import { type PermissionResult, type ToolCallRequest, type ToolPolicy } from "./tool-policy.js";
19
19
  import { type ToolRoster } from "./tool-roster.js";
20
20
  import { type PermissionMode } from "./ask-origin.js";
21
21
  import type { RuntimeCaps } from "./runtime-caps.js";
@@ -35,7 +35,7 @@ export type PermissionModeFoldReason = (typeof PERMISSION_MODE_FOLD_REASONS)[num
35
35
  * `writeFace`: the write face the mode states where no seat declared one (the mode row of {@link import("../tools/fs/containment-face.js").resolveContainmentFace}) —
36
36
  * `bypassPermissions` writes outside the write roots (`open`); every other mode states nothing and the default fence stands.
37
37
  * `unstated`: the mode's answer for a call to a tool that states no permission of its own (permission seat `passthrough`)
38
- * when no layer of the fold stated the allow — `ask` (the arbitration then answers it as any ask: dontAsk refuses it, auto's
38
+ * when no answer covers the fold's allow ({@link asksUnstated}) — `ask` (the arbitration then answers it as any ask: dontAsk refuses it, auto's
39
39
  * classifier may answer it), or `none` (the call runs; `bypassPermissions`, which installs no gate and asks nothing it would
40
40
  * release). */
41
41
  export interface ModePolicy {
@@ -77,13 +77,18 @@ export interface ModeGateSeats {
77
77
  /** Does the LAST mounted instance of this name (the one a call dispatches to) declare itself read-only — an MCP server's
78
78
  * `readOnlyHint` ({@link AgentTool.mcpReadOnlyHint})? The mode gate's `readOnlyDeclared` seat over the leg's live tools. */
79
79
  export declare function mountedToolDeclaresReadOnly(tools: readonly AgentTool[], toolName: string): boolean;
80
- /** The ONE rule of the unstated question, read at the fold's last station and at the approved-edit round: an allow nobody
81
- * stated (no `decisionReason`) on a tool that states no permission of its own, under a mode whose `unstated` answer is
82
- * `ask`. Who may answer it is the allow layer's business (a hook's own `allow` is one of its members), not this rule's. */
83
- export declare function asksUnstated(d: PermissionResult, seat: "passthrough" | undefined, mode: PermissionMode | undefined): boolean;
80
+ /** The ONE rule of the unstated question, read at the fold's last station and at the approved-edit round: an allow on a tool
81
+ * that states no permission of its own, under a mode whose `unstated` answer is `ask`, that no answer covers — no engine
82
+ * record of a settled wait licensing the bytes that run (`runs`; {@link answeredFor}). A layer's allow says nothing here
83
+ * whatever words it carries; who else may clear the question is the allow layer's business (its members, a hook's own `allow`
84
+ * among them), not this rule's. */
85
+ export declare function asksUnstated(d: PermissionResult, call: {
86
+ toolCallId: string;
87
+ toolName: string;
88
+ }, runs: unknown, seat: "passthrough" | undefined, mode: PermissionMode | undefined): boolean;
84
89
  /** The unstated question's sentence (the reference implementation's passthrough sentence, without its product name). */
85
90
  export declare const unstatedAskMessage: (toolName: string) => string;
86
- /** One call's permission seat: `passthrough` (an allow nobody stated meets the mode's unstated answer) and the question
91
+ /** One call's permission seat: `passthrough` (an allow no answer covers meets the mode's unstated answer) and the question
87
92
  * put when it does — absent ⇒ {@link unstatedAskMessage}. */
88
93
  export interface PermissionSeatReading {
89
94
  readonly seat: "passthrough";
@@ -91,7 +96,7 @@ export interface PermissionSeatReading {
91
96
  }
92
97
  /** The gate's permission-seat inputs (a member group of `ToolGateInput`), read through {@link permissionSeatAt}. */
93
98
  export interface PermissionSeatInputs {
94
- /** The called tool's permission seat ({@link import("./tool-spec.js").ToolSpec.permissionSeat}): `"passthrough"` ⇒ an allow no layer stated
99
+ /** The called tool's permission seat ({@link import("./tool-spec.js").ToolSpec.permissionSeat}): `"passthrough"` ⇒ an allow no answer covers
95
100
  * meets the mode's answer for an unstated call ({@link ModePolicy.unstated}). Absent ⇒ the tool answers for itself. */
96
101
  permissionSeat?: "passthrough";
97
102
  /** The seat read off THIS call's arguments, for a tool whose seat depends on them (the Runner wires it for the `Skill`
@@ -1,5 +1,6 @@
1
1
  import { createFsWriteGatePolicy } from "./fs-write-gate-policy.js";
2
2
  import { isProtectedWrite } from "./tool-registry.js";
3
+ import { answeredFor } from "./tool-policy.js";
3
4
  import { rosterEntryNamed } from "./tool-roster.js";
4
5
  import { wireOf } from "./tool-name-readings.js";
5
6
  import { clampPermissionMode, isPermissionMode, PERMISSION_MODES } from "./ask-origin.js";
@@ -41,8 +42,8 @@ export function modeAcceptsFileCommand(mode, toolName, args) {
41
42
  export function mountedToolDeclaresReadOnly(tools, toolName) {
42
43
  return tools.findLast((t) => t.name === toolName)?.mcpReadOnlyHint === true;
43
44
  }
44
- export function asksUnstated(d, seat, mode) {
45
- return d.action === "allow" && d.decisionReason === undefined && seat === "passthrough" && mode !== undefined && modePolicyOf(mode).unstated === "ask";
45
+ export function asksUnstated(d, call, runs, seat, mode) {
46
+ return d.action === "allow" && seat === "passthrough" && mode !== undefined && modePolicyOf(mode).unstated === "ask" && !answeredFor(d, call, runs);
46
47
  }
47
48
  export const unstatedAskMessage = (toolName) => `The agent requested permission to use ${toolName}, but it has not been granted yet.`;
48
49
  export function permissionSeatAt(input, args) {
@@ -24,7 +24,8 @@ export interface ReceiptMode {
24
24
  * once on the object (a getter that throws refuses the receipt; one that moves the object's prototype refuses it, but
25
25
  * under `others: "copy"`).
26
26
  * `refuse`: only own enumerable string-keyed DATA properties are members; an accessor, a non-enumerable property or
27
- * a symbol-keyed one refuses the receipt (no getter ever runs).
27
+ * a symbol-keyed one refuses the receipt (no getter ever runs) — under `contain`, each is a member left out and reported
28
+ * (a symbol-keyed one at its container's path and its `String(symbol)`), its container received.
28
29
  */
29
30
  readonly accessors: "call-once" | "refuse";
30
31
  /** How many levels of containers are copied (`Infinity` = every level). */
@@ -82,8 +82,13 @@ export function receive(value, mode, held) {
82
82
  const out = list ? [] : {};
83
83
  seen.set(v, out);
84
84
  open.add(v);
85
- if (mode.accessors === "refuse" && Object.getOwnPropertySymbols(captured).length > 0)
86
- refuseContainer(v, "hidden");
85
+ if (mode.accessors === "refuse") {
86
+ const symbols = Object.getOwnPropertySymbols(captured);
87
+ if (symbols.length > 0 && mode.contain !== true)
88
+ refuseContainer(v, "hidden");
89
+ for (const s of symbols)
90
+ faults.push(Object.freeze({ kind: "hidden", path: Object.freeze([...path, String(s)]) }));
91
+ }
87
92
  stack.push({ v, out, list, proto, captured, keys: Object.getOwnPropertyNames(captured), level, next: 0, height: 1, key: "", member: undefined });
88
93
  return out;
89
94
  };
@@ -35,8 +35,9 @@ export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "cli
35
35
  * · `envFacts` — the sandbox facts (profile / egress / scratchpad).
36
36
  * · `memoryPersistenceCapable` — the deployment's DECLARED persistence statement: `false` is a REMOVAL (floor),
37
37
  * `true` a default a child's own declaration may narrow.
38
- * · `memoryOff` — the request-level "memory is off" FACT (`spec.memory.enabled === false`): a REMOVAL
39
- * (floor) that REWRITES the child's own memory declaration instead of travelling as a value.
38
+ * · `memory` — the request-level "memory is off" FACT, `{ enabled: false }` and nothing else of the declaration (the clamp
39
+ * table's `memory.enabled` row; present only when the parent stated `enabled: false`): a REMOVAL (floor) that
40
+ * REWRITES the one member on the child's own memory declaration — its scopes and knobs are the child's.
40
41
  * · `compaction` — the request's CONTEXT-MANAGEMENT settings (enabled / withinTask / reserve / keepRecent /
41
42
  * instructions / attachWorkingFiles / staleToolResultOffload / clampTolerance). One set of settings for the
42
43
  * whole delegation tree, measured per leg against the leg's OWN window: the numbers are repaired against the
@@ -65,7 +66,9 @@ export interface ParentSpecSeat {
65
66
  readonly additionalReadDirectories?: readonly string[];
66
67
  readonly envFacts?: TaskSpec["envFacts"];
67
68
  readonly memoryPersistenceCapable?: boolean;
68
- readonly memoryOff?: true;
69
+ readonly memory?: {
70
+ readonly enabled: false;
71
+ };
69
72
  readonly compaction?: TaskSpec["compaction"];
70
73
  readonly skillListingBudgetFraction?: number;
71
74
  readonly skillListingMaxDescChars?: number;
@@ -73,13 +76,16 @@ export interface ParentSpecSeat {
73
76
  readonly promptForm?: "minimal";
74
77
  readonly promptBinding?: string;
75
78
  }
79
+ /** Refuse (by name, `config.deps_retired_key`) a seat that still carries a retired member. Read at every door a seat enters
80
+ * by hand: the ctx reader, the apply, and the send lane's waker clamps. */
81
+ export declare function assertParentSpecSeatCurrent(seat: object | undefined): void;
76
82
  /** Mint the seat off the parent's REBOUND spec and its frozen question face (spec over deps — the caller resolves it once,
77
83
  * the same value the parent's own question routing reads). Absent fields stay absent; `oneShot` only when `true`. */
78
84
  export declare function parentSpecSeatOf(spec: Omit<ParentSpecSeatFields, "onQuestion">, frozenOnQuestion: OnQuestion | undefined): ParentSpecSeat;
79
85
  /**
80
86
  * Apply the seat to a child spec — ONE law for every member, both lanes:
81
87
  * a value the child spec already carries WINS; the seat FILLS an unset one; the three REMOVAL values
82
- * (`oneShot: true`, `memoryPersistenceCapable: false`, `memoryOff: true`) land UNCONDITIONALLY.
88
+ * (`oneShot: true`, `memoryPersistenceCapable: false`, `memory: { enabled: false }`) land UNCONDITIONALLY.
83
89
  * "Already carries" is the child's own authorship — a deployment baseline, a trusted script, an agent definition's
84
90
  * fold (the Task lane folds the definition's `memoryPersistenceCapable` before applying; the workflow lane applies at
85
91
  * the launch site AFTER the agentType fold) — so a definition may narrow a parent `true` to `false`, a baseline-pinned
@@ -88,15 +94,6 @@ export declare function parentSpecSeatOf(spec: Omit<ParentSpecSeatFields, "onQue
88
94
  * No seat (a tool running outside a Runner task) ⇒ the child spec is returned as is.
89
95
  */
90
96
  export declare function applyParentSpecSeat<S extends ParentSpecSeatFields>(child: S, seat: ParentSpecSeat | undefined): S;
91
- /** The ONE way the memory-off floor is written onto a spec — the seat's apply above and the resume lane's
92
- * clamp fold both call it: keep every field the spec authored (scopes, write scope, content-safety knobs)
93
- * and state `enabled: false` over them. Returning a value rather than deleting the key IS the mechanism:
94
- * the next seat mint reads `spec.memory.enabled === false`, so a spec whose `memory` was removed would end
95
- * the floor one level down. A spec that declared nothing gets the bare `{ enabled: false }`, which the
96
- * normalizer reads as "no usable scopes" — identical to the absent declaration it replaces. */
97
- export declare function memoryTurnedOff(memory: TaskSpec["memory"]): NonNullable<TaskSpec["memory"]> & {
98
- enabled: false;
99
- };
100
97
  /** The ctx keys that restate a seat member as THIS run's own fact (tools read them for scope / receipts / clamps). */
101
98
  declare const SEAT_FACTS_ON_CTX: readonly ["principal", "oneShot", "onQuestion", "memoryPersistenceCapable"];
102
99
  /**