@sema-agent/core 7.53.0 → 7.54.1

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 (249) hide show
  1. package/CHANGELOG.md +291 -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 +7 -9
  104. package/dist/core/runner/prepare-acquire-reconcile.js +8 -6
  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 +683 -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/parent-chain-guard.d.ts +10 -0
  195. package/dist/engine/session/parent-chain-guard.js +10 -0
  196. package/dist/engine/session/repo-utils.d.ts +5 -0
  197. package/dist/engine/session/repo-utils.js +9 -0
  198. package/dist/engine/session/storage-base.js +5 -2
  199. package/dist/index.d.ts +23 -9
  200. package/dist/index.js +19 -8
  201. package/dist/internal/harness.d.ts +1 -0
  202. package/dist/internal/harness.js +1 -0
  203. package/dist/internal/llm.d.ts +1 -1
  204. package/dist/internal/llm.js +1 -1
  205. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  206. package/dist/orchestration/run-workflow-tool.js +1 -0
  207. package/dist/orchestration/workflow-governance.d.ts +1 -1
  208. package/dist/orchestration/workflow.d.ts +3 -0
  209. package/dist/orchestration/workflow.js +2 -1
  210. package/dist/prompt-assembly/catalog/schema.d.ts +3 -0
  211. package/dist/prompt-assembly/catalog/schema.js +3 -0
  212. package/dist/prompt-assembly/overlay.js +2 -2
  213. package/dist/prompt-assembly/select.js +2 -2
  214. package/dist/prompts/default.d.ts +1 -1
  215. package/dist/shell/lex.d.ts +87 -8
  216. package/dist/shell/lex.js +283 -49
  217. package/dist/stores/file/index.d.ts +8 -6
  218. package/dist/tools/artifact/artifact-tool.js +10 -0
  219. package/dist/tools/fs/bash-program-position.d.ts +3 -15
  220. package/dist/tools/fs/bash-program-position.js +3 -10
  221. package/dist/tools/fs/bash-readonly-classifier.js +2 -0
  222. package/dist/tools/fs/fs-bash.d.ts +3 -3
  223. package/dist/tools/fs/fs-bash.js +72 -101
  224. package/dist/tools/fs/fs-read.d.ts +12 -0
  225. package/dist/tools/fs/fs-read.js +10 -9
  226. package/dist/tools/fs/fs-shared.d.ts +27 -31
  227. package/dist/tools/fs/fs-shared.js +12 -15
  228. package/dist/tools/fs/fs-write.js +1 -1
  229. package/dist/tools/fs/index.js +2 -2
  230. package/dist/tools/fs/read-deny-home.d.ts +1 -1
  231. package/dist/tools/fs/read-deny-home.js +5 -4
  232. package/dist/tools/fs/read-deny.d.ts +16 -3
  233. package/dist/tools/fs/read-deny.js +12 -9
  234. package/dist/tools/fs/safety.d.ts +7 -6
  235. package/dist/tools/fs/shell-read-bindings.d.ts +5 -3
  236. package/dist/tools/fs/shell-read-bindings.js +15 -16
  237. package/dist/tools/fs/shell-read-device.js +1 -1
  238. package/dist/tools/fs/shell-read-plan.js +36 -47
  239. package/dist/tools/fs/shell-read-word.d.ts +1 -6
  240. package/dist/tools/fs/shell-read-word.js +2 -20
  241. package/dist/tools/fs/shell-reentry-text.js +1 -1
  242. package/dist/tools/web.d.ts +2 -2
  243. package/package.json +4 -1
  244. package/test/export-surface.snapshot.json +5654 -5520
  245. package/wire-changes/core@7.53.0.json +30 -8
  246. package/wire-changes/core@7.54.0.json +2870 -0
  247. package/wire-changes/core@7.54.1.json +37 -0
  248. package/dist/core/shell-scan.d.ts +0 -60
  249. package/dist/core/shell-scan.js +0 -183
@@ -0,0 +1,196 @@
1
+ import { hasConversationContent } from "./session.js";
2
+ export const CROSS_BOUNDARY_TAINT_BITS = Object.freeze(["targetExecutionArmed", "offTargetReadObserved"]);
3
+ export const CROSS_BOUNDARY_POSTURES = Object.freeze(["single_user", "multi_tenant"]);
4
+ const postureSeatIsTheSet = true;
5
+ void postureSeatIsTheSet;
6
+ export const BOUNDARY_LEGS = Object.freeze(["env", "outside_read", "outside_write"]);
7
+ const LEG_SET = new Set(BOUNDARY_LEGS);
8
+ const OBLIGATION_TABLE = {
9
+ env_to_target: { host_lane: "none", armed: "none", armed_observed: "mandate_if_governed" },
10
+ outside_read: { host_lane: "none", armed: "record", armed_observed: "none" },
11
+ outside_write: { host_lane: "none", armed: "mandate_if_governed", armed_observed: "mandate_if_governed" },
12
+ };
13
+ const ENV_LEG_ROWS = { target: ["env_to_target"], off_target: ["outside_read", "outside_write"] };
14
+ const TREE_MEMBERS = [...CROSS_BOUNDARY_TAINT_BITS, "governed"];
15
+ function assertTree(tree, at) {
16
+ if (typeof tree !== "object" || tree === null || Array.isArray(tree))
17
+ throw new TypeError(`${at}: a cross-boundary tree is a record of presence bits (got ${tree === null ? "null" : Array.isArray(tree) ? "an array" : typeof tree})`);
18
+ for (const member of TREE_MEMBERS) {
19
+ const v = tree[member];
20
+ if (v !== undefined && v !== true)
21
+ throw new TypeError(`${at}: tree member "${member}" is presence-coded (true or absent), got ${typeof v}`);
22
+ }
23
+ }
24
+ export function crossBoundaryObligation(legs, reachedEnvIsTarget, tree) {
25
+ if (!Array.isArray(legs))
26
+ throw new TypeError(`crossBoundaryObligation: legs is a list of ${BOUNDARY_LEGS.join(" | ")}`);
27
+ if (typeof reachedEnvIsTarget !== "boolean")
28
+ throw new TypeError("crossBoundaryObligation: reachedEnvIsTarget is a boolean");
29
+ assertTree(tree, "crossBoundaryObligation");
30
+ const column = tree.targetExecutionArmed !== true ? "host_lane" : tree.offTargetReadObserved === true ? "armed_observed" : "armed";
31
+ let mandate = false;
32
+ let record = false;
33
+ for (const leg of legs) {
34
+ if (!LEG_SET.has(leg))
35
+ throw new TypeError(`crossBoundaryObligation: ${JSON.stringify(leg)} is not a boundary leg (${BOUNDARY_LEGS.join(", ")})`);
36
+ const rows = leg === "env" ? ENV_LEG_ROWS[reachedEnvIsTarget ? "target" : "off_target"] : [leg];
37
+ for (const row of rows) {
38
+ const cell = OBLIGATION_TABLE[row][column];
39
+ if (cell === "record")
40
+ record = true;
41
+ else if (cell === "mandate_if_governed" && crossBoundaryTreeGoverned(tree))
42
+ mandate = true;
43
+ }
44
+ }
45
+ return Object.freeze({ mandate, record });
46
+ }
47
+ export function crossBoundaryTreeGoverned(tree) {
48
+ return tree.governed === true;
49
+ }
50
+ export function joinCrossBoundaryTree(a, b) {
51
+ assertTree(a, "joinCrossBoundaryTree");
52
+ assertTree(b, "joinCrossBoundaryTree");
53
+ const out = {};
54
+ if (a.targetExecutionArmed === true || b.targetExecutionArmed === true)
55
+ out.targetExecutionArmed = true;
56
+ if (a.offTargetReadObserved === true || b.offTargetReadObserved === true)
57
+ out.offTargetReadObserved = true;
58
+ if (a.governed === true || b.governed === true)
59
+ out.governed = true;
60
+ return Object.freeze(out);
61
+ }
62
+ export function crossBoundaryTreeCovers(covering, tree) {
63
+ assertTree(covering, "crossBoundaryTreeCovers");
64
+ assertTree(tree, "crossBoundaryTreeCovers");
65
+ return TREE_MEMBERS.every((m) => tree[m] !== true || covering[m] === true);
66
+ }
67
+ export const STRICTEST_CROSS_BOUNDARY_TREE = Object.freeze({ targetExecutionArmed: true, offTargetReadObserved: true, governed: true });
68
+ export const CROSS_BOUNDARY_RECORD_TYPE = "engine:cross_boundary";
69
+ export function crossBoundaryRecordData(tree) {
70
+ assertTree(tree, "crossBoundaryRecordData");
71
+ if (tree.targetExecutionArmed !== true)
72
+ throw new TypeError("crossBoundaryRecordData: only an armed tree is recorded");
73
+ return Object.freeze({ targetExecutionArmed: true, ...(tree.offTargetReadObserved === true ? { offTargetReadObserved: true } : {}), ...(tree.governed === true ? { governed: true } : {}) });
74
+ }
75
+ const RECORD_MEMBERS = TREE_MEMBERS;
76
+ function ownedRecordTree(data) {
77
+ if (typeof data !== "object" || data === null || Array.isArray(data) || Object.getPrototypeOf(data) !== Object.prototype)
78
+ return { why: `its data is ${data === null ? "null" : Array.isArray(data) ? "an array" : typeof data === "object" ? "not a plain record" : typeof data}` };
79
+ const owned = new Map();
80
+ for (const key of Reflect.ownKeys(data)) {
81
+ if (typeof key !== "string" || !RECORD_MEMBERS.includes(key))
82
+ return { why: `its data carries ${typeof key === "string" ? JSON.stringify(key.slice(0, 40)) : "a symbol key"}, which is not a member` };
83
+ const d = Object.getOwnPropertyDescriptor(data, key);
84
+ if (d === undefined || !("value" in d))
85
+ return { why: `"${key}" is not a data member` };
86
+ owned.set(key, d.value);
87
+ }
88
+ if (owned.get("targetExecutionArmed") !== true)
89
+ return { why: `"targetExecutionArmed" is not true` };
90
+ for (const m of ["offTargetReadObserved", "governed"])
91
+ if (owned.get(m) !== undefined && owned.get(m) !== true)
92
+ return { why: `"${m}" is neither true nor absent` };
93
+ return { tree: Object.freeze({ targetExecutionArmed: true, ...(owned.get("offTargetReadObserved") === true ? { offTargetReadObserved: true } : {}), ...(owned.get("governed") === true ? { governed: true } : {}) }) };
94
+ }
95
+ function readBranchEntry(branch, i) {
96
+ let id;
97
+ try {
98
+ const e = branch[i];
99
+ if (typeof e !== "object" || e === null)
100
+ return { kind: "other", content: false };
101
+ const { type, customType } = e;
102
+ if (type !== "custom" || customType !== CROSS_BOUNDARY_RECORD_TYPE)
103
+ return { kind: "other", content: typeof type === "string" && hasConversationContent([{ type }]) };
104
+ const rawId = e.id;
105
+ id = typeof rawId === "string" ? rawId : undefined;
106
+ const read = ownedRecordTree(e.data);
107
+ return "why" in read ? { kind: "unreadable", ...(id !== undefined ? { id } : {}), why: read.why } : { kind: "record", ...(id !== undefined ? { id } : {}), tree: read.tree };
108
+ }
109
+ catch {
110
+ return { kind: "unreadable", ...(id !== undefined ? { id } : {}), why: "an entry on the branch could not be read" };
111
+ }
112
+ }
113
+ export function readCrossBoundaryRecord(branch) {
114
+ const strictest = (id, why) => Object.freeze({ kind: "malformed", ...(id !== undefined ? { entryId: id } : {}), tree: STRICTEST_CROSS_BOUNDARY_TREE, why: `the cross-boundary record on this branch cannot be read: ${why}` });
115
+ let isList;
116
+ let length;
117
+ try {
118
+ isList = Array.isArray(branch);
119
+ length = isList ? branch.length : undefined;
120
+ }
121
+ catch {
122
+ return strictest(undefined, "the branch could not be read");
123
+ }
124
+ if (!isList)
125
+ throw new TypeError("readCrossBoundaryRecord: the branch is a list of session entries");
126
+ if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0)
127
+ return strictest(undefined, "the branch could not be read");
128
+ let joined;
129
+ let newest;
130
+ let uncoveredContent = false;
131
+ for (let i = 0; i < length; i++) {
132
+ const entry = readBranchEntry(branch, i);
133
+ if (entry.kind === "other") {
134
+ uncoveredContent ||= entry.content;
135
+ continue;
136
+ }
137
+ if (entry.kind === "unreadable")
138
+ return strictest(entry.id, entry.why);
139
+ joined = joinCrossBoundaryTree(joined ?? {}, entry.tree);
140
+ newest = entry.id;
141
+ uncoveredContent = false;
142
+ }
143
+ return joined === undefined ? Object.freeze({ kind: "absent", uncoveredContent }) : Object.freeze({ kind: "recorded", ...(newest !== undefined ? { entryId: newest } : {}), tree: joined, uncoveredContent });
144
+ }
145
+ export function crossBoundaryPostureUndeclaredNotice(at) {
146
+ assertTree(at.tree, "crossBoundaryPostureUndeclaredNotice");
147
+ return {
148
+ code: "config.cross_boundary_posture_undeclared",
149
+ message: 'This run executes on an external-content target (the device lane) and no RunnerDeps.crossBoundaryPosture is configured — the engine reads the permissive "single_user" posture. ' +
150
+ 'On a multi-tenant deployment declare "multi_tenant", so an org-side write from a tainted run is put to a person; on a single-user deployment declare "single_user" to silence this line.',
151
+ detail: { sessionId: at.sessionId, runId: at.runId, taint: CROSS_BOUNDARY_TAINT_BITS.filter((b) => at.tree[b] === true) },
152
+ };
153
+ }
154
+ export function crossBoundaryRecordUnwrittenNotice(at) {
155
+ return {
156
+ code: "config.cross_boundary_record_unwritten",
157
+ message: `The session's cross-boundary record could not be written at ${at.phase} (${at.cause.slice(0, 300)}) — ` +
158
+ (at.governed
159
+ ? "the tree is governed: the next leg on this session reads the content after its newest record as off-target, so its device calls are put to a person."
160
+ : "the tree is not governed: no judgment reads the record, so its failure refused nothing; the tree's growth, or the leg's end, writes next."),
161
+ detail: { sessionId: at.sessionId, runId: at.runId, phase: at.phase, governed: at.governed, cause: at.cause.slice(0, 300) },
162
+ };
163
+ }
164
+ const NO_LEGS = Object.freeze([]);
165
+ export function boundaryFlowFace(entry) {
166
+ return entry?.boundaryFlow === "engine" ? { boundaryLegs: NO_LEGS } : {};
167
+ }
168
+ function ownedLegList(v) {
169
+ try {
170
+ if (!Array.isArray(v))
171
+ return undefined;
172
+ const length = v.length;
173
+ if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0)
174
+ return undefined;
175
+ const held = new Set();
176
+ for (let i = 0; i < length; i++) {
177
+ const leg = v[i];
178
+ if (!LEG_SET.has(leg))
179
+ return undefined;
180
+ held.add(leg);
181
+ }
182
+ return BOUNDARY_LEGS.filter((l) => held.has(l));
183
+ }
184
+ catch {
185
+ return undefined;
186
+ }
187
+ }
188
+ export function rosterBoundaryLegs(source, declared, effect) {
189
+ const list = declared === undefined || declared === "per_call" ? undefined : ownedLegList(declared);
190
+ if (declared !== undefined && declared !== "per_call" && list === undefined)
191
+ return undefined;
192
+ const reads = source === "builtin" || source === "synthetic" || source === "mcp" || source === "a2a";
193
+ if (!reads || declared === undefined)
194
+ return effect === "read" ? ["outside_read"] : ["outside_write"];
195
+ return declared === "per_call" ? "per_call" : list;
196
+ }
@@ -1,4 +1,5 @@
1
- import type { PermissionResult } from "./tool-policy.js";
1
+ import { type AssertAllKeysHandled } from "./ask-origin.js";
2
+ import type { DecisionMember, PermissionResult } from "./tool-policy.js";
2
3
  export declare const refusedOutOfContract: (message: string) => PermissionResult;
3
4
  /** Is `d` a refusal the engine itself minted (by object identity — a look-alike `deny` a caller built is not one)? The receipt's
4
5
  * answer is one of two things (the engine's refusal or its detached copy), and this tells them apart. */
@@ -12,6 +13,48 @@ export declare const isEngineRefusal: (d: unknown) => boolean;
12
13
  * two entry-list members) — one word per author, so "who said this needs no asking" is read off the decision. */
13
14
  declare const DECISION_REASONS: readonly ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only", "skill", "deployment_rule", "request_rule"];
14
15
  export type DecisionReason = (typeof DECISION_REASONS)[number];
16
+ /** The arms of the decision union that declare member `M`. */
17
+ type ArmOf<M extends DecisionMember> = PermissionResult extends infer U ? (U extends {
18
+ action: infer A;
19
+ } ? (M extends keyof U ? A : never) : never) : never;
20
+ /**
21
+ * Every member of the decision union with the ARMS that declare it — the one table the receipt's member list is read off
22
+ * (`DECISION_MEMBERS`), fenced to the union both ways: `satisfies` admits no member and no arm the union lacks, and
23
+ * {@link DecisionMemberArmsCoverTheUnion} no arm the union has and a row lacks. A present member on an arm outside its row is
24
+ * out of contract like a word outside a closed set ({@link screenContractValues}): an ask's mark on an allow or a deny is a
25
+ * decision that contradicts itself, and a consumer branching on the member would read the other arm's meaning.
26
+ */
27
+ export declare const DECISION_MEMBER_ARMS: Readonly<{
28
+ readonly action: readonly ["allow", "ask", "deny"];
29
+ readonly updatedInput: readonly ["allow", "ask", "deny"];
30
+ readonly message: readonly ["allow", "ask", "deny"];
31
+ readonly decisionReason: readonly ["allow", "ask", "deny"];
32
+ readonly attestation: readonly ["allow", "ask", "deny"];
33
+ readonly requiresRealApproval: readonly ["ask"];
34
+ readonly denialLimitFallback: readonly ["ask"];
35
+ readonly classifierNoRuling: readonly ["ask"];
36
+ readonly allowRuleShadowed: readonly ["ask"];
37
+ readonly probeReason: readonly ["ask"];
38
+ readonly probeCause: readonly ["ask"];
39
+ readonly probeMandated: readonly ["ask"];
40
+ readonly crossBoundaryMandated: readonly ["ask"];
41
+ readonly mandated: readonly ["ask"];
42
+ readonly mandate: readonly ["ask"];
43
+ readonly probeReadRootCandidate: readonly ["ask"];
44
+ readonly matchedAskRule: readonly ["ask"];
45
+ readonly ruleEvidence: readonly ["ask"];
46
+ readonly segmentCoverage: readonly ["ask"];
47
+ readonly origin: readonly ["ask"];
48
+ readonly askClass: readonly ["ask"];
49
+ readonly classes: readonly ["ask"];
50
+ readonly guards: readonly ["ask"];
51
+ readonly ruleUnreadable: readonly ["ask"];
52
+ readonly authoredBy: readonly ["ask"];
53
+ readonly foldedAskClasses: readonly ["ask"];
54
+ }>;
55
+ export type DecisionMemberArmsCoverTheUnion = AssertAllKeysHandled<{
56
+ [M in DecisionMember]: Exclude<ArmOf<M>, (typeof DECISION_MEMBER_ARMS)[M][number]>;
57
+ }[DecisionMember]>;
15
58
  export declare const RETIRED_WINDOW_MEMBER_DENY_MESSAGE: string;
16
59
  export declare const carriesRetiredWindow: (fallback: unknown) => boolean;
17
60
  /**
@@ -32,8 +32,17 @@ const CLOSED_MEMBERS = [
32
32
  { member: "classifierNoRuling", words: AUTO_MODE_FALLBACK_CAUSES, has: isAutoModeFallbackCause },
33
33
  { member: "requiresRealApproval", words: [true, false], has: (v) => v === true || v === false },
34
34
  { member: "probeMandated", words: [true], has: (v) => v === true },
35
+ { member: "crossBoundaryMandated", words: [true], has: (v) => v === true },
35
36
  { member: "mandated", words: [true], has: (v) => v === true },
36
37
  ];
38
+ const EVERY_ARM = ["allow", "ask", "deny"];
39
+ const ASK = ["ask"];
40
+ export const DECISION_MEMBER_ARMS = Object.freeze({
41
+ action: EVERY_ARM, updatedInput: EVERY_ARM, message: EVERY_ARM, decisionReason: EVERY_ARM, attestation: EVERY_ARM,
42
+ requiresRealApproval: ASK, denialLimitFallback: ASK, classifierNoRuling: ASK, allowRuleShadowed: ASK, probeReason: ASK, probeCause: ASK,
43
+ probeMandated: ASK, crossBoundaryMandated: ASK, mandated: ASK, mandate: ASK, probeReadRootCandidate: ASK, matchedAskRule: ASK, ruleEvidence: ASK,
44
+ segmentCoverage: ASK, origin: ASK, askClass: ASK, classes: ASK, guards: ASK, ruleUnreadable: ASK, authoredBy: ASK, foldedAskClasses: ASK,
45
+ });
37
46
  const RETIRED_WINDOW_MEMBER = "autoDenyAfterMs";
38
47
  export const RETIRED_WINDOW_MEMBER_DENY_MESSAGE = `a permission decision's "denialLimitFallback" carries "${RETIRED_WINDOW_MEMBER}" — the decision names the counts only; ` +
39
48
  `the auto-deny window is decided by the ask's route (a live approver arms it, every other route runs none) and cannot be ` +
@@ -91,6 +100,13 @@ export function screenContractValues(contract, opts) {
91
100
  continue;
92
101
  return unrecognized(member, v, words);
93
102
  }
103
+ const arm = contract.action;
104
+ for (const [member, arms] of Object.entries(DECISION_MEMBER_ARMS)) {
105
+ if (contract[member] === undefined || (arm === undefined ? EVERY_ARM.every((a) => arms.includes(a)) : arms.includes(arm)))
106
+ continue;
107
+ return refusedOutOfContract(`a permission decision carries "${member}" on ${arm === undefined ? "a result with no action (no opinion)" : `its "${arm}" arm`} — the member belongs to ` +
108
+ `the ${arms.join("/")} arm only; denied fail-closed rather than letting consumers branch on a member its arm does not hold`);
109
+ }
94
110
  if (carriesRetiredWindow(contract.denialLimitFallback))
95
111
  return refusedOutOfContract(RETIRED_WINDOW_MEMBER_DENY_MESSAGE);
96
112
  return undefined;
@@ -262,6 +262,9 @@ export declare function argsAsTheToolReads(req: {
262
262
  export declare function argsReadBy(read: (args: unknown) => unknown, args: unknown): unknown;
263
263
  /** The value `args` was read from, when a reading moved it; otherwise `args` itself. */
264
264
  export declare function argsReadFrom(args: unknown): unknown;
265
+ /** `copy` is a detached copy of `original` (the decision receipt's): the same bytes, so read from what `original` was read
266
+ * from — the recorded step survives the copy instead of being lost to the copy's new identity. */
267
+ export declare function carryArgsReadFrom(original: unknown, copy: unknown): void;
265
268
  /**
266
269
  * `req` with its arguments replaced by `args` AS THE TOOL READS THEM ({@link argsAsTheToolReads}) and — when `req` carries
267
270
  * a reading of its own target — a reading of the NEW args taken the same way (same environment, same root / cwd, same
@@ -167,6 +167,10 @@ const READ_FROM = new WeakMap();
167
167
  export function argsReadFrom(args) {
168
168
  return typeof args === "object" && args !== null && READ_FROM.has(args) ? READ_FROM.get(args) : args;
169
169
  }
170
+ export function carryArgsReadFrom(original, copy) {
171
+ if (typeof original === "object" && original !== null && READ_FROM.has(original) && typeof copy === "object" && copy !== null)
172
+ READ_FROM.set(copy, READ_FROM.get(original));
173
+ }
170
174
  export async function withArgsReading(req, args, signal) {
171
175
  const read = argsAsTheToolReads(req, args);
172
176
  const reading = callTargetReadingOf(req);
@@ -399,6 +399,16 @@ export interface EngineNotice {
399
399
  * ({@link import("./secret-env.js").holdOwnCredentialValues}); `detail: { reason }` — the kind, never the value. Delivered
400
400
  * to `RunnerDeps.onNotice` from the `new Runner` receipt; the intakes with no structured seat (a brain's construction,
401
401
  * a request's route reading, a peer's dial, a directly built distiller seat) print its line on `console.warn`.
402
+ * - `"config.cross_boundary_posture_undeclared"` — a run whose tree executes on a target device (armed) on a deployment that
403
+ * configured no `RunnerDeps.crossBoundaryPosture`: the permissive `"single_user"` reading is in force. Once per run;
404
+ * `detail: { sessionId, runId, taint }` — `taint` lists the tree's taint bits
405
+ * ({@link import("./cross-boundary-taint.js").CROSS_BOUNDARY_TAINT_BITS}). Minted by
406
+ * {@link import("./cross-boundary-taint.js").crossBoundaryPostureUndeclaredNotice}. Audience `operator`.
407
+ * - `"config.cross_boundary_record_unwritten"` — a session's cross-boundary record the engine owed could not be written (the
408
+ * store threw, or answered with no entry id) and refused nothing: on a tree that is not governed no judgment reads the record;
409
+ * at a governed leg's prepare or settle the next leg reads the content after the newest record as off-target. Per failed write;
410
+ * `detail: { sessionId, runId, phase, governed, cause }` (`phase`: `prepare` / `exit` / `suspend` / `settle`). Minted by
411
+ * {@link import("./cross-boundary-taint.js").crossBoundaryRecordUnwrittenNotice}. Audience `operator`.
402
412
  * - `"config.permission_mode_unavailable"` — the permission mode a task requested is not the mode the leg runs
403
413
  * under: the deployment denies it (`by: "operator_deny"` — `RuntimeCaps.bypassPermissions` / `autoMode` is
404
414
  * `false`), a requested `auto` has no classifier face (`by: "face_absent"` — neither the leg's deps nor its chain carry
@@ -477,7 +487,13 @@ export interface EngineNotice {
477
487
  * frame's two count keys). They are NOT redelivered (a steer aimed at a finished run must not
478
488
  * fire at the next one — unlike ENGINE notes, which pend per session); the loud half of the
479
489
  * #257 contract's "accepted = enqueued, not consumed" sentence;
480
- * `detail: { <countKey>, cause, taskId?, sessionId?, runId? }` (the `code` and the `countKey` of each family are the
490
+ * The words a person wrote on an approval card (a durable `policy_ask` allow's `feedback`) that did not reach the
491
+ * model are counted in the SAME account under the steer code — one more row of that family, `detail.kind:
492
+ * "approval_feedback"` (absent on every other row) — when the message carrying them never committed and the row did not come back
493
+ * (a returned row does not carry them: its next decide is fresh); when the pause tried to carry them forward and the parked-steer
494
+ * carrier refused, the row also names the store's refusal as `detail.carry` (`steering.invalid_content` | `steering.queue_full`) and
495
+ * its sentence says so (`cause` stays `leg_end`: the leg ended with them owed — the closed set has no carrier words);
496
+ * `detail: { <countKey>, cause, kind?, carry?, taskId?, sessionId?, runId? }` (the `code` and the `countKey` of each family are the
481
497
  * table {@link USER_INPUT_UNDRAINED_FAMILIES}, keyed by the lane the input was queued on — `injectionLaneOf` — and the
482
498
  * minter writes exactly those; `cause` on both rows, closed `USER_INPUT_UNDRAINED_CAUSES`: `"leg_end"` — this engine's arm, the
483
499
  * leg ended with it still owed; `"crashed"` — minted by the SERVING LAYER, not this engine, before the
@@ -874,7 +890,8 @@ export interface EngineNotice {
874
890
  * toolCallId, cause, parkLaneExisted, remedy? }` — `parkLaneExisted` says whether a park lane was armed and
875
891
  * declined/failed (`true`) or none existed (`false`, the usual shape); `remedy` (the two causes that say nobody
876
892
  * could be asked — `approver_unavailable` and `no_approver_wired`; never `mandate_unreconstructible`)
877
- * is the one sentence naming the caller-side change that would have let the call run, as the tool text carries it. Audience `"user"`: the refused
893
+ * is the one sentence naming the caller-side change that would have let the call run, as the tool text carries it (for arguments
894
+ * no approval can be bound to, the binding's own sentence: the remedy's approver half would not let them run). Audience `"user"`: the refused
878
895
  * work is the session user's own; the operator hears it through the sink like every code.
879
896
  *
880
897
  * - `"memory.consolidation_recommended"` (design/339 §2.2/§6.2) — the engine-minted per-scope
@@ -1015,6 +1032,16 @@ export interface OwedUserInputAccount {
1015
1032
  seq?: number;
1016
1033
  }>;
1017
1034
  carried: number;
1035
+ /** The approving person's words (a `policy_ask` allow's `feedback`), counted APART from `frames`: owed, and how many the migration carried.
1036
+ * They are on no row of their own, and a returned row (the compensation reopened it) does not carry them either — its next decide is a
1037
+ * fresh one the host may re-supply them on — so, unlike the caller's own frames, they are not announced when the row came back (the row is
1038
+ * pending again and the person is asked again). `refusal`: the code the parked-steer carrier answered when the pause tried to carry them
1039
+ * (`steering.invalid_content` — over its bound; `steering.queue_full`), the store's own words; absent when no carry was refused. */
1040
+ approval?: {
1041
+ owed: number;
1042
+ carried: number;
1043
+ refusal?: ApprovalCarryRefusal;
1044
+ };
1018
1045
  };
1019
1046
  }
1020
1047
  /**
@@ -1069,6 +1096,9 @@ export declare function settleOwedUserInput(account: OwedUserInputAccount, rowRe
1069
1096
  */
1070
1097
  export declare const USER_INPUT_UNDRAINED_CAUSES: readonly ["leg_end", "crashed"];
1071
1098
  export type UserInputUndrainedCause = (typeof USER_INPUT_UNDRAINED_CAUSES)[number];
1099
+ /** What the parked-steer carrier answered (the checkpoint store's own refusal codes, not a set of this notice) when a pause tried to carry an
1100
+ * approving person's words: refused as content / over its bound, or its queue was full. `detail.carry` of the approval row, absent otherwise. */
1101
+ export type ApprovalCarryRefusal = "steering.invalid_content" | "steering.queue_full";
1072
1102
  /**
1073
1103
  * The two families of the stranded-input account, keyed by the lane the input was queued on (`injectionLaneOf` maps a steer's
1074
1104
  * tier to it): the notice `code` the family is announced under, and the `detail` key that carries its count
@@ -1113,5 +1143,7 @@ export declare function parentLegIds(internals: {
1113
1143
  export declare function undrainedUserInputNotices(counts: {
1114
1144
  steer: number;
1115
1145
  followUp: number;
1146
+ approvalFeedback?: number;
1147
+ approvalCarry?: ApprovalCarryRefusal;
1116
1148
  }, taskId?: string, sessionId?: string, runId?: string, parent?: ParentLegIds): EngineNotice[];
1117
1149
  export {};
@@ -44,7 +44,10 @@ export function settleOwedUserInput(account, rowReturned, ids, onNotice) {
44
44
  SETTLED_OWED_ACCOUNTS.add(account);
45
45
  try {
46
46
  const owed = owedUserInputCounts(account.held, account.migrated, { ...account.continuation, rowReturned });
47
- for (const notice of undrainedUserInputNotices(owed, ids.taskId, ids.sessionId, ids.runId, ids.parent))
47
+ const approval = account.continuation.approval;
48
+ const approvalFeedback = approval === undefined || rowReturned ? 0 : Math.max(0, approval.owed - approval.carried);
49
+ const approvalCarry = approvalFeedback > 0 ? approval?.refusal : undefined;
50
+ for (const notice of undrainedUserInputNotices({ ...owed, ...(approvalFeedback > 0 ? { approvalFeedback } : {}), ...(approvalCarry !== undefined ? { approvalCarry } : {}) }, ids.taskId, ids.sessionId, ids.runId, ids.parent))
48
51
  deliverEngineNotice(onNotice, notice);
49
52
  }
50
53
  catch {
@@ -84,6 +87,19 @@ export function undrainedUserInputNotices(counts, taskId, sessionId, runId, pare
84
87
  if (toParent)
85
88
  copies.push({ code: STEER_UNDRAINED, message: `${message}${copyTail}`, detail: { ...count, ...ptid, sessionId: parentSession, childSessionId: sessionId } });
86
89
  }
90
+ const approvalFeedback = counts.approvalFeedback ?? 0;
91
+ if (approvalFeedback > 0) {
92
+ const why = counts.approvalCarry === "steering.invalid_content"
93
+ ? "the leg paused with them still owed and the parked-steer carrier refused them (longer than a parked steer may be)"
94
+ : counts.approvalCarry === "steering.queue_full"
95
+ ? "the leg paused with them still owed and the parked-steer carrier refused them (its queue was full)"
96
+ : "the leg ended before the message carrying them was committed";
97
+ const message = `${approvalFeedback} approval-card feedback(s) (the words a person wrote when approving a tool call) never reached the model — ${why}. They are NOT redelivered; re-send if still wanted.`;
98
+ const count = { [USER_INPUT_UNDRAINED_FAMILIES.steer.countKey]: approvalFeedback, cause, kind: "approval_feedback", ...(counts.approvalCarry !== undefined ? { carry: counts.approvalCarry } : {}) };
99
+ own.push({ code: STEER_UNDRAINED, message, detail: { ...count, ...tid, ...sid, ...rid } });
100
+ if (toParent)
101
+ copies.push({ code: STEER_UNDRAINED, message: `${message}${copyTail}`, detail: { ...count, ...ptid, sessionId: parentSession, childSessionId: sessionId } });
102
+ }
87
103
  if (counts.followUp > 0) {
88
104
  const message = `${counts.followUp} user follow-up(s) ${tail}`;
89
105
  const count = { [USER_INPUT_UNDRAINED_FAMILIES.followUp.countKey]: counts.followUp, cause };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The engine's storage seats as ONE backend contract.
3
+ *
4
+ * A backend is the thing a deployment constructs over one storage — a data root, a database — and whose
5
+ * members it wires into {@link import("./runner-deps.js").RunnerDeps}. Before this contract the shape of
6
+ * "a backend" was whatever the file family's aggregate class happened to carry, so a second backend had
7
+ * nothing to implement and a host composing several had nothing to type its engine half against. The
8
+ * shape now lives here, once: the bundled file backend implements it, and so does any other.
9
+ *
10
+ * Membership rule: a member is an engine seat whose durable store lives in the backend's storage and that
11
+ * every backend supplies, the bundled file backend included — `retention` is the one optional member,
12
+ * and a seat the file backend does not supply is not a member. Each member's NAME is its `RunnerDeps` seat and its TYPE is
13
+ * that seat's store type, so each member wires into its seat as it is (name the seats; a backend object may carry
14
+ * members that are not seats, so it is not spread). The member-to-seat correspondence is pinned in both directions
15
+ * by the test suite, together with the classification of the store-shaped seats that are not members. One reading per member:
16
+ * - `fileHistoryStore` is the store only. The seat also accepts the word `"disabled"`; that word is the
17
+ * deployment's wiring decision, not something a storage supplies.
18
+ *
19
+ * A host's own product stores do not belong here: a host that keeps more stores beside these composes
20
+ * an `EngineStorageBackend` with its own, rather than widening this contract.
21
+ */
22
+ import type { SessionStore } from "./session.js";
23
+ import type { CheckpointStore } from "./checkpoint-store.js";
24
+ import type { ToolResultStore } from "./tool-result-store.js";
25
+ import type { SessionPolicyStore } from "./session-policy-store.js";
26
+ import type { WorkflowJournalStore } from "./workflow-journal-store.js";
27
+ import type { UsageWindowStore } from "./usage-window-store.js";
28
+ import type { BackgroundAgentStore } from "./background-agent-store.js";
29
+ import type { MailboxStore } from "./mailbox-store.js";
30
+ import type { FileHistoryStore } from "./file-history-store.js";
31
+ import type { ManagedRetentionCapability } from "./retention.js";
32
+ import type { RosterStore } from "../agents/roster-store.js";
33
+ export interface EngineStorageBackend {
34
+ /** `RunnerDeps.sessionStore`. */
35
+ readonly sessionStore: SessionStore;
36
+ /** `RunnerDeps.checkpointStore`. */
37
+ readonly checkpointStore: CheckpointStore;
38
+ /** `RunnerDeps.toolResultStore`. */
39
+ readonly toolResultStore: ToolResultStore;
40
+ /** `RunnerDeps.sessionPolicyStore`. */
41
+ readonly sessionPolicyStore: SessionPolicyStore;
42
+ /** `RunnerDeps.workflowJournalStore`. */
43
+ readonly workflowJournalStore: WorkflowJournalStore;
44
+ /** `RunnerDeps.usageWindowStore`. */
45
+ readonly usageWindowStore: UsageWindowStore;
46
+ /** `RunnerDeps.backgroundAgentStore` — and the same instance wherever a deployment-composed Agent tool
47
+ * takes its agent store (the seat's pairing rule). */
48
+ readonly backgroundAgentStore: BackgroundAgentStore;
49
+ /** `RunnerDeps.mailboxStore` (same single-instance pairing as `backgroundAgentStore`). */
50
+ readonly mailboxStore: MailboxStore;
51
+ /** `RunnerDeps.rosterStore`. */
52
+ readonly rosterStore: RosterStore;
53
+ /** `RunnerDeps.fileHistoryStore` — the store; the seat's `"disabled"` word is not a backend's to give. */
54
+ readonly fileHistoryStore: FileHistoryStore;
55
+ /**
56
+ * The backend-level managed-retention executor, when this backend has one.
57
+ *
58
+ * Retention execution belongs to the backend, not to each store: the capability's session deletion
59
+ * re-checks live references inside the deletion transaction, and those references are held by several
60
+ * stores at once (pending checkpoints, background-agent rows, workflow runs), so only a party that
61
+ * spans them can honor it. Absent ⇒ this backend executes no managed retention — the bundled file
62
+ * backend is one such backend.
63
+ *
64
+ * Not the same field as a store's `retention`: on a store that member is the declaration WORD
65
+ * ({@link import("./retention.js").RetentionDeclaring}, `"managed" | "none"`); here it is the
66
+ * executor OBJECT. A backend is not a store: handed to `assertRetentionCapability` as one, a backend
67
+ * that carries this member is refused as an unparseable declaration.
68
+ */
69
+ readonly retention?: ManagedRetentionCapability;
70
+ /** Release everything the backend holds (handles, locks) so a successor over the same storage can open
71
+ * it. Call it at shutdown. */
72
+ dispose(): Promise<void>;
73
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -75,6 +75,10 @@ export declare function trackKeyOf(root: string, absPath: string): string;
75
75
  * before; pass it and a foreign-family record is refused by name instead of coerced.
76
76
  */
77
77
  export declare function resolveTrackKey(root: string, key: string, minted?: FileHistoryTrackKeyDescriptor): FileHistoryTrackKeyResolution;
78
+ /** The file-history failure codes, spelled once: {@link FileHistoryError}'s `code` is derived from this tuple (each
79
+ * word is documented there). A host that recognizes a code in failure text, or keeps a code in a store, reads this
80
+ * value rather than keeping its own list. */
81
+ export declare const FILE_HISTORY_ERROR_CODES: readonly ["track_failed", "snapshot_failed", "restore_failed", "adopt_failed", "not_found", "aborted", "unsupported_format", "conflict"];
78
82
  export interface FileHistoryError {
79
83
  /** `not_found` = no boundary for that (scope, entryId); `aborted` = the signal fired (or the
80
84
  * fencing discarded a timed-out attempt); `conflict` = a non-destructive-target law refused
@@ -89,7 +93,7 @@ export interface FileHistoryError {
89
93
  * work its shape drove; a fault in the PUBLISH step itself — the disk refusing a good graph —
90
94
  * answers `adopt_failed`); `unsupported_format`
91
95
  * = a foreign-epoch or wrong-version envelope; the rest name the verb that failed. */
92
- code: "track_failed" | "snapshot_failed" | "restore_failed" | "adopt_failed" | "not_found" | "aborted" | "unsupported_format" | "conflict";
96
+ code: (typeof FILE_HISTORY_ERROR_CODES)[number];
93
97
  message: string;
94
98
  }
95
99
  export type FileHistoryResult = {
@@ -291,7 +295,7 @@ export interface FileHistoryStore {
291
295
  * discarding that baseline would be the fake-pristine hole DV-14 exists to weld shut.
292
296
  *
293
297
  * `verify` is the EVIDENCE arm (adversarial round 2). A caller whose write failed with a code the
294
- * FileSystem contract defines as "nothing was written" (`already_exists`, `precondition_failed`)
298
+ * FileSystem contract defines as "nothing was written" (`already_exists`, `precondition_failed`, `target_unavailable`)
295
299
  * has proof and passes nothing. Every other failure — a plain write is not contractually atomic,
296
300
  * so ENOSPC, a dying transport or a thrown backend may have TRUNCATED the file before reporting —
297
301
  * must hand an `env` here: the store then re-reads the path ONCE and retracts only if the world
@@ -66,6 +66,7 @@ export function resolveTrackKey(root, key, minted) {
66
66
  return { ok: false, reason: `unsafe recorded key escapes root: ${key}` };
67
67
  return { ok: true, abs: joinUnder(root, keyN) };
68
68
  }
69
+ export const FILE_HISTORY_ERROR_CODES = Object.freeze(["track_failed", "snapshot_failed", "restore_failed", "adopt_failed", "not_found", "aborted", "unsupported_format", "conflict"]);
69
70
  export const FILE_HISTORY_DIFF_LINE_BUDGET = 2000;
70
71
  export function countRestoreLineDiff(current, target) {
71
72
  const isBinary = (b) => b !== undefined && b.includes(0);
@@ -165,8 +165,16 @@ export async function runGateFold(pass) {
165
165
  pass.deniedBy = "write_protection";
166
166
  }
167
167
  }
168
- if (asksUnstated(pass.decision, permissionSeatAt(input, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput)?.seat, input.permissionMode?.())) {
169
- pass.decision = { action: "ask", message: permissionSeatAt(input, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput)?.question ?? unstatedAskMessage(toolName), ...(pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {}) };
168
+ if (pass.decision.action !== "deny" && input.crossBoundaryCheck?.obligation({ toolName, args: pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput, toolCallId }).mandate === true) {
169
+ const marks = { requiresRealApproval: true, crossBoundaryMandated: true };
170
+ if (pass.decision.action === "ask")
171
+ pass.decision = { ...pass.decision, ...marks };
172
+ else
173
+ [pass.decision, pass.tightenedBy] = [{ action: "ask", message: `tool "${toolName}" crosses the device boundary of a run that executes on a target device — a person must confirm it`, decisionReason: "safety", ...marks }, "safety"];
174
+ }
175
+ const unstatedRuns = pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput;
176
+ if (asksUnstated(pass.decision, pass.req, unstatedRuns, permissionSeatAt(input, unstatedRuns)?.seat, input.permissionMode?.())) {
177
+ pass.decision = { action: "ask", message: permissionSeatAt(input, unstatedRuns)?.question ?? unstatedAskMessage(toolName), ...(pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {}) };
170
178
  pass.tightenedBy = "unstated";
171
179
  }
172
180
  return await pass.afterFold(pass);