blun-king-cli 9.1.567 → 9.1.569

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 (262) hide show
  1. package/agent-spine-plugin/.claude-plugin/marketplace.json +1 -1
  2. package/agent-spine-plugin/.claude-plugin/plugin.json +1 -1
  3. package/agent-spine-plugin/.codex-plugin/plugin.json +2 -1
  4. package/agent-spine-plugin/CHANGELOG.md +1581 -0
  5. package/agent-spine-plugin/README.md +30 -4
  6. package/agent-spine-plugin/blun.plugin.json +3 -3
  7. package/agent-spine-plugin/docs/acceptance.md +61 -0
  8. package/agent-spine-plugin/docs/assignment-continuation.md +48 -0
  9. package/agent-spine-plugin/docs/host-integration.md +178 -0
  10. package/agent-spine-plugin/docs/preflight-recall.md +69 -0
  11. package/agent-spine-plugin/docs/preservation-contract.md +53 -0
  12. package/agent-spine-plugin/docs/quality-gates.md +50 -0
  13. package/agent-spine-plugin/docs/releasing.md +85 -0
  14. package/agent-spine-plugin/docs/session-timeline.md +251 -0
  15. package/agent-spine-plugin/docs/source-roots.md +113 -0
  16. package/agent-spine-plugin/docs/structured-completion.md +67 -0
  17. package/agent-spine-plugin/docs/world-model.md +94 -0
  18. package/agent-spine-plugin/hooks/codex.json +2 -2
  19. package/agent-spine-plugin/hooks/hooks.json +1 -1
  20. package/agent-spine-plugin/hooks/version.json +1 -1
  21. package/agent-spine-plugin/package.json +13 -3
  22. package/agent-spine-plugin/scripts/check-codex-install.js +226 -0
  23. package/agent-spine-plugin/scripts/check-hosts.js +14 -5
  24. package/agent-spine-plugin/scripts/check-install-hook.js +226 -0
  25. package/agent-spine-plugin/scripts/check-install-selfstarter.js +154 -0
  26. package/agent-spine-plugin/scripts/check-install.js +478 -0
  27. package/agent-spine-plugin/scripts/check-line-budget.js +58 -0
  28. package/agent-spine-plugin/scripts/check-syntax.js +29 -0
  29. package/agent-spine-plugin/scripts/github-actions.js +11 -0
  30. package/agent-spine-plugin/scripts/hermetic-process.js +183 -0
  31. package/agent-spine-plugin/scripts/release-check.js +145 -0
  32. package/agent-spine-plugin/scripts/run-acceptance.js +19 -0
  33. package/agent-spine-plugin/scripts/run-checks.js +47 -0
  34. package/agent-spine-plugin/scripts/run-tests-hermetic.js +89 -0
  35. package/agent-spine-plugin/skills/agent-spine/SKILL.md +16 -2
  36. package/agent-spine-plugin/spine-example/1-identity.md +12 -0
  37. package/agent-spine-plugin/spine-example/2-voice.md +6 -0
  38. package/agent-spine-plugin/spine-example/3-conduct.md +8 -0
  39. package/agent-spine-plugin/spine-example/4-history.md +4 -0
  40. package/agent-spine-plugin/src/cli-agent.js +296 -0
  41. package/agent-spine-plugin/src/cli-attention.js +95 -0
  42. package/agent-spine-plugin/src/cli-autonomy.js +36 -0
  43. package/agent-spine-plugin/src/cli-common.js +71 -0
  44. package/agent-spine-plugin/src/cli-continuity.js +116 -0
  45. package/agent-spine-plugin/src/cli-core.js +128 -0
  46. package/agent-spine-plugin/src/cli-diagnostics.js +291 -0
  47. package/agent-spine-plugin/src/cli-host.js +21 -0
  48. package/agent-spine-plugin/src/cli-learning.js +305 -0
  49. package/agent-spine-plugin/src/cli-premortem.js +16 -0
  50. package/agent-spine-plugin/src/cli-sharing.js +230 -0
  51. package/agent-spine-plugin/src/cli.js +40 -1350
  52. package/agent-spine-plugin/src/codex-reader-launcher.js +182 -0
  53. package/agent-spine-plugin/src/hook.js +185 -567
  54. package/agent-spine-plugin/src/index.js +16 -2
  55. package/agent-spine-plugin/src/lib/acceptance.js +113 -11
  56. package/agent-spine-plugin/src/lib/action-lesson-recall.js +53 -0
  57. package/agent-spine-plugin/src/lib/attention-context.js +167 -0
  58. package/agent-spine-plugin/src/lib/attention-events.js +113 -0
  59. package/agent-spine-plugin/src/lib/attention-privacy.js +72 -0
  60. package/agent-spine-plugin/src/lib/attention-schema.js +164 -0
  61. package/agent-spine-plugin/src/lib/attention-storage.js +93 -0
  62. package/agent-spine-plugin/src/lib/attention.js +9 -600
  63. package/agent-spine-plugin/src/lib/audit-premortem.js +223 -0
  64. package/agent-spine-plugin/src/lib/audit.js +56 -6
  65. package/agent-spine-plugin/src/lib/autonomy-policy.js +110 -0
  66. package/agent-spine-plugin/src/lib/autonomy-store.js +202 -0
  67. package/agent-spine-plugin/src/lib/autonomy.js +8 -0
  68. package/agent-spine-plugin/src/lib/briefing.js +67 -4
  69. package/agent-spine-plugin/src/lib/catalog-document-read.js +51 -0
  70. package/agent-spine-plugin/src/lib/catalog.js +1 -1
  71. package/agent-spine-plugin/src/lib/codex-installation.js +231 -0
  72. package/agent-spine-plugin/src/lib/codex-skill-installation.js +211 -0
  73. package/agent-spine-plugin/src/lib/context.js +3 -1
  74. package/agent-spine-plugin/src/lib/delivery-agent-usage.js +224 -0
  75. package/agent-spine-plugin/src/lib/delivery-assignment.js +220 -0
  76. package/agent-spine-plugin/src/lib/delivery-command-actions.js +453 -0
  77. package/agent-spine-plugin/src/lib/delivery-knowledge.js +78 -0
  78. package/agent-spine-plugin/src/lib/delivery-premortem-binding.js +107 -0
  79. package/agent-spine-plugin/src/lib/delivery-premortem-closure.js +171 -0
  80. package/agent-spine-plugin/src/lib/delivery-premortem-codec.js +45 -0
  81. package/agent-spine-plugin/src/lib/delivery-premortem-correction.js +101 -0
  82. package/agent-spine-plugin/src/lib/delivery-premortem-file.js +21 -0
  83. package/agent-spine-plugin/src/lib/delivery-premortem-index.js +493 -0
  84. package/agent-spine-plugin/src/lib/delivery-premortem-inspection.js +37 -0
  85. package/agent-spine-plugin/src/lib/delivery-premortem-recovery.js +120 -0
  86. package/agent-spine-plugin/src/lib/delivery-premortem-rejection.js +65 -0
  87. package/agent-spine-plugin/src/lib/delivery-premortem-results.js +28 -0
  88. package/agent-spine-plugin/src/lib/delivery-premortem-session-guard.js +46 -0
  89. package/agent-spine-plugin/src/lib/delivery-premortem-write-ledger.js +285 -0
  90. package/agent-spine-plugin/src/lib/delivery-premortem.js +499 -0
  91. package/agent-spine-plugin/src/lib/delivery-shell-heredoc.js +115 -0
  92. package/agent-spine-plugin/src/lib/delivery-shell-substitutions.js +111 -0
  93. package/agent-spine-plugin/src/lib/delivery-shell-wrapper.js +64 -0
  94. package/agent-spine-plugin/src/lib/delivery-target.js +73 -0
  95. package/agent-spine-plugin/src/lib/delivery-verification.js +445 -0
  96. package/agent-spine-plugin/src/lib/documents.js +27 -5
  97. package/agent-spine-plugin/src/lib/filesystem-retry.js +2 -0
  98. package/agent-spine-plugin/src/lib/gateway-common.js +68 -0
  99. package/agent-spine-plugin/src/lib/gateway-control.js +302 -0
  100. package/agent-spine-plugin/src/lib/gateway-delivery.js +103 -0
  101. package/agent-spine-plugin/src/lib/gateway-execution.js +350 -0
  102. package/agent-spine-plugin/src/lib/gateway-host-lifecycle.js +185 -0
  103. package/agent-spine-plugin/src/lib/gateway-inspection.js +85 -0
  104. package/agent-spine-plugin/src/lib/gateway-knowledge.js +129 -0
  105. package/agent-spine-plugin/src/lib/gateway-policy-provenance.js +197 -0
  106. package/agent-spine-plugin/src/lib/gateway-premortem-disposition.js +82 -0
  107. package/agent-spine-plugin/src/lib/gateway-premortem.js +363 -0
  108. package/agent-spine-plugin/src/lib/gateway-runs.js +300 -0
  109. package/agent-spine-plugin/src/lib/gateway-runtime-identity.js +31 -0
  110. package/agent-spine-plugin/src/lib/gateway-runtime-records.js +21 -0
  111. package/agent-spine-plugin/src/lib/gateway-runtime.js +18 -1623
  112. package/agent-spine-plugin/src/lib/gateway-state-transaction.js +356 -0
  113. package/agent-spine-plugin/src/lib/gateway-state.js +342 -0
  114. package/agent-spine-plugin/src/lib/hook-artifact-guards.js +356 -0
  115. package/agent-spine-plugin/src/lib/hook-audit.js +16 -2
  116. package/agent-spine-plugin/src/lib/hook-briefing-use.js +68 -0
  117. package/agent-spine-plugin/src/lib/hook-context.js +424 -0
  118. package/agent-spine-plugin/src/lib/hook-final-message.js +26 -0
  119. package/agent-spine-plugin/src/lib/hook-input.js +27 -0
  120. package/agent-spine-plugin/src/lib/hook-output.js +140 -0
  121. package/agent-spine-plugin/src/lib/hook-premortem.js +257 -0
  122. package/agent-spine-plugin/src/lib/hook-process-advisory.js +37 -0
  123. package/agent-spine-plugin/src/lib/hook-protection.js +95 -0
  124. package/agent-spine-plugin/src/lib/hook-stop-verification.js +84 -0
  125. package/agent-spine-plugin/src/lib/hook-timeline.js +91 -0
  126. package/agent-spine-plugin/src/lib/identifier-analysis.js +446 -0
  127. package/agent-spine-plugin/src/lib/indexed-memory.js +23 -6
  128. package/agent-spine-plugin/src/lib/knowledge-evidence.js +431 -0
  129. package/agent-spine-plugin/src/lib/learning-applications.js +441 -0
  130. package/agent-spine-plugin/src/lib/learning-candidates.js +274 -0
  131. package/agent-spine-plugin/src/lib/learning-context.js +130 -0
  132. package/agent-spine-plugin/src/lib/learning-delivery-contracts.js +310 -0
  133. package/agent-spine-plugin/src/lib/learning-evaluation-contracts.js +350 -0
  134. package/agent-spine-plugin/src/lib/learning-evaluation-registration.js +372 -0
  135. package/agent-spine-plugin/src/lib/learning-evaluation-revocation.js +292 -0
  136. package/agent-spine-plugin/src/lib/learning-evidence-contracts.js +321 -0
  137. package/agent-spine-plugin/src/lib/learning-findings.js +458 -0
  138. package/agent-spine-plugin/src/lib/learning-measurement-contracts.js +285 -0
  139. package/agent-spine-plugin/src/lib/learning-measurements.js +265 -0
  140. package/agent-spine-plugin/src/lib/learning-outcome-contracts.js +165 -0
  141. package/agent-spine-plugin/src/lib/learning-outcomes.js +343 -0
  142. package/agent-spine-plugin/src/lib/learning-reconciliation.js +290 -0
  143. package/agent-spine-plugin/src/lib/learning-retry-contracts.js +188 -0
  144. package/agent-spine-plugin/src/lib/learning-schema.js +229 -0
  145. package/agent-spine-plugin/src/lib/learning-scope-targets.js +424 -0
  146. package/agent-spine-plugin/src/lib/learning-state-upgrade.js +477 -0
  147. package/agent-spine-plugin/src/lib/learning-status-configuration.js +476 -0
  148. package/agent-spine-plugin/src/lib/learning-storage.js +203 -0
  149. package/agent-spine-plugin/src/lib/learning-trial-recovery.js +218 -0
  150. package/agent-spine-plugin/src/lib/learning-validation-contracts.js +375 -0
  151. package/agent-spine-plugin/src/lib/learning-validation-renewal.js +298 -0
  152. package/agent-spine-plugin/src/lib/learning-validation-runtime.js +234 -0
  153. package/agent-spine-plugin/src/lib/learning.js +36 -6923
  154. package/agent-spine-plugin/src/lib/lesson-recall-session.js +172 -0
  155. package/agent-spine-plugin/src/lib/mcp-autonomy-tools.js +37 -0
  156. package/agent-spine-plugin/src/lib/mcp-delivery-completion.js +124 -0
  157. package/agent-spine-plugin/src/lib/mcp-delivery-tools.js +45 -0
  158. package/agent-spine-plugin/src/lib/mcp-premortem.js +68 -0
  159. package/agent-spine-plugin/src/lib/mcp-runtime.js +269 -0
  160. package/agent-spine-plugin/src/lib/mcp-source-context.js +51 -0
  161. package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +164 -0
  162. package/agent-spine-plugin/src/lib/mcp-world-tools.js +52 -0
  163. package/agent-spine-plugin/src/lib/owned-file-lock.js +30 -10
  164. package/agent-spine-plugin/src/lib/project-portfolio.js +176 -0
  165. package/agent-spine-plugin/src/lib/selfstarter-core.js +382 -0
  166. package/agent-spine-plugin/src/lib/selfstarter-jobs.js +149 -0
  167. package/agent-spine-plugin/src/lib/selfstarter-lease.js +204 -0
  168. package/agent-spine-plugin/src/lib/selfstarter-policy.js +90 -0
  169. package/agent-spine-plugin/src/lib/selfstarter-workspace.js +111 -0
  170. package/agent-spine-plugin/src/lib/selfstarter.js +11 -911
  171. package/agent-spine-plugin/src/lib/session-timeline-auth.js +316 -0
  172. package/agent-spine-plugin/src/lib/session-timeline-codex.js +58 -0
  173. package/agent-spine-plugin/src/lib/session-timeline-contract.js +48 -0
  174. package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +41 -0
  175. package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +132 -0
  176. package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +17 -0
  177. package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +500 -0
  178. package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +157 -0
  179. package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +74 -0
  180. package/agent-spine-plugin/src/lib/session-timeline-host-receipt.js +117 -0
  181. package/agent-spine-plugin/src/lib/session-timeline-invocation.js +201 -0
  182. package/agent-spine-plugin/src/lib/session-timeline-king.js +79 -0
  183. package/agent-spine-plugin/src/lib/session-timeline-prior.js +59 -0
  184. package/agent-spine-plugin/src/lib/session-timeline-provider.js +34 -0
  185. package/agent-spine-plugin/src/lib/session-timeline-query.js +55 -0
  186. package/agent-spine-plugin/src/lib/session-timeline-results.js +31 -0
  187. package/agent-spine-plugin/src/lib/session-timeline-root.js +11 -0
  188. package/agent-spine-plugin/src/lib/session-timeline-search.js +82 -0
  189. package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +217 -0
  190. package/agent-spine-plugin/src/lib/session-timeline-source.js +83 -0
  191. package/agent-spine-plugin/src/lib/session-timeline-state.js +45 -0
  192. package/agent-spine-plugin/src/lib/session-timeline-transport.js +50 -0
  193. package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +148 -0
  194. package/agent-spine-plugin/src/lib/session-timeline.js +453 -0
  195. package/agent-spine-plugin/src/lib/source-roots.js +69 -136
  196. package/agent-spine-plugin/src/lib/source-tree-scan.js +178 -0
  197. package/agent-spine-plugin/src/lib/task-knowledge-context.js +78 -0
  198. package/agent-spine-plugin/src/lib/timeline-tool-guard.js +202 -0
  199. package/agent-spine-plugin/src/lib/world-knowledge.js +249 -0
  200. package/agent-spine-plugin/src/lib/world-model.js +278 -0
  201. package/agent-spine-plugin/src/mcp.js +20 -161
  202. package/agent-spine-plugin/src/version.js +1 -1
  203. package/agent-spine-plugin/src/worker.js +22 -5
  204. package/bin/agent-resume-snapshot.cjs +2 -2
  205. package/bin/agentspine-king-goal-inbox.mjs +111 -0
  206. package/bin/agentspine-king-goal-intake.mjs +106 -0
  207. package/bin/baseline-skill-performance-policy.cjs +1 -16
  208. package/bin/core-bootstrap.js +2 -0
  209. package/bin/curiosity-scout-policy.cjs +5 -1
  210. package/bin/input-draft-persistence.cjs +2 -2
  211. package/bin/king-tui-function-contract.json +33 -0
  212. package/bin/launcher-restart-policy.cjs +150 -0
  213. package/bin/launcher-runtime.js +56 -44
  214. package/bin/managed-context-startup-policy.cjs +27 -0
  215. package/bin/managed-plugin-selection.cjs +116 -0
  216. package/bin/mistake-relevance-policy.cjs +1 -1
  217. package/bin/observer-hooks.cjs +14 -0
  218. package/bin/oversized-context-offload-policy.cjs +86 -0
  219. package/bin/plugin-bootstrap.js +7 -40
  220. package/bin/proactive-compaction-policy.cjs +1 -1
  221. package/bin/provider-model-refresh-deadline.cjs +53 -0
  222. package/bin/provider-model-refresh-policy.cjs +107 -0
  223. package/bin/release-artifact-freeze-policy.cjs +30 -0
  224. package/bin/repeated-user-message-projection.cjs +3 -126
  225. package/bin/research-page-result.cjs +74 -0
  226. package/bin/runtime-exit-ledger.cjs +1 -0
  227. package/bin/session-compaction-policy.cjs +84 -0
  228. package/bin/skill-listing-performance-policy.cjs +2 -2
  229. package/bin/standard-tools-bootstrap.js +0 -37
  230. package/bin/subagent-skill-policy.cjs +3 -1
  231. package/bin/telegram-approval-relay.cjs +12 -7
  232. package/bin/telegram-private-conversation-policy.cjs +3 -2
  233. package/bin/telegram-queue-handoff-policy.cjs +24 -0
  234. package/bin/thinking-activity-status-policy.cjs +1 -1
  235. package/bin/thinking-only-guard.cjs +16 -12
  236. package/bin/tool-call-loop-policy.cjs +0 -2
  237. package/bin/tool-result-offload-policy.cjs +11 -2
  238. package/bin/tui-functional-contract.cjs +55 -0
  239. package/bin/update-notice.js +18 -14
  240. package/bin/user-message-offload-policy.cjs +1 -1
  241. package/bin/user-prompt-hook-origin-policy.cjs +34 -0
  242. package/bin/windows-node-crash-dump.cjs +110 -0
  243. package/blun.mjs +2517 -1048
  244. package/codebase-index/codebase_index.py +4 -3
  245. package/package.json +3 -17
  246. package/standard-skills/research-evidence/SKILL.md +39 -0
  247. package/standard-skills/research-evidence/references/evidence-format.md +82 -0
  248. package/standard-skills/research-evidence/scripts/evidence-collection.cjs +254 -0
  249. package/standard-skills/research-evidence/scripts/score-report.cjs +112 -0
  250. package/standard-skills/web-lesen/SKILL.md +37 -22
  251. package/standard-skills/web-lesen/scripts/crawl_public.py +376 -0
  252. package/telegram-plugin/DELIVERY.md +36 -0
  253. package/telegram-plugin/bin/telegram-approval-relay.cjs +13 -7
  254. package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +122 -0
  255. package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +3 -2
  256. package/telegram-plugin/bin/telegram-reply-parts.cjs +149 -0
  257. package/telegram-plugin/dist/bridge.mjs +7 -56
  258. package/telegram-plugin/dist/mcp-server.mjs +33 -4
  259. package/agent-spine-plugin/skill/SKILL.md +0 -76
  260. package/bin/mnemo-connect-heartbeat.cjs +0 -204
  261. package/bin/mnemo-tool-agent-policy.cjs +0 -22
  262. package/telegram-plugin/bin/telegram-mnemo-capture.cjs +0 -297
@@ -0,0 +1,316 @@
1
+ import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import { lstat, mkdir, open, unlink, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { replaceFileWithRetry } from "./filesystem-retry.js";
6
+ import { stateRoot } from "./paths.js";
7
+ import { sessionTimelineRootDigest } from "./session-timeline-root.js";
8
+ import { verifyWindowsTimelineDirectoryAcl, verifyWindowsTimelineFileAcl } from "./session-timeline-windows-acl.js";
9
+
10
+ const KEY_BYTES = 32;
11
+ const SIGNATURE_RE = /^[a-f0-9]{64}$/;
12
+ const HEAD_SCHEMA = "agentspine.session-timeline-head/v1";
13
+ const HEAD_MAX_BYTES = 64 * 1024;
14
+
15
+ function canonical(value) {
16
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
17
+ if (value && typeof value === "object") {
18
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
19
+ }
20
+ return JSON.stringify(value);
21
+ }
22
+
23
+ function payload(state) {
24
+ const { signature: _signature, ...unsigned } = state;
25
+ return canonical(unsigned);
26
+ }
27
+
28
+ function privateMode(metadata) {
29
+ return process.platform !== "win32" && (metadata.mode & 0o077n) !== 0n;
30
+ }
31
+
32
+ function unsafeDirectory(metadata) {
33
+ return !metadata.isDirectory() || metadata.isSymbolicLink()
34
+ || (process.platform !== "win32" && (metadata.mode & 0o022n) !== 0n);
35
+ }
36
+
37
+ function ownerMatches(metadata) {
38
+ return typeof process.getuid !== "function" || metadata.uid === BigInt(process.getuid());
39
+ }
40
+
41
+ async function stateDirectory(create) {
42
+ const path = stateRoot();
43
+ if (create) await mkdir(path, { recursive: true, mode: 0o700 });
44
+ const metadata = await lstat(path, { bigint: true });
45
+ if (unsafeDirectory(metadata)) throw new Error("session timeline state root is invalid");
46
+ await verifyWindowsTimelineDirectoryAcl(path, { metadata, role: "parent" });
47
+ return { path, metadata };
48
+ }
49
+
50
+ async function integrityDirectory(create) {
51
+ const root = await stateDirectory(create);
52
+ const path = join(root.path, "integrity");
53
+ let created = false;
54
+ if (create) {
55
+ try { await mkdir(path, { mode: 0o700 }); created = true; }
56
+ catch (error) { if (error.code !== "EEXIST") throw error; }
57
+ }
58
+ let metadata = await lstat(path, { bigint: true });
59
+ await verifyWindowsTimelineDirectoryAcl(path, { created, metadata, role: "integrity" });
60
+ metadata = await lstat(path, { bigint: true });
61
+ if (unsafeDirectory(metadata) || privateMode(metadata) || !ownerMatches(metadata)) {
62
+ throw new Error("session timeline integrity directory is invalid");
63
+ }
64
+ return { path, metadata, parent: root };
65
+ }
66
+
67
+ function sameDirectory(left, right) {
68
+ return left.isDirectory() && right.isDirectory() && !left.isSymbolicLink() && !right.isSymbolicLink()
69
+ && left.dev === right.dev && left.ino === right.ino;
70
+ }
71
+
72
+ async function assertStateDirectory(anchor) {
73
+ const metadata = await lstat(anchor.path, { bigint: true });
74
+ if (unsafeDirectory(metadata) || !sameDirectory(anchor.metadata, metadata)) {
75
+ throw new Error("session timeline state root changed during operation");
76
+ }
77
+ await verifyWindowsTimelineDirectoryAcl(anchor.path, { metadata, role: "parent" });
78
+ return metadata;
79
+ }
80
+
81
+ async function assertIntegrityDirectory(anchor) {
82
+ await assertStateDirectory(anchor.parent);
83
+ const metadata = await lstat(anchor.path, { bigint: true });
84
+ if (unsafeDirectory(metadata) || privateMode(metadata) || !ownerMatches(metadata) || !sameDirectory(anchor.metadata, metadata)) {
85
+ throw new Error("session timeline integrity directory changed during operation");
86
+ }
87
+ await verifyWindowsTimelineDirectoryAcl(anchor.path, { metadata, role: "integrity" });
88
+ return metadata;
89
+ }
90
+
91
+ function keyPath(integrity) {
92
+ return join(integrity.path, "session-timeline-signing.key");
93
+ }
94
+
95
+ function safePrivateFile(metadata, maximumBytes = null) {
96
+ return metadata.isFile() && !metadata.isSymbolicLink() && metadata.nlink === 1n
97
+ && (maximumBytes === null || metadata.size <= BigInt(maximumBytes))
98
+ && !privateMode(metadata) && ownerMatches(metadata);
99
+ }
100
+
101
+ async function verifyPrivateTimelineFile(path, metadata, role = "private", { created = false } = {}) {
102
+ if (!safePrivateFile(metadata)) throw new Error("session timeline private file is invalid");
103
+ await verifyWindowsTimelineFileAcl(path, { created, metadata, role });
104
+ }
105
+
106
+ async function verifyExistingPrivateTimelineFile(path, role = "private", maximumBytes = null) {
107
+ try {
108
+ const metadata = await lstat(path, { bigint: true });
109
+ if (!safePrivateFile(metadata, maximumBytes)) throw new Error("session timeline private file is invalid");
110
+ await verifyWindowsTimelineFileAcl(path, { metadata, role });
111
+ return metadata;
112
+ } catch (error) {
113
+ if (error.code === "ENOENT") return null;
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ async function existingKey(path, integrity) {
119
+ await assertIntegrityDirectory(integrity);
120
+ const metadata = await lstat(path, { bigint: true });
121
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1n || metadata.size !== BigInt(KEY_BYTES)
122
+ || privateMode(metadata) || !ownerMatches(metadata)) {
123
+ throw new Error("session timeline signing key is invalid");
124
+ }
125
+ await verifyWindowsTimelineFileAcl(path, { metadata, role: "key" });
126
+ const handle = await open(path, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0));
127
+ try {
128
+ const before = await handle.stat({ bigint: true });
129
+ if (!sameFile(metadata, before)) throw new Error("session timeline signing key is invalid");
130
+ await verifyWindowsTimelineFileAcl(path, { metadata: before, role: "key" });
131
+ const value = await handle.readFile();
132
+ const after = await handle.stat({ bigint: true });
133
+ if (!sameFile(before, after) || after.nlink !== 1n || value.byteLength !== KEY_BYTES) {
134
+ throw new Error("session timeline signing key is invalid");
135
+ }
136
+ await verifyWindowsTimelineFileAcl(path, { metadata: after, role: "key" });
137
+ await assertIntegrityDirectory(integrity);
138
+ return value;
139
+ } finally { await handle.close(); }
140
+ }
141
+
142
+ async function signingKey({ create }) {
143
+ const integrity = await integrityDirectory(create);
144
+ const path = keyPath(integrity);
145
+ try { return await existingKey(path, integrity); }
146
+ catch (error) {
147
+ if (!create || error.code !== "ENOENT") throw error;
148
+ }
149
+ const value = randomBytes(KEY_BYTES);
150
+ try {
151
+ await assertIntegrityDirectory(integrity);
152
+ const handle = await open(path, "wx", 0o600);
153
+ try {
154
+ await handle.writeFile(value);
155
+ const metadata = await handle.stat({ bigint: true });
156
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1n || metadata.size !== BigInt(KEY_BYTES)
157
+ || privateMode(metadata) || !ownerMatches(metadata)) throw new Error("session timeline signing key is invalid");
158
+ } finally { await handle.close(); }
159
+ const metadata = await lstat(path, { bigint: true });
160
+ await verifyPrivateTimelineFile(path, metadata, "key", { created: true });
161
+ await assertIntegrityDirectory(integrity);
162
+ return existingKey(path, integrity);
163
+ } catch (error) {
164
+ if (error.code !== "EEXIST") throw error;
165
+ return existingKey(path, integrity);
166
+ }
167
+ }
168
+
169
+ export async function ensureSessionTimelineTrust({ create = false } = {}) {
170
+ const integrity = await integrityDirectory(create);
171
+ try { await existingKey(keyPath(integrity), integrity); }
172
+ catch (error) {
173
+ if (error.code !== "ENOENT") throw error;
174
+ if (create) await signingKey({ create: true });
175
+ }
176
+ }
177
+
178
+ function timelineFileRole(namespace) {
179
+ if (namespace === "state") return "state";
180
+ return namespace.endsWith("head") ? "head" : "private";
181
+ }
182
+
183
+ export async function sessionTimelinePrivatePaths(root, namespace, { create = true } = {}) {
184
+ if (!/^[a-z-]{1,32}$/.test(namespace || "")) throw new Error("session timeline private namespace is invalid");
185
+ const integrity = await integrityDirectory(create);
186
+ const identifier = sessionTimelineRootDigest(root);
187
+ const assertStable = () => assertIntegrityDirectory(integrity);
188
+ const path = join(integrity.path, `session-timeline-${namespace}-${identifier}.json`);
189
+ await assertStable();
190
+ await verifyExistingPrivateTimelineFile(path, timelineFileRole(namespace));
191
+ return {
192
+ path,
193
+ lock: join(integrity.path, `session-timeline-${namespace}-${identifier}.lock`),
194
+ assertStable
195
+ };
196
+ }
197
+
198
+ export function sessionTimelineStatePaths(root, options = {}) {
199
+ return sessionTimelinePrivatePaths(root, "state", options);
200
+ }
201
+
202
+ function signature(value, key) {
203
+ return createHmac("sha256", key).update(payload(value)).digest("hex");
204
+ }
205
+
206
+ function digest(value) { return createHash("sha256").update(value).digest("hex"); }
207
+ function sameFile(left, right) {
208
+ return left.isFile() && right.isFile() && !left.isSymbolicLink() && !right.isSymbolicLink()
209
+ && left.dev === right.dev && left.ino === right.ino && left.size === right.size
210
+ && left.nlink === right.nlink && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
211
+ }
212
+
213
+ export async function readAuthenticatedTimelineState(path, maximumBytes, assertStable = null) {
214
+ await assertStable?.();
215
+ const pathname = await lstat(path, { bigint: true });
216
+ if (!pathname.isFile() || pathname.isSymbolicLink() || pathname.nlink !== 1n || pathname.size > BigInt(maximumBytes)) {
217
+ throw new Error("session timeline state is invalid");
218
+ }
219
+ await verifyWindowsTimelineFileAcl(path, { metadata: pathname, role: "private" });
220
+ const handle = await open(path, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0));
221
+ try {
222
+ const before = await handle.stat({ bigint: true });
223
+ if (!sameFile(pathname, before) || before.nlink !== 1n) throw new Error("session timeline state is invalid");
224
+ await verifyWindowsTimelineFileAcl(path, { metadata: before, role: "private" });
225
+ const content = await handle.readFile();
226
+ const after = await handle.stat({ bigint: true });
227
+ if (!sameFile(before, after) || after.nlink !== 1n || content.byteLength !== Number(after.size)) {
228
+ throw new Error("session timeline state changed during read");
229
+ }
230
+ await verifyWindowsTimelineFileAcl(path, { metadata: after, role: "private" });
231
+ await assertStable?.();
232
+ return content.toString("utf8");
233
+ } finally { await handle.close(); }
234
+ }
235
+
236
+ function headPath(integrity, root) {
237
+ return join(integrity.path, `session-timeline-head-${sessionTimelineRootDigest(root)}.json`);
238
+ }
239
+
240
+ async function saveHead(integrity, root, stateSignature, assertOwned = null) {
241
+ const path = headPath(integrity, root);
242
+ await verifyExistingPrivateTimelineFile(path, "head", HEAD_MAX_BYTES);
243
+ const head = { schema: HEAD_SCHEMA, rootDigest: sessionTimelineRootDigest(root), stateSignature, authority: "state-integrity-only" };
244
+ head.signature = signature(head, await signingKey({ create: true }));
245
+ const content = `${JSON.stringify(head)}\n`;
246
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
247
+ await assertIntegrityDirectory(integrity);
248
+ await writeFile(temporary, content, { mode: 0o600 });
249
+ try {
250
+ await verifyPrivateTimelineFile(temporary, await lstat(temporary, { bigint: true }), "head", { created: true });
251
+ await replaceFileWithRetry(temporary, path, { beforeAttempt: async () => {
252
+ await assertIntegrityDirectory(integrity); await assertOwned?.();
253
+ } });
254
+ await verifyPrivateTimelineFile(path, await lstat(path, { bigint: true }), "head");
255
+ await assertIntegrityDirectory(integrity);
256
+ }
257
+ catch (error) {
258
+ await unlink(temporary).catch((cleanup) => { if (cleanup.code !== "ENOENT") error.cleanupError = cleanup; });
259
+ throw error;
260
+ }
261
+ }
262
+
263
+ export async function recordSessionTimelineHead({ root, stateSignature, assertOwned = null }) {
264
+ if (!SIGNATURE_RE.test(stateSignature || "")) throw new Error("session timeline state signature is invalid");
265
+ const integrity = await integrityDirectory(true);
266
+ await saveHead(integrity, root, stateSignature, assertOwned);
267
+ }
268
+
269
+ export async function sessionTimelineHeadExists({ root }) {
270
+ let integrity;
271
+ try { integrity = await integrityDirectory(false); }
272
+ catch (error) {
273
+ if (error.code === "ENOENT") return false;
274
+ throw error;
275
+ }
276
+ try {
277
+ const path = headPath(integrity, root);
278
+ await verifyExistingPrivateTimelineFile(path, "head", HEAD_MAX_BYTES);
279
+ await lstat(path, { bigint: true });
280
+ return true;
281
+ } catch (error) {
282
+ if (error.code === "ENOENT") return false;
283
+ throw error;
284
+ }
285
+ }
286
+
287
+ export async function verifySessionTimelineHead({ root, stateSignature }) {
288
+ let integrity;
289
+ let head;
290
+ try {
291
+ integrity = await integrityDirectory(false);
292
+ head = JSON.parse(await readAuthenticatedTimelineState(headPath(integrity, root), HEAD_MAX_BYTES,
293
+ () => assertIntegrityDirectory(integrity)));
294
+ }
295
+ catch { throw new Error("session timeline state head is unavailable"); }
296
+ if (head?.schema !== HEAD_SCHEMA || head.rootDigest !== sessionTimelineRootDigest(root) || !SIGNATURE_RE.test(head.stateSignature || "")
297
+ || head.authority !== "state-integrity-only") throw new Error("session timeline state head is invalid");
298
+ await verifySessionTimelineState(head);
299
+ await assertIntegrityDirectory(integrity);
300
+ if (head.stateSignature !== stateSignature) throw new Error("session timeline state replay was rejected");
301
+ }
302
+
303
+ export async function sealSessionTimelineState(state) {
304
+ state.signature = signature(state, await signingKey({ create: true }));
305
+ return state;
306
+ }
307
+
308
+ export async function verifySessionTimelineState(state) {
309
+ if (!SIGNATURE_RE.test(state?.signature || "")) throw new Error("session timeline state authentication failed");
310
+ const expected = Buffer.from(signature(state, await signingKey({ create: false })), "hex");
311
+ const actual = Buffer.from(state.signature, "hex");
312
+ if (actual.byteLength !== expected.byteLength || !timingSafeEqual(actual, expected)) {
313
+ throw new Error("session timeline state authentication failed");
314
+ }
315
+ return state;
316
+ }
@@ -0,0 +1,58 @@
1
+ import { constants } from "node:fs";
2
+ import { open, realpath } from "node:fs/promises";
3
+ import { isAbsolute } from "node:path";
4
+ import { matchesSourceMetadata } from "./session-timeline-source.js";
5
+
6
+ export const CODEX_HISTORY_ADAPTER = "codex-rollout-jsonl/v1";
7
+ export const CODEX_HISTORY_CONTRACT_COMMIT = "6af345407d9c2a568da9d01b6c4b81a9e61495c0";
8
+ const HEADER_BYTES = 64 * 1024;
9
+ const ID = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
10
+ const RECORDS = new Set(["session_meta", "response_item", "event_msg", "compacted", "turn_context",
11
+ "token_usage_record", "world_state", "retained_context", "security_risk_score", "realtime_item",
12
+ "inter_agent_communication", "inter_agent_communication_metadata"]);
13
+
14
+ // Read only the explicitly supplied native source. No directory discovery,
15
+ // decompression, inherited-history traversal or source rewriting is allowed.
16
+ export async function validateCodexTimelineHeader({ source, root, sessionId }) {
17
+ let handle;
18
+ try {
19
+ handle = await open(source.path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
20
+ if (!matchesSourceMetadata(await handle.stat({ bigint: true }), source)) return false;
21
+ const bytes = Buffer.alloc(Math.min(source.size, HEADER_BYTES));
22
+ const read = await handle.read(bytes, 0, bytes.length, 0);
23
+ const end = bytes.indexOf(0x0a);
24
+ if (read.bytesRead !== bytes.length || end < 0) return false;
25
+ const line = JSON.parse(bytes.subarray(0, end).toString("utf8"));
26
+ const meta = line?.payload;
27
+ if (line.type !== "session_meta" || !meta || meta.id !== sessionId
28
+ || (meta.session_id !== undefined && meta.session_id !== sessionId)
29
+ || !ID.test(meta.id) || !isAbsolute(meta.cwd || "")
30
+ || typeof meta.cli_version !== "string" || !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(meta.cli_version)
31
+ || (meta.history_mode !== undefined && meta.history_mode !== "legacy")
32
+ || meta.history_base || meta.parent_thread_id || meta.forked_from_id
33
+ || meta.schema_version !== undefined || line.schema_version !== undefined) return false;
34
+ if (await realpath(meta.cwd) !== root) return false;
35
+ return matchesSourceMetadata(await handle.stat({ bigint: true }), source);
36
+ } catch { return false; }
37
+ finally { await handle?.close(); }
38
+ }
39
+
40
+ // Whitelist actual Responses output records, never assistant/user assertions.
41
+ // The caller applies the common secret/injection filter to the original line.
42
+ export function codexTimelineToolResult(line) {
43
+ if (!RECORDS.has(line?.type) || line.schema_version !== undefined) {
44
+ throw new Error("codex-history-format-mismatch");
45
+ }
46
+ if (line.type !== "response_item") return null;
47
+ const item = line.payload;
48
+ if (!item || !["function_call_output", "custom_tool_call_output", "mcp_tool_call_output"].includes(item.type)) return null;
49
+ if (!ID.test(item.call_id || "")) return null;
50
+ let output = item.output;
51
+ if (item.type === "mcp_tool_call_output") output = output?.content;
52
+ if (Array.isArray(output)) {
53
+ if (output.length > 12 || output.some(part => !["text", "input_text"].includes(part?.type)
54
+ || typeof part.text !== "string")) return null;
55
+ output = output.map(part => part.text).join("\n");
56
+ }
57
+ return typeof output === "string" ? { role: "tool", content: output, nativeMessageId: item.call_id } : null;
58
+ }
@@ -0,0 +1,48 @@
1
+ import { validTimelineHost } from "./session-timeline-provider.js";
2
+
3
+ export const TIMELINE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
4
+
5
+ export function safeTimelineId(value) {
6
+ return typeof value === "string" && TIMELINE_ID_RE.test(value) ? value : null;
7
+ }
8
+
9
+ export function sessionTimelineBinding({ host, sessionId, scope }) {
10
+ return {
11
+ host: validTimelineHost(host) ? host : null,
12
+ sessionId: safeTimelineId(sessionId),
13
+ entityId: safeTimelineId(scope?.entityId),
14
+ userId: safeTimelineId(scope?.userId),
15
+ tenantId: safeTimelineId(scope?.tenantId),
16
+ projectId: safeTimelineId(scope?.projectId),
17
+ groupId: safeTimelineId(scope?.groupId),
18
+ taskId: safeTimelineId(scope?.currentTaskId),
19
+ goalId: safeTimelineId(scope?.goalId),
20
+ goalStepId: safeTimelineId(scope?.goalStepId)
21
+ };
22
+ }
23
+
24
+ export function hasTimelineGroupScope(scope) {
25
+ return scope?.groupId !== undefined && scope.groupId !== null && scope.groupId !== "";
26
+ }
27
+
28
+ export function hasVerifiedTimelinePrivateScope(scope) {
29
+ return scope?.timelineVisibility === "private-verified";
30
+ }
31
+
32
+ export function completeTimelineBinding(value) {
33
+ return validTimelineHost(value.host) && !value.groupId
34
+ && ["sessionId", "entityId", "userId", "tenantId", "projectId", "taskId"].every((key) => value[key]);
35
+ }
36
+
37
+ export function sameTimelineBinding(left, right) {
38
+ return ["host", "sessionId", "entityId", "userId", "tenantId", "projectId", "groupId", "taskId", "goalId", "goalStepId"]
39
+ .every((key) => left[key] === right[key]);
40
+ }
41
+
42
+ export function validTimelineBinding(value) {
43
+ return value && validTimelineHost(value.host)
44
+ && [value.sessionId, value.entityId, value.userId, value.tenantId, value.projectId, value.taskId]
45
+ .every((item) => typeof item === "string" && TIMELINE_ID_RE.test(item))
46
+ && [value.groupId, value.goalId, value.goalStepId]
47
+ .every((item) => item === null || (typeof item === "string" && TIMELINE_ID_RE.test(item)));
48
+ }
@@ -0,0 +1,41 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import { open } from "node:fs/promises";
4
+ import { matchesSourceMetadata } from "./session-timeline-source.js";
5
+
6
+ export const PRIVATE_TIMELINE_PREFIX_BYTES = 4096;
7
+
8
+ function digest(value) { return createHash("sha256").update(value).digest("hex"); }
9
+
10
+ // A receipt and enrollment never read a complete host transcript. A small
11
+ // stable prefix makes later replacement detectable while append verification
12
+ // establishes the bounded full commitment separately.
13
+ export async function privateTimelinePrefixDigest(source, bytes = Math.min(source?.size, PRIVATE_TIMELINE_PREFIX_BYTES)) {
14
+ if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > PRIVATE_TIMELINE_PREFIX_BYTES) return null;
15
+ let handle;
16
+ try {
17
+ handle = await open(source.path, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0));
18
+ const before = await handle.stat({ bigint: true });
19
+ if (!matchesSourceMetadata(before, source) || before.size < BigInt(bytes)) return null;
20
+ const buffer = Buffer.alloc(bytes);
21
+ const { bytesRead } = bytes ? await handle.read(buffer, 0, bytes, 0) : { bytesRead: 0 };
22
+ const after = await handle.stat({ bigint: true });
23
+ if (bytesRead !== bytes || !matchesSourceMetadata(after, source) || after.size < BigInt(bytes)) return null;
24
+ return digest(buffer);
25
+ } catch {
26
+ return null;
27
+ } finally {
28
+ await handle?.close();
29
+ }
30
+ }
31
+
32
+ export async function normalizePrivateTimelineSource(source) {
33
+ const prefixBytes = Math.min(source.size, PRIVATE_TIMELINE_PREFIX_BYTES);
34
+ const prefixDigest = await privateTimelinePrefixDigest(source, prefixBytes);
35
+ if (!prefixDigest) return null;
36
+ return {
37
+ path: source.path, profileRoot: source.profileRoot, projectsRoot: source.projectsRoot,
38
+ pathDigest: source.pathDigest, identity: source.identity, size: source.size,
39
+ mtimeNs: source.mtimeNs, ctimeNs: source.ctimeNs, prefixBytes, prefixDigest, commitment: null
40
+ };
41
+ }
@@ -0,0 +1,132 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { unlink, writeFile } from "node:fs/promises";
3
+ import { replaceFileWithRetry } from "./filesystem-retry.js";
4
+ import {
5
+ readAuthenticatedTimelineState, sealSessionTimelineState, sessionTimelinePrivatePaths,
6
+ verifySessionTimelineState
7
+ } from "./session-timeline-auth.js";
8
+
9
+ export async function privateEnrollmentPaths(root, { create = true } = {}) {
10
+ const state = await sessionTimelinePrivatePaths(root, "enrollment", { create });
11
+ const head = await sessionTimelinePrivatePaths(root, "enrollment-head", { create });
12
+ return {
13
+ ...state,
14
+ headPath: head.path,
15
+ assertStable: async () => { await state.assertStable(); await head.assertStable(); }
16
+ };
17
+ }
18
+
19
+ function validHead(value, root, headSchema, headAuthority) {
20
+ return value && value.schema === headSchema && value.rootDigest === root.digest
21
+ && validGeneration(value.generation) && validSignature(value.signature)
22
+ && validSignature(value.stateSignature)
23
+ && (value.generation === 1 ? value.previousSignature === null : validSignature(value.previousSignature))
24
+ && value.authority === headAuthority;
25
+ }
26
+
27
+ function validSignature(value) { return /^[a-f0-9]{64}$/.test(value || ""); }
28
+ function validGeneration(value) { return Number.isSafeInteger(value) && value > 0; }
29
+
30
+ async function readHead(path, root, assertStable, headSchema, headAuthority, maximumBytes) {
31
+ const text = await readAuthenticatedTimelineState(path, maximumBytes, assertStable);
32
+ const head = await verifySessionTimelineState(JSON.parse(text));
33
+ if (!validHead(head, root, headSchema, headAuthority)) throw new Error("private timeline enrollment head is invalid");
34
+ return head;
35
+ }
36
+
37
+ export async function readPrivateEnrollmentState({
38
+ path, headPath, root, assertStable, assertOwned = null, empty, validate, headSchema, headAuthority, maximumBytes
39
+ }) {
40
+ let text;
41
+ try {
42
+ text = await readAuthenticatedTimelineState(path, maximumBytes, assertStable);
43
+ } catch (error) {
44
+ if (error.code !== "ENOENT") throw error;
45
+ try { await readHead(headPath, root, assertStable, headSchema, headAuthority, maximumBytes); }
46
+ catch (headError) {
47
+ if (headError.code === "ENOENT") return empty(root.value);
48
+ throw headError;
49
+ }
50
+ throw new Error("private timeline enrollment state is missing");
51
+ }
52
+ const state = await verifySessionTimelineState(validate(JSON.parse(text), root.value));
53
+ const head = await readHead(headPath, root, assertStable, headSchema, headAuthority, maximumBytes);
54
+ if (matchesHead(state, head)) return state;
55
+ if (typeof assertOwned !== "function" || !recoverableForwardCommit(state, head)) {
56
+ throw new Error("private timeline enrollment state replay was rejected");
57
+ }
58
+ // A state replacement can survive a crash before its paired head replacement.
59
+ // Repair is allowed only while holding the owned lock and only for the exact
60
+ // authenticated one-generation successor of the retained head. Re-read both
61
+ // records after writing so a path swap cannot produce usable content.
62
+ const freshState = await readAuthenticatedTimelineState(path, maximumBytes, assertStable);
63
+ const checkedState = await verifySessionTimelineState(validate(JSON.parse(freshState), root.value));
64
+ const checkedHead = await readHead(headPath, root, assertStable, headSchema, headAuthority, maximumBytes);
65
+ if (!sameState(state, checkedState) || !recoverableForwardCommit(checkedState, checkedHead)) {
66
+ throw new Error("private timeline enrollment state replay was rejected");
67
+ }
68
+ await savePrivateEnrollmentHead({ state: checkedState, path: headPath, root, assertOwned, assertStable,
69
+ headSchema, headAuthority, maximumBytes });
70
+ const finalStateText = await readAuthenticatedTimelineState(path, maximumBytes, assertStable);
71
+ const finalState = await verifySessionTimelineState(validate(JSON.parse(finalStateText), root.value));
72
+ const finalHead = await readHead(headPath, root, assertStable, headSchema, headAuthority, maximumBytes);
73
+ if (!matchesHead(finalState, finalHead)) throw new Error("private timeline enrollment state replay was rejected");
74
+ return finalState;
75
+ }
76
+
77
+ function matchesHead(state, head) {
78
+ return state.generation === head.generation && state.signature === head.stateSignature
79
+ && state.previousSignature === head.previousSignature;
80
+ }
81
+
82
+ function sameState(left, right) {
83
+ return left.generation === right.generation && left.previousSignature === right.previousSignature
84
+ && left.signature === right.signature;
85
+ }
86
+
87
+ function recoverableForwardCommit(state, head) {
88
+ return state.generation === head.generation + 1 && state.previousSignature === head.stateSignature;
89
+ }
90
+
91
+ async function saveSigned(value, path, maximumBytes, label, assertOwned, assertStable) {
92
+ const assertWritable = async () => { await assertStable(); await assertOwned(); };
93
+ await assertWritable();
94
+ await sealSessionTimelineState(value);
95
+ const content = `${JSON.stringify(value)}\n`;
96
+ if (Buffer.byteLength(content) > maximumBytes) throw new Error(`${label} exceeds its byte budget`);
97
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
98
+ await writeFile(temporary, content, { mode: 0o600 });
99
+ try {
100
+ await replaceFileWithRetry(temporary, path, { beforeAttempt: assertWritable });
101
+ await assertWritable();
102
+ } catch (error) {
103
+ await unlink(temporary).catch((cleanup) => { if (cleanup.code !== "ENOENT") error.cleanupError = cleanup; });
104
+ throw error;
105
+ }
106
+ }
107
+
108
+ export async function savePrivateEnrollmentState({
109
+ state, path, headPath, root, assertOwned, assertStable, headSchema, headAuthority, maximumBytes
110
+ }) {
111
+ if (!Number.isSafeInteger(state.generation) || state.generation < 0 || state.generation >= Number.MAX_SAFE_INTEGER
112
+ || (state.generation > 0 && !validSignature(state.signature))) {
113
+ throw new Error("private timeline enrollment generation is invalid");
114
+ }
115
+ state.previousSignature = state.generation === 0 ? null : state.signature;
116
+ state.generation += 1;
117
+ delete state.signature;
118
+ await saveSigned(state, path, maximumBytes, "private timeline enrollment state", assertOwned, assertStable);
119
+ await savePrivateEnrollmentHead({ state, path: headPath, root, assertOwned, assertStable,
120
+ headSchema, headAuthority, maximumBytes });
121
+ }
122
+
123
+ export async function savePrivateEnrollmentHead({
124
+ state, path, root, assertOwned, assertStable, headSchema, headAuthority, maximumBytes
125
+ }) {
126
+ const head = {
127
+ schema: headSchema, rootDigest: root.digest, generation: state.generation,
128
+ stateSignature: state.signature, previousSignature: state.previousSignature,
129
+ authority: headAuthority
130
+ };
131
+ await saveSigned(head, path, maximumBytes, "private timeline enrollment head", assertOwned, assertStable);
132
+ }
@@ -0,0 +1,17 @@
1
+ import { sameTimelineBinding } from "./session-timeline-contract.js";
2
+ import { resolvePrivateSessionTimelineEnrollment } from "./session-timeline-enrollment.js";
3
+ import { validTimelineTransportDigest } from "./session-timeline-transport.js";
4
+
5
+ // Keep the digest inside the signed enrollment record. This verifier returns
6
+ // only a boolean so neither the transport value nor its digest reaches cards.
7
+ export async function timelineTransportEnrollmentMatches({
8
+ root, binding, enrollmentDigest, transportDigest, hostHome = null
9
+ }) {
10
+ if (!validTimelineTransportDigest(transportDigest) || !/^[a-f0-9]{64}$/.test(enrollmentDigest || "")) return false;
11
+ const enrollment = await resolvePrivateSessionTimelineEnrollment({
12
+ root, host: binding?.host, sessionId: binding?.sessionId, hostHome,
13
+ expectedTransportDigest: transportDigest
14
+ });
15
+ return enrollment.status === "enrolled" && enrollment.enrollmentDigest === enrollmentDigest
16
+ && sameTimelineBinding(enrollment.binding, binding);
17
+ }