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,11 @@
1
+ import { createHash } from "node:crypto";
2
+ import { comparablePath } from "./paths.js";
3
+
4
+ export function sessionTimelineRootPath(root) {
5
+ if (typeof root !== "string" || !root) throw new Error("session timeline root is invalid");
6
+ return comparablePath(root);
7
+ }
8
+
9
+ export function sessionTimelineRootDigest(root) {
10
+ return createHash("sha256").update(sessionTimelineRootPath(root)).digest("hex");
11
+ }
@@ -0,0 +1,82 @@
1
+ function parseEvents(buffer, start, eventFromLine) {
2
+ const events = [];
3
+ let cursor = 0;
4
+ while (cursor < buffer.byteLength) {
5
+ const newline = buffer.indexOf(0x0a, cursor);
6
+ const end = newline < 0 ? buffer.byteLength : newline + 1;
7
+ if (end > cursor) {
8
+ const event = eventFromLine(buffer.subarray(cursor, end).toString("utf8"), start + cursor);
9
+ if (event) events.push(event);
10
+ }
11
+ cursor = end;
12
+ }
13
+ return events;
14
+ }
15
+
16
+ async function timestampLineAt({ handle, position, size, last, readRange, extractTimestamp }) {
17
+ const start = Math.max(0, Math.min(position, Math.max(0, size - 1)));
18
+ const bytes = await readRange(handle, start, Math.min(128 * 1024, size - start));
19
+ let cursor = start === 0 ? 0 : bytes.indexOf(0x0a) + 1;
20
+ if (cursor >= bytes.byteLength || (start > 0 && cursor <= 0)) return null;
21
+ let result = null;
22
+ while (cursor < bytes.byteLength) {
23
+ const ending = bytes.indexOf(0x0a, cursor);
24
+ const end = ending < 0 ? bytes.byteLength : ending + 1;
25
+ if (end <= cursor) return null;
26
+ try {
27
+ const at = extractTimestamp(JSON.parse(bytes.subarray(cursor, end).toString("utf8")));
28
+ if (at) {
29
+ result = { at, offset: start + cursor };
30
+ if (!last) return result;
31
+ }
32
+ } catch {}
33
+ cursor = end;
34
+ }
35
+ return result;
36
+ }
37
+
38
+ async function seekTimestamp({ handle, size, target, readRange, extractTimestamp }) {
39
+ const lower = await timestampLineAt({ handle, position: 0, size, last: false, readRange, extractTimestamp });
40
+ const upper = await timestampLineAt({ handle, position: Math.max(0, size - 128 * 1024), size, last: true, readRange, extractTimestamp });
41
+ if (!lower || !upper) return { status: "unavailable", reason: "timeline-not-seekable" };
42
+ const lowerAt = new Date(lower.at).getTime();
43
+ const upperAt = new Date(upper.at).getTime();
44
+ const targetAt = target.getTime();
45
+ if (lowerAt > upperAt) return { status: "unavailable", reason: "timeline-not-monotonic" };
46
+ if (targetAt < lowerAt) return { status: "before-start", offset: lower.offset };
47
+ if (targetAt > upperAt) return { status: "out-of-range" };
48
+ let left = lower;
49
+ let right = upper;
50
+ for (let count = 0; count < 24 && right.offset - left.offset > 128 * 1024; count += 1) {
51
+ const probe = await timestampLineAt({ handle, position: Math.floor((left.offset + right.offset) / 2), size,
52
+ last: false, readRange, extractTimestamp });
53
+ if (!probe) return { status: "unavailable", reason: "timeline-not-seekable" };
54
+ const value = new Date(probe.at).getTime();
55
+ if (value < new Date(left.at).getTime() || value > new Date(right.at).getTime()) {
56
+ return { status: "unavailable", reason: "timeline-not-monotonic" };
57
+ }
58
+ if (value <= targetAt) left = probe;
59
+ else right = probe;
60
+ }
61
+ return { status: "found", offset: left.offset };
62
+ }
63
+
64
+ export async function seekTimelineEvidence({ handle, size, target, wanted, windowMs, readRange, eventFromLine,
65
+ extractTimestamp, matches, rank }) {
66
+ const start = await seekTimestamp({ handle, size, target: new Date(target.getTime() - windowMs), readRange, extractTimestamp });
67
+ if (!["found", "before-start"].includes(start.status)) return start;
68
+ const bytes = await readRange(handle, start.offset, Math.min(4 * 1024 * 1024, size - start.offset));
69
+ const ending = bytes.lastIndexOf(0x0a);
70
+ const selected = ending < 0 && start.offset + bytes.byteLength !== size ? -1 : ending < 0 ? bytes.byteLength : ending + 1;
71
+ if (selected < 0) return { status: "unavailable", reason: "timeline-not-seekable" };
72
+ const events = rank(parseEvents(bytes.subarray(0, selected), start.offset, eventFromLine)
73
+ .filter((event) => matches(event, wanted, target, windowMs)), wanted, target).slice(0, 8);
74
+ return { status: "searched", events, budgetExhausted: selected < size - start.offset };
75
+ }
76
+
77
+ export async function verifyTimelineEvent({ handle, event, readRange, digest, eventFromLine }) {
78
+ if (!Number.isSafeInteger(event.offset) || !Number.isSafeInteger(event.bytes) || event.bytes > 1024 * 1024) return null;
79
+ const raw = await readRange(handle, event.offset, event.bytes);
80
+ if (raw.byteLength !== event.bytes || digest(raw) !== event.sha256) return null;
81
+ return eventFromLine(raw.toString("utf8"), event.offset);
82
+ }
@@ -0,0 +1,217 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { lstat, mkdtemp, open, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ // Numeric FileSystemRights values and SDDL tokens are stable across Windows
8
+ // display languages. Friendly account names never enter the authorization path.
9
+ const FULL_CONTROL = 0x001f01ff;
10
+ const WRITE_MASK = 0x000d0156;
11
+ const GENERIC_WRITE_MASK = 0x50000000;
12
+ const MAX_ACL_BYTES = 1024 * 1024;
13
+ const SID = /^S-1-\d+(?:-\d+)+$/i;
14
+ const DACL_FLAGS = /^(?:(?:P|AR|AI))*$/;
15
+ const ACE_FLAGS = new Set(["CI", "OI", "NP", "IO", "ID"]);
16
+ const RIGHT_TOKENS = new Set([
17
+ "GA", "GR", "GW", "GX", "RC", "SD", "WD", "WO",
18
+ "RP", "WP", "CC", "DC", "LC", "SW", "LO", "DT", "CR",
19
+ "FA", "FR", "FW", "FX", "KA", "KR", "KW", "KX",
20
+ "NR", "NW", "NX"
21
+ ]);
22
+ const WRITE_TOKENS = new Set([
23
+ "GA", "GW", "SD", "WD", "WO", "WP", "CC", "DC", "SW", "DT", "CR",
24
+ "FA", "FW", "KA", "KW"
25
+ ]);
26
+ const TRUSTEE_ALIASES = new Map([
27
+ ["SY", "s-1-5-18"],
28
+ ["BA", "s-1-5-32-544"],
29
+ ["CO", "s-1-3-0"],
30
+ ["LA", "sddl:la"]
31
+ ]);
32
+
33
+ function splitTokens(value, allowed, label) {
34
+ if (value.length % 2 !== 0) {
35
+ throw new Error(`session timeline Windows SDDL ${label} is malformed`);
36
+ }
37
+ const tokens = value.match(/../g) || [];
38
+ if (tokens.some(token => !allowed.has(token))) {
39
+ throw new Error(`session timeline Windows SDDL ${label} is unsupported`);
40
+ }
41
+ return tokens;
42
+ }
43
+
44
+ function principalFromSddl(value, localAdministratorSid) {
45
+ if (SID.test(value)) return value.toLowerCase();
46
+ if (value === "LA" && SID.test(localAdministratorSid || "")
47
+ && localAdministratorSid.toLowerCase().endsWith("-500")) {
48
+ return localAdministratorSid.toLowerCase();
49
+ }
50
+ if (TRUSTEE_ALIASES.has(value)) return TRUSTEE_ALIASES.get(value);
51
+ if (/^[A-Z]{2}$/.test(value)) return `sddl:${value.toLowerCase()}`;
52
+ throw new Error("session timeline Windows SDDL trustee is malformed");
53
+ }
54
+
55
+ function numericRights(value) {
56
+ if (!/^0x[0-9a-f]{1,8}$/i.test(value)) return null;
57
+ return Number.parseInt(value.slice(2), 16) >>> 0;
58
+ }
59
+
60
+ function rightsFromSddl(value, type, flags) {
61
+ const rights = [];
62
+ if (flags.includes("ID")) rights.push("I");
63
+ if (flags.includes("IO")) rights.push("IO");
64
+ if (type === "D") return [...rights, "DENY"];
65
+ const mask = numericRights(value);
66
+ if (mask === null) {
67
+ const tokens = splitTokens(value, RIGHT_TOKENS, "rights");
68
+ if (tokens.includes("FA") || tokens.includes("GA")) rights.push("F");
69
+ if (tokens.some(token => WRITE_TOKENS.has(token))) rights.push("W");
70
+ return rights;
71
+ }
72
+ if ((mask & FULL_CONTROL) === FULL_CONTROL || (mask & 0x10000000) !== 0) rights.push("F");
73
+ if ((mask & WRITE_MASK) !== 0 || (mask & GENERIC_WRITE_MASK) !== 0) rights.push("W");
74
+ return rights;
75
+ }
76
+
77
+ export function parseWindowsSddlAcl(sddl, { localAdministratorSid = null } = {}) {
78
+ if (typeof sddl !== "string" || sddl.length > MAX_ACL_BYTES) {
79
+ throw new Error("session timeline Windows SDDL is unavailable");
80
+ }
81
+ const value = sddl.replace(/^\uFEFF/, "").trim();
82
+ if (!value.startsWith("D:")) throw new Error("session timeline Windows SDDL has no DACL");
83
+ const firstAce = value.indexOf("(", 2);
84
+ if (firstAce < 0 || !DACL_FLAGS.test(value.slice(2, firstAce))) {
85
+ throw new Error("session timeline Windows SDDL flags are malformed");
86
+ }
87
+ const entries = [];
88
+ let offset = firstAce;
89
+ while (offset < value.length) {
90
+ const match = /^\(([^()]*)\)/.exec(value.slice(offset));
91
+ if (!match) throw new Error("session timeline Windows SDDL ACE is malformed");
92
+ const fields = match[1].split(";");
93
+ if (fields.length !== 6 || !["A", "D"].includes(fields[0]) || fields[3] || fields[4]) {
94
+ throw new Error("session timeline Windows SDDL ACE is unsupported");
95
+ }
96
+ const flags = splitTokens(fields[1], ACE_FLAGS, "ACE flags");
97
+ const principal = principalFromSddl(fields[5], localAdministratorSid);
98
+ entries.push({ principal, rights: rightsFromSddl(fields[2], fields[0], flags) });
99
+ if (entries.length > 2048) throw new Error("session timeline Windows SDDL exceeds ACE limit");
100
+ offset += match[0].length;
101
+ }
102
+ if (!entries.length) throw new Error("session timeline Windows SDDL has no ACEs");
103
+ return entries;
104
+ }
105
+
106
+ export function parseWindowsAclSave(bytes, options = {}) {
107
+ if (!Buffer.isBuffer(bytes) || !bytes.length || bytes.length > MAX_ACL_BYTES || bytes.length % 2 !== 0) {
108
+ throw new Error("session timeline Windows ACL save is unavailable");
109
+ }
110
+ const lines = bytes.toString("utf16le").replace(/^\uFEFF/, "").split(/\r?\n/).filter(Boolean);
111
+ if (lines.length !== 2 || !lines[0].trim() || !lines[1].trim().startsWith("D:")) {
112
+ throw new Error("session timeline Windows ACL save is malformed");
113
+ }
114
+ return parseWindowsSddlAcl(lines[1].trim(), options);
115
+ }
116
+
117
+ export function windowsAclSaveCommand(path, outputPath) {
118
+ return [path, "/save", outputPath, "/q"];
119
+ }
120
+
121
+ async function readStableAclFile(path, options) {
122
+ const pathname = await lstat(path, { bigint: true });
123
+ if (!pathname.isFile() || pathname.isSymbolicLink() || pathname.nlink !== 1n
124
+ || pathname.size <= 0n || pathname.size > BigInt(MAX_ACL_BYTES)) {
125
+ throw new Error("session timeline Windows ACL save is unsafe");
126
+ }
127
+ const handle = await open(path, "r");
128
+ try {
129
+ const before = await handle.stat({ bigint: true });
130
+ const bytes = await handle.readFile();
131
+ const after = await handle.stat({ bigint: true });
132
+ const current = await lstat(path, { bigint: true });
133
+ if (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1n) {
134
+ throw new Error("session timeline Windows ACL save changed while reading");
135
+ }
136
+ for (const field of ["dev", "ino", "size", "mtimeNs", "ctimeNs"]) {
137
+ if (before[field] !== after[field] || after[field] !== current[field]) {
138
+ throw new Error("session timeline Windows ACL save changed while reading");
139
+ }
140
+ }
141
+ return parseWindowsAclSave(bytes, options);
142
+ } finally {
143
+ await handle.close();
144
+ }
145
+ }
146
+
147
+ export function windowsLocalAdministratorSid(identity, env) {
148
+ if (!identity || !SID.test(identity.sid || "") || typeof identity.account !== "string") return null;
149
+ const computer = typeof env?.COMPUTERNAME === "string" ? env.COMPUTERNAME.trim().toLowerCase() : "";
150
+ const [domain] = identity.account.trim().toLowerCase().split("\\");
151
+ if (!computer || domain !== computer || !identity.sid.toLowerCase().endsWith("-500")) return null;
152
+ return identity.sid.toLowerCase();
153
+ }
154
+
155
+ async function readNativeWindowsAcl(path, env, runCommand, run, identity) {
156
+ const directory = await mkdtemp(join(tmpdir(), "agentspine-timeline-sddl-"));
157
+ const outputPath = join(directory, `${randomUUID()}.acl`);
158
+ try {
159
+ const claim = await open(outputPath, "wx", 0o600);
160
+ await claim.close();
161
+ const binary = join(env.SystemRoot, "System32", "icacls.exe");
162
+ runCommand(run, binary, windowsAclSaveCommand(path, outputPath), env);
163
+ return await readStableAclFile(outputPath, {
164
+ localAdministratorSid: windowsLocalAdministratorSid(identity, env)
165
+ });
166
+ } finally {
167
+ await rm(directory, { recursive: true, force: true });
168
+ }
169
+ }
170
+
171
+ // Retained only for dependency-injected compatibility tests. Production uses
172
+ // icacls /save and parses its SID/SDDL export without starting PowerShell.
173
+ export function windowsSidAclCommand(path) {
174
+ const encodedPath = Buffer.from(path, "utf8").toString("base64");
175
+ const script = [
176
+ "$ErrorActionPreference = 'Stop'",
177
+ `$target = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedPath}'))`,
178
+ "if ([IO.Directory]::Exists($target)) { $acl = [IO.Directory]::GetAccessControl($target) }",
179
+ "else { $acl = [IO.File]::GetAccessControl($target) }",
180
+ "$rules = @($acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier]) | ForEach-Object {",
181
+ " [PSCustomObject]@{ sid=$_.IdentityReference.Value; mask=[int]$_.FileSystemRights; inherited=$_.IsInherited; propagation=[int]$_.PropagationFlags; inheritance=[int]$_.InheritanceFlags; type=[int]$_.AccessControlType }",
182
+ "})",
183
+ "$rules | ConvertTo-Json -Compress -AsArray"
184
+ ].join("\n");
185
+ return ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand",
186
+ Buffer.from(script, "utf16le").toString("base64")];
187
+ }
188
+
189
+ export async function readWindowsSidAcl(path, env, runCommand, run, identity = null) {
190
+ if (run === spawnSync) return readNativeWindowsAcl(path, env, runCommand, run, identity);
191
+ const binary = join(env.SystemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
192
+ return parseWindowsSidAcl(runCommand(run, binary, windowsSidAclCommand(path), env));
193
+ }
194
+
195
+ export function parseWindowsSidAcl(output) {
196
+ const rows = JSON.parse(output);
197
+ if (!Array.isArray(rows) || !rows.length || rows.length > 2048) {
198
+ throw new Error("session timeline Windows SID ACL is unavailable");
199
+ }
200
+ return rows.map(row => {
201
+ if (!row || !SID.test(row.sid) || typeof row.inherited !== "boolean"
202
+ || !Number.isInteger(row.mask) || row.mask < 0 || row.mask > 0x1fffff
203
+ || ![0, 1].includes(row.type) || ![0, 1, 2, 3].includes(row.propagation)
204
+ || ![0, 1, 2, 3].includes(row.inheritance)) {
205
+ throw new Error("session timeline Windows SID ACL is malformed");
206
+ }
207
+ const rights = [];
208
+ if (row.inherited) rights.push("I");
209
+ if (row.propagation & 2) rights.push("IO");
210
+ if (row.type === 1) rights.push("DENY");
211
+ else {
212
+ if ((row.mask & FULL_CONTROL) === FULL_CONTROL) rights.push("F");
213
+ if (row.mask & WRITE_MASK) rights.push("W");
214
+ }
215
+ return { principal: row.sid.toLowerCase(), rights };
216
+ });
217
+ }
@@ -0,0 +1,83 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, realpath } from "node:fs/promises";
3
+ import { isAbsolute, join, resolve } from "node:path";
4
+ import { comparablePath, isInside } from "./paths.js";
5
+ import { validTimelineHost } from "./session-timeline-provider.js";
6
+
7
+ function digest(value) { return createHash("sha256").update(value).digest("hex"); }
8
+
9
+ function metadataValue(metadata, key) {
10
+ return String(metadata[key] ?? BigInt(Math.floor(Number(metadata[`${key.slice(0, -2)}Ms`]) * 1_000_000)));
11
+ }
12
+
13
+ function sourceIdentity(metadata) { return [metadata.dev, metadata.ino].join(":"); }
14
+
15
+ const SOURCE_LOCATION_KEYS = ["path", "profileRoot", "projectsRoot", "pathDigest", "identity"];
16
+
17
+ export function validSessionTimelineSourceMetadata(value) {
18
+ return value && typeof value.path === "string" && isAbsolute(value.path) && value.path.length <= 4096
19
+ && typeof value.profileRoot === "string" && isAbsolute(value.profileRoot) && value.profileRoot.length <= 4096
20
+ && typeof value.projectsRoot === "string" && isAbsolute(value.projectsRoot) && value.projectsRoot.length <= 4096
21
+ && value.pathDigest === digest(value.path) && typeof value.identity === "string" && value.identity.length > 0
22
+ && Number.isSafeInteger(value.size) && value.size >= 0
23
+ && /^[0-9]+$/.test(value.mtimeNs || "") && /^[0-9]+$/.test(value.ctimeNs || "");
24
+ }
25
+
26
+ export function sameSessionTimelineSourceLocation(left, right) {
27
+ return validSessionTimelineSourceMetadata(left) && validSessionTimelineSourceMetadata(right)
28
+ && SOURCE_LOCATION_KEYS.every((key) => left[key] === right[key]);
29
+ }
30
+
31
+ export async function sourcePath(value, hostHome, host = "claude") {
32
+ if (!validTimelineHost(host)) return { status: "unavailable", reason: "unknown-timeline-host" };
33
+ if (typeof value !== "string" || !isAbsolute(value)) return { status: "unavailable", reason: "no-validated-transcript" };
34
+ let profileRoot; let projectsRoot;
35
+ try {
36
+ if (!isAbsolute(hostHome || "")) throw new Error("invalid-host-root");
37
+ const profile = await lstat(hostHome);
38
+ if (!profile.isDirectory() || profile.isSymbolicLink()) throw new Error("invalid-host-root");
39
+ profileRoot = await realpath(hostHome);
40
+ const directory = ["codex", "king"].includes(host) ? "sessions" : "projects";
41
+ const projectsPath = join(profileRoot, directory);
42
+ const projects = await lstat(projectsPath);
43
+ if (!projects.isDirectory() || projects.isSymbolicLink()) throw new Error("invalid-host-root");
44
+ projectsRoot = await realpath(projectsPath);
45
+ if (!isInside(profileRoot, projectsRoot)) throw new Error("invalid-host-root");
46
+ }
47
+ catch (error) { return { status: "unavailable", reason: error.code === "ENOENT" ? "no-validated-transcript" : "transcript-root-unavailable" }; }
48
+ let metadata;
49
+ try { metadata = await lstat(value, { bigint: true }); }
50
+ catch (error) { return { status: "unavailable", reason: error.code === "ENOENT" ? "transcript-missing" : "transcript-unreadable" }; }
51
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1n) {
52
+ return { status: "unavailable", reason: "transcript-not-regular" };
53
+ }
54
+ let canonical;
55
+ try { canonical = await realpath(value); }
56
+ catch { return { status: "unavailable", reason: "transcript-unreadable" }; }
57
+ if (["codex", "king"].includes(host) && comparablePath(canonical) !== comparablePath(resolve(value))) {
58
+ return { status: "unavailable", reason: "transcript-symlink-path" };
59
+ }
60
+ if (!isInside(projectsRoot, canonical)) return { status: "unavailable", reason: "transcript-outside-host-projects" };
61
+ return { status: "registered", path: canonical, profileRoot, projectsRoot, pathDigest: digest(canonical),
62
+ identity: [metadata.dev, metadata.ino].join(":"), size: Number(metadata.size),
63
+ mtimeNs: metadataValue(metadata, "mtimeNs"), ctimeNs: metadataValue(metadata, "ctimeNs") };
64
+ }
65
+
66
+ export function matchesSourceMetadata(metadata, source) {
67
+ const identity = [metadata.dev, metadata.ino].join(":");
68
+ return metadata.isFile() && !metadata.isSymbolicLink() && metadata.nlink === 1n && identity === source.identity
69
+ && Number(metadata.size) === source.size && metadataValue(metadata, "mtimeNs") === source.mtimeNs
70
+ && metadataValue(metadata, "ctimeNs") === source.ctimeNs;
71
+ }
72
+
73
+ export async function pathMatchesSource(source, hostHome = null) {
74
+ // A post-compaction read may not have a fresh host root. The persisted
75
+ // enrollment root is the sealed authority in that case; ambient process
76
+ // configuration must never redirect a previously bound transcript.
77
+ const trustedHome = isAbsolute(hostHome || "") ? hostHome : source?.profileRoot;
78
+ if (!isAbsolute(trustedHome || "")) return false;
79
+ const current = await sourcePath(source.path, trustedHome, source.binding?.host || "claude");
80
+ return current.status === "registered" && current.profileRoot === source.profileRoot && current.projectsRoot === source.projectsRoot
81
+ && current.pathDigest === source.pathDigest && current.identity === source.identity
82
+ && current.size === source.size && current.mtimeNs === source.mtimeNs && current.ctimeNs === source.ctimeNs;
83
+ }
@@ -0,0 +1,45 @@
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, recordSessionTimelineHead, sealSessionTimelineState,
6
+ sessionTimelineHeadExists, verifySessionTimelineHead, verifySessionTimelineState
7
+ } from "./session-timeline-auth.js";
8
+
9
+ export async function readTimelineState({ path, root, maximumBytes, empty, validate, assertStable = null }) {
10
+ try {
11
+ const content = await readAuthenticatedTimelineState(path, maximumBytes, assertStable);
12
+ const state = await verifySessionTimelineState(validate(JSON.parse(content), root));
13
+ await verifySessionTimelineHead({ root, stateSignature: state.signature });
14
+ return state;
15
+ } catch (error) {
16
+ if (error.code === "ENOENT") {
17
+ if (await sessionTimelineHeadExists({ root })) throw new Error("session timeline state is missing after a signed head");
18
+ return empty(root);
19
+ }
20
+ throw error;
21
+ }
22
+ }
23
+
24
+ export async function saveTimelineState({ state, path, root, maximumBytes, assertOwned, assertStable = null }) {
25
+ const assertWritable = async () => {
26
+ await assertStable?.();
27
+ await assertOwned?.();
28
+ };
29
+ await assertWritable();
30
+ await sealSessionTimelineState(state);
31
+ const content = `${JSON.stringify(state, null, 2)}\n`;
32
+ if (Buffer.byteLength(content) > maximumBytes) throw new Error("session timeline state exceeds 16 MiB");
33
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
34
+ await writeFile(temporary, content, { mode: 0o600 });
35
+ try {
36
+ await replaceFileWithRetry(temporary, path, { beforeAttempt: assertWritable });
37
+ await assertWritable();
38
+ }
39
+ catch (error) {
40
+ await unlink(temporary).catch((cleanup) => { if (cleanup.code !== "ENOENT") error.cleanupError = cleanup; });
41
+ throw error;
42
+ }
43
+ await recordSessionTimelineHead({ root, stateSignature: state.signature, assertOwned: assertWritable });
44
+ await assertStable?.();
45
+ }
@@ -0,0 +1,50 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import { completeTimelineBinding, safeTimelineId } from "./session-timeline-contract.js";
3
+ import { sessionTimelineRootDigest } from "./session-timeline-root.js";
4
+
5
+ // This capability is deliberately supplied only by a locally configured host
6
+ // transport. It is never a MCP argument, stored value, hook output, or card.
7
+ export const TIMELINE_TRANSPORT_CAPABILITY_ENV = "AGENTSPINE_TIMELINE_SESSION_CAPABILITY";
8
+ export const TIMELINE_TRANSPORT_SESSION_ENV = "AGENTSPINE_TIMELINE_TRANSPORT_SESSION_ID";
9
+
10
+ const CAPABILITY_RE = /^astc_[A-Za-z0-9_-]{43}$/;
11
+ const DIGEST_RE = /^[a-f0-9]{64}$/;
12
+ function validDigest(value) { return typeof value === "string" && DIGEST_RE.test(value); }
13
+
14
+ function canonical(value) {
15
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
16
+ if (value && typeof value === "object") {
17
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
18
+ }
19
+ return JSON.stringify(value);
20
+ }
21
+
22
+ function digest(value) { return createHash("sha256").update(value).digest("hex"); }
23
+
24
+ function capability(environment) {
25
+ const value = environment?.[TIMELINE_TRANSPORT_CAPABILITY_ENV];
26
+ const sessionId = safeTimelineId(environment?.[TIMELINE_TRANSPORT_SESSION_ENV]);
27
+ return typeof value === "string" && CAPABILITY_RE.test(value) && sessionId ? { value, sessionId } : null;
28
+ }
29
+
30
+ // The environment must carry a freshly generated 32-byte capability for the
31
+ // exact host session. A global or argument-supplied fallback is intentionally
32
+ // absent: standard MCP stdio does not authenticate callers by itself.
33
+ export function timelineTransportDigest({ root, binding, environment = process.env }) {
34
+ const current = capability(environment);
35
+ if (typeof root !== "string" || !root || !completeTimelineBinding(binding) || !current
36
+ || current.sessionId !== binding.sessionId) return null;
37
+ return digest(`agentspine.timeline-transport/v1\0${current.value}\0${sessionTimelineRootDigest(root)}\0${canonical(binding)}`);
38
+ }
39
+
40
+ export function validTimelineTransportDigest(value) {
41
+ return validDigest(value);
42
+ }
43
+
44
+ export function sameTimelineTransportDigest(left, right) {
45
+ if (!validTimelineTransportDigest(left) || !validTimelineTransportDigest(right)) return false;
46
+ return timingSafeEqual(Buffer.from(left, "hex"), Buffer.from(right, "hex"));
47
+ }
48
+
49
+ // The one-use record is valid only while the exact local enrollment that
50
+ // produced it is still active. This check occurs before the record is removed.
@@ -0,0 +1,148 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { isAbsolute, join } from "node:path";
3
+ import { readWindowsSidAcl } from "./session-timeline-sid-acl.js";
4
+
5
+ const SID_RE = /\bS-\d+(?:-\d+)+\b/i;
6
+ const WRITE_RIGHTS = new Set(["AD", "D", "DC", "DE", "F", "GA", "GW", "M", "W", "WA", "WD", "WDAC", "WEA", "WO"]);
7
+ const SYSTEM_PRINCIPALS = new Set([
8
+ "s-1-5-18", "s-1-5-32-544", "sddl:la"
9
+ ]);
10
+ const MANDATORY_LABEL_PREFIX = "mandatory label\\";
11
+ const FILE_ROLES = new Set(["key", "state", "head", "private"]);
12
+
13
+ function failure(message, result = null) {
14
+ const detail = result?.error?.message || result?.stderr || result?.stdout || "no diagnostic";
15
+ throw new Error(`session timeline Windows ACL ${message}: ${String(detail).trim().slice(0, 240)}`);
16
+ }
17
+
18
+ function executable(name, env) {
19
+ const root = env.SystemRoot;
20
+ if (typeof root !== "string" || !isAbsolute(root)) throw new Error("session timeline Windows system root is unavailable");
21
+ return join(root, "System32", name);
22
+ }
23
+
24
+ function runCommand(run, binary, args, env) {
25
+ const result = run(binary, args, { encoding: "utf8", env, shell: false, timeout: 1_500, windowsHide: true });
26
+ if (!result || result.status !== 0 || result.error || result.signal) failure(`${binary} failed`, result);
27
+ return `${result.stdout || ""}\n${result.stderr || ""}`;
28
+ }
29
+
30
+ function identityFrom(output) {
31
+ const sid = output.match(SID_RE)?.[0]?.toLowerCase();
32
+ const account = output.match(/^\s*"((?:[^"]|"")*)"\s*,/m)?.[1]?.replaceAll('""', '"').toLowerCase();
33
+ if (!sid || !account) throw new Error("session timeline Windows identity is unavailable");
34
+ return { sid, account };
35
+ }
36
+
37
+ function normalizedPrincipal(value) {
38
+ return value.trim().replace(/^\*/, "").replaceAll("/", "\\").toLowerCase();
39
+ }
40
+
41
+ function entriesFrom(output, path) {
42
+ const values = [];
43
+ let first = true;
44
+ for (const source of output.split(/\r?\n/)) {
45
+ let line = source.trim();
46
+ if (!line) continue;
47
+ if (first && line.toLowerCase().startsWith(path.toLowerCase())) line = line.slice(path.length).trim();
48
+ first = false;
49
+ const match = line.match(/^(.+?):((?:\([^)]*\))+)/);
50
+ if (!match) continue;
51
+ values.push({ principal: normalizedPrincipal(match[1]), rights: [...match[2].matchAll(/\(([^)]*)\)/g)].map((item) => item[1].toUpperCase()) });
52
+ }
53
+ return values;
54
+ }
55
+
56
+ function canWrite(entry) { return entry.rights.some((right) => WRITE_RIGHTS.has(right)); }
57
+ function isSelf(principal, identity) { return principal === identity.sid; }
58
+ export function isTrustedWindowsWriter(principal, identity) {
59
+ return isSelf(principal, identity) || SYSTEM_PRINCIPALS.has(principal);
60
+ }
61
+ function isMandatoryLabel(entry) {
62
+ return entry.principal.startsWith(MANDATORY_LABEL_PREFIX)
63
+ && entry.rights.every((right) => new Set(["I", "IO", "OI", "CI", "NW"]).has(right));
64
+ }
65
+ function isPrivatePrincipal(entry, identity) {
66
+ return isTrustedWindowsWriter(entry.principal, identity) || isMandatoryLabel(entry);
67
+ }
68
+ function isTrustedParentWriter(entry, identity) {
69
+ return isTrustedWindowsWriter(entry.principal, identity)
70
+ || (entry.principal === "s-1-3-0" && entry.rights.includes("IO"));
71
+ }
72
+
73
+ function verifyParent(entries, identity) {
74
+ if (!entries.length) throw new Error("session timeline Windows parent ACL has no readable entries");
75
+ const unsafe = entries.find((entry) => canWrite(entry) && !isTrustedParentWriter(entry, identity));
76
+ if (unsafe) throw new Error(`session timeline Windows parent ACL grants write access to ${unsafe.principal}`);
77
+ }
78
+
79
+ function verifyIntegrity(entries, identity) {
80
+ const owner = entries.find((entry) => isSelf(entry.principal, identity)
81
+ && entry.rights.includes("F") && !entry.rights.includes("I") && !entry.rights.includes("IO"));
82
+ if (!owner || entries.some((entry) => !isPrivatePrincipal(entry, identity))) {
83
+ throw new Error("session timeline Windows integrity ACL is not private to the current SID");
84
+ }
85
+ }
86
+
87
+ function verifyFile(entries, identity, role) {
88
+ const owner = entries.find((entry) => isSelf(entry.principal, identity)
89
+ && entry.rights.includes("F") && !entry.rights.includes("IO"));
90
+ if (!owner || entries.some((entry) => !isPrivatePrincipal(entry, identity))) {
91
+ throw new Error(`session timeline Windows ${role} ACL is not private to the current SID`);
92
+ }
93
+ }
94
+
95
+ function fingerprint(metadata) {
96
+ if (!metadata) return null;
97
+ return [metadata.dev, metadata.ino, metadata.mtimeNs, metadata.ctimeNs].map(String).join(":");
98
+ }
99
+
100
+ export function createWindowsTimelineAclVerifier({ platform = process.platform, env = process.env, run = spawnSync } = {}) {
101
+ const cache = new Map();
102
+ let identity = null;
103
+ return async function verifyWindowsTimelineDirectoryAcl(path, { created = false, metadata = null, role = "integrity" } = {}) {
104
+ if (platform !== "win32") return;
105
+ if (!isAbsolute(path) || !["integrity", "parent"].includes(role)) {
106
+ throw new Error("session timeline Windows ACL request is invalid");
107
+ }
108
+ const cached = `${role}:${path.toLowerCase()}:${fingerprint(metadata)}`;
109
+ if (!created && fingerprint(metadata) && cache.has(cached)) return;
110
+ const whoami = executable("whoami.exe", env);
111
+ const icacls = executable("icacls.exe", env);
112
+ identity ||= identityFrom(runCommand(run, whoami, ["/user", "/fo", "csv", "/nh"], env));
113
+ if (role === "integrity" && created) {
114
+ runCommand(run, icacls, [path, "/inheritance:r", "/grant:r", `*${identity.sid}:(OI)(CI)(F)`], env);
115
+ }
116
+ const entries = await readWindowsSidAcl(path, env, runCommand, run, identity);
117
+ if (role === "integrity") verifyIntegrity(entries, identity); else verifyParent(entries, identity);
118
+ if (!created && fingerprint(metadata)) cache.set(cached, true);
119
+ };
120
+ }
121
+
122
+ // Every boundary supplies fresh file metadata. On Windows, changing a security
123
+ // descriptor changes ctime, so only an identical file identity/change-time can
124
+ // reuse a parsed ACL. A changed DACL therefore misses this cache and is denied.
125
+ export function createWindowsTimelineFileAclVerifier({ platform = process.platform, env = process.env, run = spawnSync } = {}) {
126
+ const cache = new Map();
127
+ let identity = null;
128
+ return async function verifyWindowsTimelineFileAcl(path, { created = false, metadata = null, role = "private" } = {}) {
129
+ if (platform !== "win32") return;
130
+ if (!isAbsolute(path) || !FILE_ROLES.has(role)) {
131
+ throw new Error("session timeline Windows file ACL request is invalid");
132
+ }
133
+ const cached = `${role}:${path.toLowerCase()}:${fingerprint(metadata)}`;
134
+ if (!created && fingerprint(metadata) && cache.has(cached)) return;
135
+ const whoami = executable("whoami.exe", env);
136
+ const icacls = executable("icacls.exe", env);
137
+ identity ||= identityFrom(runCommand(run, whoami, ["/user", "/fo", "csv", "/nh"], env));
138
+ if (created) runCommand(run, icacls, [path, "/inheritance:r", "/grant:r", `*${identity.sid}:(F)`], env);
139
+ verifyFile(await readWindowsSidAcl(path, env, runCommand, run, identity), identity, role);
140
+ if (!created && fingerprint(metadata)) cache.set(cached, true);
141
+ };
142
+ }
143
+
144
+ const verifyWindowsTimelineDirectoryAcl = createWindowsTimelineAclVerifier();
145
+ const verifyWindowsTimelineFileAcl = createWindowsTimelineFileAclVerifier();
146
+
147
+ export { entriesFrom as parseWindowsIcaclsEntries };
148
+ export { verifyWindowsTimelineDirectoryAcl, verifyWindowsTimelineFileAcl };