open-multi-agent-kit 0.90.8 → 0.91.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +21 -2
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +4 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/commands/doctor-provider-cli.d.ts +33 -0
  7. package/dist/commands/doctor-provider-cli.d.ts.map +1 -0
  8. package/dist/commands/doctor-provider-cli.js +215 -0
  9. package/dist/commands/doctor-provider-cli.js.map +1 -0
  10. package/dist/commands/doctor-provider-transport.d.ts +35 -0
  11. package/dist/commands/doctor-provider-transport.d.ts.map +1 -0
  12. package/dist/commands/doctor-provider-transport.js +185 -0
  13. package/dist/commands/doctor-provider-transport.js.map +1 -0
  14. package/dist/commands/doctor-provider.d.ts +121 -7
  15. package/dist/commands/doctor-provider.d.ts.map +1 -1
  16. package/dist/commands/doctor-provider.js +1295 -102
  17. package/dist/commands/doctor-provider.js.map +1 -1
  18. package/dist/commands/session-doctor-cli.d.ts +43 -0
  19. package/dist/commands/session-doctor-cli.d.ts.map +1 -0
  20. package/dist/commands/session-doctor-cli.js +494 -0
  21. package/dist/commands/session-doctor-cli.js.map +1 -0
  22. package/dist/commands/verify-ci.d.ts +21 -0
  23. package/dist/commands/verify-ci.d.ts.map +1 -0
  24. package/dist/commands/verify-ci.js +66 -0
  25. package/dist/commands/verify-ci.js.map +1 -0
  26. package/dist/core/agent-session-services.d.ts +3 -0
  27. package/dist/core/agent-session-services.d.ts.map +1 -1
  28. package/dist/core/agent-session-services.js +3 -0
  29. package/dist/core/agent-session-services.js.map +1 -1
  30. package/dist/core/agent-session.d.ts +140 -2
  31. package/dist/core/agent-session.d.ts.map +1 -1
  32. package/dist/core/agent-session.js +1179 -95
  33. package/dist/core/agent-session.js.map +1 -1
  34. package/dist/core/agent-tool-settings.d.ts +35 -0
  35. package/dist/core/agent-tool-settings.d.ts.map +1 -0
  36. package/dist/core/agent-tool-settings.js +155 -0
  37. package/dist/core/agent-tool-settings.js.map +1 -0
  38. package/dist/core/atomic-session-file.d.ts +14 -0
  39. package/dist/core/atomic-session-file.d.ts.map +1 -0
  40. package/dist/core/atomic-session-file.js +104 -0
  41. package/dist/core/atomic-session-file.js.map +1 -0
  42. package/dist/core/command-safety.d.ts.map +1 -1
  43. package/dist/core/command-safety.js +0 -8
  44. package/dist/core/command-safety.js.map +1 -1
  45. package/dist/core/compaction/compaction.d.ts +6 -0
  46. package/dist/core/compaction/compaction.d.ts.map +1 -1
  47. package/dist/core/compaction/compaction.js +12 -1
  48. package/dist/core/compaction/compaction.js.map +1 -1
  49. package/dist/core/compaction/hysteresis.d.ts +27 -0
  50. package/dist/core/compaction/hysteresis.d.ts.map +1 -0
  51. package/dist/core/compaction/hysteresis.js +84 -0
  52. package/dist/core/compaction/hysteresis.js.map +1 -0
  53. package/dist/core/compaction/index.d.ts +2 -0
  54. package/dist/core/compaction/index.d.ts.map +1 -1
  55. package/dist/core/compaction/index.js +2 -0
  56. package/dist/core/compaction/index.js.map +1 -1
  57. package/dist/core/compaction/transaction.d.ts +172 -0
  58. package/dist/core/compaction/transaction.d.ts.map +1 -0
  59. package/dist/core/compaction/transaction.js +824 -0
  60. package/dist/core/compaction/transaction.js.map +1 -0
  61. package/dist/core/context-budget-governor-v2.d.ts +2 -1
  62. package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
  63. package/dist/core/context-budget-governor-v2.js +2 -1
  64. package/dist/core/context-budget-governor-v2.js.map +1 -1
  65. package/dist/core/context-budget-reserved-tokens.d.ts +110 -0
  66. package/dist/core/context-budget-reserved-tokens.d.ts.map +1 -0
  67. package/dist/core/context-budget-reserved-tokens.js +188 -0
  68. package/dist/core/context-budget-reserved-tokens.js.map +1 -0
  69. package/dist/core/context-budget-v2-cache-invalidation.d.ts +133 -0
  70. package/dist/core/context-budget-v2-cache-invalidation.d.ts.map +1 -0
  71. package/dist/core/context-budget-v2-cache-invalidation.js +452 -0
  72. package/dist/core/context-budget-v2-cache-invalidation.js.map +1 -0
  73. package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
  74. package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
  75. package/dist/core/context-budget-v2-cache-keys.js +6 -0
  76. package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
  77. package/dist/core/context-budget-v2-cache-provider.d.ts +4 -0
  78. package/dist/core/context-budget-v2-cache-provider.d.ts.map +1 -1
  79. package/dist/core/context-budget-v2-cache-provider.js +19 -0
  80. package/dist/core/context-budget-v2-cache-provider.js.map +1 -1
  81. package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
  82. package/dist/core/context-budget-v2-plan-cache-keys.js +1 -0
  83. package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
  84. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  85. package/dist/core/context-budget-v2-planner.js +1 -0
  86. package/dist/core/context-budget-v2-planner.js.map +1 -1
  87. package/dist/core/context-budget-v2-types.d.ts +4 -0
  88. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  89. package/dist/core/context-budget-v2-types.js.map +1 -1
  90. package/dist/core/durable-file-directory.d.ts +4 -0
  91. package/dist/core/durable-file-directory.d.ts.map +1 -0
  92. package/dist/core/durable-file-directory.js +76 -0
  93. package/dist/core/durable-file-directory.js.map +1 -0
  94. package/dist/core/durable-file-identity.d.ts +26 -0
  95. package/dist/core/durable-file-identity.d.ts.map +1 -0
  96. package/dist/core/durable-file-identity.js +118 -0
  97. package/dist/core/durable-file-identity.js.map +1 -0
  98. package/dist/core/durable-file-io.d.ts +29 -0
  99. package/dist/core/durable-file-io.d.ts.map +1 -0
  100. package/dist/core/durable-file-io.js +179 -0
  101. package/dist/core/durable-file-io.js.map +1 -0
  102. package/dist/core/durable-file-lock-cleanup.d.ts +12 -0
  103. package/dist/core/durable-file-lock-cleanup.d.ts.map +1 -0
  104. package/dist/core/durable-file-lock-cleanup.js +39 -0
  105. package/dist/core/durable-file-lock-cleanup.js.map +1 -0
  106. package/dist/core/durable-file-lock-owner.d.ts +18 -0
  107. package/dist/core/durable-file-lock-owner.d.ts.map +1 -0
  108. package/dist/core/durable-file-lock-owner.js +78 -0
  109. package/dist/core/durable-file-lock-owner.js.map +1 -0
  110. package/dist/core/durable-file-lock-storage.d.ts +35 -0
  111. package/dist/core/durable-file-lock-storage.d.ts.map +1 -0
  112. package/dist/core/durable-file-lock-storage.js +263 -0
  113. package/dist/core/durable-file-lock-storage.js.map +1 -0
  114. package/dist/core/durable-file-mode.d.ts +8 -0
  115. package/dist/core/durable-file-mode.d.ts.map +1 -0
  116. package/dist/core/durable-file-mode.js +41 -0
  117. package/dist/core/durable-file-mode.js.map +1 -0
  118. package/dist/core/extensions/loader.d.ts +12 -0
  119. package/dist/core/extensions/loader.d.ts.map +1 -1
  120. package/dist/core/extensions/loader.js +46 -37
  121. package/dist/core/extensions/loader.js.map +1 -1
  122. package/dist/core/footer-data-provider.d.ts +7 -1
  123. package/dist/core/footer-data-provider.d.ts.map +1 -1
  124. package/dist/core/footer-data-provider.js +12 -0
  125. package/dist/core/footer-data-provider.js.map +1 -1
  126. package/dist/core/index.d.ts +7 -2
  127. package/dist/core/index.d.ts.map +1 -1
  128. package/dist/core/index.js +6 -0
  129. package/dist/core/index.js.map +1 -1
  130. package/dist/core/metrics-sampler.d.ts +20 -0
  131. package/dist/core/metrics-sampler.d.ts.map +1 -1
  132. package/dist/core/metrics-sampler.js +52 -0
  133. package/dist/core/metrics-sampler.js.map +1 -1
  134. package/dist/core/model-registry.d.ts.map +1 -1
  135. package/dist/core/model-registry.js +4 -1
  136. package/dist/core/model-registry.js.map +1 -1
  137. package/dist/core/model-resolver.d.ts.map +1 -1
  138. package/dist/core/model-resolver.js +16 -7
  139. package/dist/core/model-resolver.js.map +1 -1
  140. package/dist/core/provider-resilience.d.ts +51 -0
  141. package/dist/core/provider-resilience.d.ts.map +1 -0
  142. package/dist/core/provider-resilience.js +86 -0
  143. package/dist/core/provider-resilience.js.map +1 -0
  144. package/dist/core/redaction.d.ts +8 -0
  145. package/dist/core/redaction.d.ts.map +1 -0
  146. package/dist/core/redaction.js +31 -0
  147. package/dist/core/redaction.js.map +1 -0
  148. package/dist/core/resource-loader.d.ts.map +1 -1
  149. package/dist/core/resource-loader.js +36 -0
  150. package/dist/core/resource-loader.js.map +1 -1
  151. package/dist/core/run-journal-store.d.ts +53 -0
  152. package/dist/core/run-journal-store.d.ts.map +1 -0
  153. package/dist/core/run-journal-store.js +278 -0
  154. package/dist/core/run-journal-store.js.map +1 -0
  155. package/dist/core/run-journal.d.ts +172 -0
  156. package/dist/core/run-journal.d.ts.map +1 -0
  157. package/dist/core/run-journal.js +1045 -0
  158. package/dist/core/run-journal.js.map +1 -0
  159. package/dist/core/sdk.d.ts +7 -0
  160. package/dist/core/sdk.d.ts.map +1 -1
  161. package/dist/core/sdk.js +94 -3
  162. package/dist/core/sdk.js.map +1 -1
  163. package/dist/core/session-doctor-plan.d.ts +128 -0
  164. package/dist/core/session-doctor-plan.d.ts.map +1 -0
  165. package/dist/core/session-doctor-plan.js +833 -0
  166. package/dist/core/session-doctor-plan.js.map +1 -0
  167. package/dist/core/session-file-compaction.d.ts +6 -0
  168. package/dist/core/session-file-compaction.d.ts.map +1 -0
  169. package/dist/core/session-file-compaction.js +169 -0
  170. package/dist/core/session-file-compaction.js.map +1 -0
  171. package/dist/core/session-file-head.d.ts +10 -0
  172. package/dist/core/session-file-head.d.ts.map +1 -0
  173. package/dist/core/session-file-head.js +78 -0
  174. package/dist/core/session-file-head.js.map +1 -0
  175. package/dist/core/session-file-persistence.d.ts +30 -0
  176. package/dist/core/session-file-persistence.d.ts.map +1 -0
  177. package/dist/core/session-file-persistence.js +106 -0
  178. package/dist/core/session-file-persistence.js.map +1 -0
  179. package/dist/core/session-integrity.d.ts +43 -0
  180. package/dist/core/session-integrity.d.ts.map +1 -0
  181. package/dist/core/session-integrity.js +420 -0
  182. package/dist/core/session-integrity.js.map +1 -0
  183. package/dist/core/session-manager.d.ts +24 -4
  184. package/dist/core/session-manager.d.ts.map +1 -1
  185. package/dist/core/session-manager.js +242 -125
  186. package/dist/core/session-manager.js.map +1 -1
  187. package/dist/core/session-owner-lease.d.ts +22 -0
  188. package/dist/core/session-owner-lease.d.ts.map +1 -0
  189. package/dist/core/session-owner-lease.js +176 -0
  190. package/dist/core/session-owner-lease.js.map +1 -0
  191. package/dist/core/session-path-policy.d.ts +132 -0
  192. package/dist/core/session-path-policy.d.ts.map +1 -0
  193. package/dist/core/session-path-policy.js +780 -0
  194. package/dist/core/session-path-policy.js.map +1 -0
  195. package/dist/core/session-repair-plan.d.ts +49 -0
  196. package/dist/core/session-repair-plan.d.ts.map +1 -0
  197. package/dist/core/session-repair-plan.js +202 -0
  198. package/dist/core/session-repair-plan.js.map +1 -0
  199. package/dist/core/session-termination.d.ts +106 -0
  200. package/dist/core/session-termination.d.ts.map +1 -0
  201. package/dist/core/session-termination.js +445 -0
  202. package/dist/core/session-termination.js.map +1 -0
  203. package/dist/core/settings-manager.d.ts +52 -0
  204. package/dist/core/settings-manager.d.ts.map +1 -1
  205. package/dist/core/settings-manager.js +54 -5
  206. package/dist/core/settings-manager.js.map +1 -1
  207. package/dist/core/system-prompt.d.ts.map +1 -1
  208. package/dist/core/system-prompt.js +1 -1
  209. package/dist/core/system-prompt.js.map +1 -1
  210. package/dist/core/tools/grep.d.ts.map +1 -1
  211. package/dist/core/tools/grep.js +47 -0
  212. package/dist/core/tools/grep.js.map +1 -1
  213. package/dist/core/tools/omp-pure-seams.d.ts +41 -0
  214. package/dist/core/tools/omp-pure-seams.d.ts.map +1 -0
  215. package/dist/core/tools/omp-pure-seams.js +142 -0
  216. package/dist/core/tools/omp-pure-seams.js.map +1 -0
  217. package/dist/core/tools/omp-seam-runtime.d.ts +100 -0
  218. package/dist/core/tools/omp-seam-runtime.d.ts.map +1 -0
  219. package/dist/core/tools/omp-seam-runtime.js +40 -0
  220. package/dist/core/tools/omp-seam-runtime.js.map +1 -0
  221. package/dist/core/tools/read.d.ts.map +1 -1
  222. package/dist/core/tools/read.js +34 -1
  223. package/dist/core/tools/read.js.map +1 -1
  224. package/dist/core/verified-bash-adapter.d.ts +36 -0
  225. package/dist/core/verified-bash-adapter.d.ts.map +1 -0
  226. package/dist/core/verified-bash-adapter.js +90 -0
  227. package/dist/core/verified-bash-adapter.js.map +1 -0
  228. package/dist/guardrails/command-redaction.d.ts +32 -0
  229. package/dist/guardrails/command-redaction.d.ts.map +1 -0
  230. package/dist/guardrails/command-redaction.js +776 -0
  231. package/dist/guardrails/command-redaction.js.map +1 -0
  232. package/dist/guardrails/evidence-attestation.d.ts +31 -0
  233. package/dist/guardrails/evidence-attestation.d.ts.map +1 -0
  234. package/dist/guardrails/evidence-attestation.js +178 -0
  235. package/dist/guardrails/evidence-attestation.js.map +1 -0
  236. package/dist/guardrails/evidence-receipt-store.d.ts +33 -0
  237. package/dist/guardrails/evidence-receipt-store.d.ts.map +1 -0
  238. package/dist/guardrails/evidence-receipt-store.js +446 -0
  239. package/dist/guardrails/evidence-receipt-store.js.map +1 -0
  240. package/dist/guardrails/evidence-receipt.d.ts +58 -0
  241. package/dist/guardrails/evidence-receipt.d.ts.map +1 -0
  242. package/dist/guardrails/evidence-receipt.js +389 -0
  243. package/dist/guardrails/evidence-receipt.js.map +1 -0
  244. package/dist/guardrails/evidence-system.d.ts +39 -15
  245. package/dist/guardrails/evidence-system.d.ts.map +1 -1
  246. package/dist/guardrails/evidence-system.js +255 -97
  247. package/dist/guardrails/evidence-system.js.map +1 -1
  248. package/dist/guardrails/replay-ledger-io.d.ts +6 -0
  249. package/dist/guardrails/replay-ledger-io.d.ts.map +1 -0
  250. package/dist/guardrails/replay-ledger-io.js +42 -0
  251. package/dist/guardrails/replay-ledger-io.js.map +1 -0
  252. package/dist/guardrails/replay-ledger-lock-owner.d.ts +32 -0
  253. package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -0
  254. package/dist/guardrails/replay-ledger-lock-owner.js +138 -0
  255. package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -0
  256. package/dist/guardrails/replay-ledger-lock.d.ts +24 -0
  257. package/dist/guardrails/replay-ledger-lock.d.ts.map +1 -0
  258. package/dist/guardrails/replay-ledger-lock.js +135 -0
  259. package/dist/guardrails/replay-ledger-lock.js.map +1 -0
  260. package/dist/guardrails/replay-ledger-mutation-gate.d.ts +24 -0
  261. package/dist/guardrails/replay-ledger-mutation-gate.d.ts.map +1 -0
  262. package/dist/guardrails/replay-ledger-mutation-gate.js +205 -0
  263. package/dist/guardrails/replay-ledger-mutation-gate.js.map +1 -0
  264. package/dist/guardrails/replay-ledger-quarantine.d.ts +2 -0
  265. package/dist/guardrails/replay-ledger-quarantine.d.ts.map +1 -0
  266. package/dist/guardrails/replay-ledger-quarantine.js +19 -0
  267. package/dist/guardrails/replay-ledger-quarantine.js.map +1 -0
  268. package/dist/guardrails/replay-ledger-store.d.ts +32 -0
  269. package/dist/guardrails/replay-ledger-store.d.ts.map +1 -0
  270. package/dist/guardrails/replay-ledger-store.js +211 -0
  271. package/dist/guardrails/replay-ledger-store.js.map +1 -0
  272. package/dist/guardrails/shell-command-name-glob.d.ts +3 -0
  273. package/dist/guardrails/shell-command-name-glob.d.ts.map +1 -0
  274. package/dist/guardrails/shell-command-name-glob.js +233 -0
  275. package/dist/guardrails/shell-command-name-glob.js.map +1 -0
  276. package/dist/guardrails/shell-command-option.d.ts +7 -0
  277. package/dist/guardrails/shell-command-option.d.ts.map +1 -0
  278. package/dist/guardrails/shell-command-option.js +24 -0
  279. package/dist/guardrails/shell-command-option.js.map +1 -0
  280. package/dist/guardrails/shell-command-tokenizer.d.ts +29 -0
  281. package/dist/guardrails/shell-command-tokenizer.d.ts.map +1 -0
  282. package/dist/guardrails/shell-command-tokenizer.js +145 -0
  283. package/dist/guardrails/shell-command-tokenizer.js.map +1 -0
  284. package/dist/guardrails/shell-command-wrapper.d.ts +18 -0
  285. package/dist/guardrails/shell-command-wrapper.d.ts.map +1 -0
  286. package/dist/guardrails/shell-command-wrapper.js +42 -0
  287. package/dist/guardrails/shell-command-wrapper.js.map +1 -0
  288. package/dist/guardrails/verified-executor.d.ts +65 -0
  289. package/dist/guardrails/verified-executor.d.ts.map +1 -0
  290. package/dist/guardrails/verified-executor.js +171 -0
  291. package/dist/guardrails/verified-executor.js.map +1 -0
  292. package/dist/guardrails/workspace-fingerprint.d.ts +23 -0
  293. package/dist/guardrails/workspace-fingerprint.d.ts.map +1 -0
  294. package/dist/guardrails/workspace-fingerprint.js +634 -0
  295. package/dist/guardrails/workspace-fingerprint.js.map +1 -0
  296. package/dist/index.d.ts +15 -3
  297. package/dist/index.d.ts.map +1 -1
  298. package/dist/index.js +13 -1
  299. package/dist/index.js.map +1 -1
  300. package/dist/main.d.ts.map +1 -1
  301. package/dist/main.js +12 -0
  302. package/dist/main.js.map +1 -1
  303. package/dist/modes/interactive/components/footer.d.ts +1 -1
  304. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  305. package/dist/modes/interactive/components/footer.js +11 -8
  306. package/dist/modes/interactive/components/footer.js.map +1 -1
  307. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  308. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  309. package/dist/modes/interactive/interactive-mode.js +59 -9
  310. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  311. package/dist/modes/interactive/theme/omk-aurora-dark.json +80 -0
  312. package/dist/modes/interactive/theme/omk-aurora-light.json +80 -0
  313. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  314. package/dist/modes/interactive/theme/theme.js +5 -0
  315. package/dist/modes/interactive/theme/theme.js.map +1 -1
  316. package/dist/modes/print-mode.d.ts.map +1 -1
  317. package/dist/modes/print-mode.js +43 -2
  318. package/dist/modes/print-mode.js.map +1 -1
  319. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  320. package/dist/modes/rpc/rpc-mode.js +23 -5
  321. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  322. package/dist/modes/rpc/rpc-types.d.ts +3 -0
  323. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  324. package/dist/modes/rpc/rpc-types.js.map +1 -1
  325. package/dist/types/evidence.d.ts +211 -1
  326. package/dist/types/evidence.d.ts.map +1 -1
  327. package/dist/types/evidence.js.map +1 -1
  328. package/dist/verify-ci.d.ts +3 -0
  329. package/dist/verify-ci.d.ts.map +1 -0
  330. package/dist/verify-ci.js +30 -0
  331. package/dist/verify-ci.js.map +1 -0
  332. package/docs/adaptorch-preview.md +1 -1
  333. package/docs/compaction.md +38 -4
  334. package/docs/extensions.md +2 -0
  335. package/docs/provider-resilience.md +87 -0
  336. package/docs/sdk.md +238 -1
  337. package/docs/settings.md +80 -2
  338. package/docs/themes.md +2 -0
  339. package/docs/usage.md +38 -0
  340. package/examples/extensions/custom-provider-anthropic/package-lock.json +5 -5
  341. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  342. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  343. package/examples/extensions/gondolin/package-lock.json +2 -2
  344. package/examples/extensions/gondolin/package.json +1 -1
  345. package/examples/extensions/sandbox/package-lock.json +2 -2
  346. package/examples/extensions/sandbox/package.json +1 -1
  347. package/examples/extensions/with-deps/package-lock.json +4 -4
  348. package/examples/extensions/with-deps/package.json +1 -1
  349. package/npm-shrinkwrap.json +22 -13
  350. package/package.json +6 -5
@@ -0,0 +1,176 @@
1
+ import { acquireDurableFileLockSync, acquireDurableFileMutationLockSync, DurableFileLockBusyError, inspectDurableFileLockSync, resolveDurableFileIdentity, } from "./durable-file-identity.js";
2
+ const OWNER_SCOPE = "session-owner";
3
+ export class SessionOwnerLeaseHeldError extends Error {
4
+ observation;
5
+ constructor(observation) {
6
+ super("Session has a live or indeterminate owner lease; automatic recovery is refused");
7
+ this.name = "SessionOwnerLeaseHeldError";
8
+ this.observation = observation;
9
+ }
10
+ }
11
+ export function sessionPathFromPersistenceArtifact(path) {
12
+ return path.endsWith(".runjournal") ? path.slice(0, -".runjournal".length) : path;
13
+ }
14
+ function identitiesOverlap(left, right) {
15
+ return left.lockKeys.some((key) => right.lockKeys.includes(key));
16
+ }
17
+ export function inspectSessionOwnerLeaseUnlockedSync(path) {
18
+ return inspectDurableFileLockSync(sessionPathFromPersistenceArtifact(path), OWNER_SCOPE);
19
+ }
20
+ export function inspectSessionOwnerLeaseSync(path) {
21
+ const sessionPath = sessionPathFromPersistenceArtifact(path);
22
+ const mutation = acquireDurableFileMutationLockSync(sessionPath);
23
+ try {
24
+ return inspectSessionOwnerLeaseUnlockedSync(sessionPath);
25
+ }
26
+ finally {
27
+ mutation.release();
28
+ }
29
+ }
30
+ export function assertSessionOwnerRecoveryAllowedUnlockedSync(path, ownerLease) {
31
+ const sessionPath = sessionPathFromPersistenceArtifact(path);
32
+ if (ownerLease?.owns(sessionPath))
33
+ return;
34
+ const observation = inspectSessionOwnerLeaseUnlockedSync(sessionPath);
35
+ if (observation.status !== "absent")
36
+ throw new SessionOwnerLeaseHeldError(observation);
37
+ }
38
+ function releaseFailedAcquisition(ownerLock, mutation, failure) {
39
+ const failures = [failure];
40
+ try {
41
+ ownerLock.release();
42
+ }
43
+ catch (error) {
44
+ failures.push(error instanceof Error ? error : new Error(String(error)));
45
+ }
46
+ try {
47
+ mutation.release();
48
+ }
49
+ catch (error) {
50
+ failures.push(error instanceof Error ? error : new Error(String(error)));
51
+ }
52
+ if (failures.length === 1)
53
+ throw failure;
54
+ throw new AggregateError(failures, "Session owner lease acquisition cleanup failed");
55
+ }
56
+ export function acquireSessionOwnerLeaseSync(path) {
57
+ let sessionPath = sessionPathFromPersistenceArtifact(path);
58
+ const mutation = acquireDurableFileMutationLockSync(sessionPath);
59
+ let ownerLock;
60
+ try {
61
+ ownerLock = acquireDurableFileLockSync(sessionPath, OWNER_SCOPE, { timeoutMs: 0 });
62
+ }
63
+ catch (error) {
64
+ const failure = error instanceof DurableFileLockBusyError
65
+ ? new SessionOwnerLeaseHeldError(inspectSessionOwnerLeaseUnlockedSync(sessionPath))
66
+ : error;
67
+ try {
68
+ mutation.release();
69
+ }
70
+ catch (cleanupError) {
71
+ const cleanupFailure = cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError));
72
+ throw new AggregateError([failure, cleanupFailure], "Session owner lease acquisition cleanup failed");
73
+ }
74
+ throw failure;
75
+ }
76
+ try {
77
+ mutation.release();
78
+ }
79
+ catch (error) {
80
+ releaseFailedAcquisition(ownerLock, mutation, error instanceof Error ? error : new Error(String(error)));
81
+ }
82
+ let ownedPath = sessionPath;
83
+ let identity = ownerLock.identity;
84
+ let pendingMutation;
85
+ let released = false;
86
+ function drainPendingMutation() {
87
+ if (!pendingMutation)
88
+ return;
89
+ pendingMutation.release();
90
+ pendingMutation = undefined;
91
+ }
92
+ const lease = {
93
+ get sessionPath() {
94
+ return sessionPath;
95
+ },
96
+ get identity() {
97
+ return identity;
98
+ },
99
+ get released() {
100
+ return released;
101
+ },
102
+ owns(candidate) {
103
+ return !released && identitiesOverlap(identity, resolveDurableFileIdentity(candidate));
104
+ },
105
+ refresh(mutationLockHeld = false) {
106
+ lease.transfer(sessionPath, mutationLockHeld);
107
+ },
108
+ transfer(path, mutationLockHeld = false) {
109
+ if (released)
110
+ throw new TypeError("Cannot transfer a released session owner lease");
111
+ const nextPath = sessionPathFromPersistenceArtifact(path);
112
+ drainPendingMutation();
113
+ if (ownedPath === nextPath && sessionPath !== nextPath) {
114
+ sessionPath = nextPath;
115
+ identity = ownerLock.identity;
116
+ return;
117
+ }
118
+ const nextMutation = mutationLockHeld ? undefined : acquireDurableFileMutationLockSync(nextPath);
119
+ let failure;
120
+ try {
121
+ ownerLock.transfer(nextPath, { timeoutMs: 0 });
122
+ ownedPath = nextPath;
123
+ }
124
+ catch (error) {
125
+ failure =
126
+ error instanceof DurableFileLockBusyError
127
+ ? new SessionOwnerLeaseHeldError(inspectSessionOwnerLeaseUnlockedSync(nextPath))
128
+ : error;
129
+ }
130
+ if (nextMutation) {
131
+ try {
132
+ nextMutation.release();
133
+ }
134
+ catch (error) {
135
+ pendingMutation = nextMutation;
136
+ const cleanupFailure = error instanceof Error ? error : new Error(String(error));
137
+ failure = failure
138
+ ? new AggregateError([failure, cleanupFailure], "Session owner lease transfer failed")
139
+ : cleanupFailure;
140
+ }
141
+ }
142
+ if (failure)
143
+ throw failure;
144
+ sessionPath = nextPath;
145
+ identity = ownerLock.identity;
146
+ },
147
+ release() {
148
+ if (released)
149
+ return;
150
+ drainPendingMutation();
151
+ const releaseLock = acquireDurableFileMutationLockSync(ownedPath);
152
+ let failure;
153
+ try {
154
+ ownerLock.release();
155
+ }
156
+ catch (error) {
157
+ failure = error instanceof Error ? error : new Error(String(error));
158
+ }
159
+ try {
160
+ releaseLock.release();
161
+ }
162
+ catch (error) {
163
+ pendingMutation = releaseLock;
164
+ const cleanupFailure = error instanceof Error ? error : new Error(String(error));
165
+ failure = failure
166
+ ? new AggregateError([failure, cleanupFailure], "Session owner lease release failed")
167
+ : cleanupFailure;
168
+ }
169
+ if (failure)
170
+ throw failure;
171
+ released = true;
172
+ },
173
+ };
174
+ return lease;
175
+ }
176
+ //# sourceMappingURL=session-owner-lease.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-owner-lease.js","sourceRoot":"","sources":["../../src/core/session-owner-lease.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tacquireDurableFileLockSync,\n\tacquireDurableFileMutationLockSync,\n\ttype DurableFileIdentity,\n\ttype DurableFileLock,\n\tDurableFileLockBusyError,\n\ttype DurableFileLockObservation,\n\tinspectDurableFileLockSync,\n\tresolveDurableFileIdentity,\n} from \"./durable-file-identity.ts\";\n\nconst OWNER_SCOPE = \"session-owner\";\n\nexport interface SessionOwnerLease {\n\treadonly sessionPath: string;\n\treadonly identity: DurableFileIdentity;\n\treadonly released: boolean;\n\towns(path: string): boolean;\n\t/** Refresh inode aliases after the owner creates or atomically replaces the session file. */\n\trefresh(mutationLockHeld?: boolean): void;\n\t/** Transfer ownership without releasing the current path unless the new path is acquired. */\n\ttransfer(path: string, mutationLockHeld?: boolean): void;\n\trelease(): void;\n}\n\nexport class SessionOwnerLeaseHeldError extends Error {\n\treadonly observation: DurableFileLockObservation;\n\n\tconstructor(observation: DurableFileLockObservation) {\n\t\tsuper(\"Session has a live or indeterminate owner lease; automatic recovery is refused\");\n\t\tthis.name = \"SessionOwnerLeaseHeldError\";\n\t\tthis.observation = observation;\n\t}\n}\n\nexport function sessionPathFromPersistenceArtifact(path: string): string {\n\treturn path.endsWith(\".runjournal\") ? path.slice(0, -\".runjournal\".length) : path;\n}\n\nfunction identitiesOverlap(left: DurableFileIdentity, right: DurableFileIdentity): boolean {\n\treturn left.lockKeys.some((key) => right.lockKeys.includes(key));\n}\n\nexport function inspectSessionOwnerLeaseUnlockedSync(path: string): DurableFileLockObservation {\n\treturn inspectDurableFileLockSync(sessionPathFromPersistenceArtifact(path), OWNER_SCOPE);\n}\n\nexport function inspectSessionOwnerLeaseSync(path: string): DurableFileLockObservation {\n\tconst sessionPath = sessionPathFromPersistenceArtifact(path);\n\tconst mutation = acquireDurableFileMutationLockSync(sessionPath);\n\ttry {\n\t\treturn inspectSessionOwnerLeaseUnlockedSync(sessionPath);\n\t} finally {\n\t\tmutation.release();\n\t}\n}\n\nexport function assertSessionOwnerRecoveryAllowedUnlockedSync(path: string, ownerLease?: SessionOwnerLease): void {\n\tconst sessionPath = sessionPathFromPersistenceArtifact(path);\n\tif (ownerLease?.owns(sessionPath)) return;\n\tconst observation = inspectSessionOwnerLeaseUnlockedSync(sessionPath);\n\tif (observation.status !== \"absent\") throw new SessionOwnerLeaseHeldError(observation);\n}\n\nfunction releaseFailedAcquisition(ownerLock: DurableFileLock, mutation: DurableFileLock, failure: Error): never {\n\tconst failures = [failure];\n\ttry {\n\t\townerLock.release();\n\t} catch (error) {\n\t\tfailures.push(error instanceof Error ? error : new Error(String(error)));\n\t}\n\ttry {\n\t\tmutation.release();\n\t} catch (error) {\n\t\tfailures.push(error instanceof Error ? error : new Error(String(error)));\n\t}\n\tif (failures.length === 1) throw failure;\n\tthrow new AggregateError(failures, \"Session owner lease acquisition cleanup failed\");\n}\n\nexport function acquireSessionOwnerLeaseSync(path: string): SessionOwnerLease {\n\tlet sessionPath = sessionPathFromPersistenceArtifact(path);\n\tconst mutation = acquireDurableFileMutationLockSync(sessionPath);\n\tlet ownerLock: DurableFileLock;\n\ttry {\n\t\townerLock = acquireDurableFileLockSync(sessionPath, OWNER_SCOPE, { timeoutMs: 0 });\n\t} catch (error) {\n\t\tconst failure =\n\t\t\terror instanceof DurableFileLockBusyError\n\t\t\t\t? new SessionOwnerLeaseHeldError(inspectSessionOwnerLeaseUnlockedSync(sessionPath))\n\t\t\t\t: error;\n\t\ttry {\n\t\t\tmutation.release();\n\t\t} catch (cleanupError) {\n\t\t\tconst cleanupFailure = cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError));\n\t\t\tthrow new AggregateError([failure, cleanupFailure], \"Session owner lease acquisition cleanup failed\");\n\t\t}\n\t\tthrow failure;\n\t}\n\ttry {\n\t\tmutation.release();\n\t} catch (error) {\n\t\treleaseFailedAcquisition(ownerLock, mutation, error instanceof Error ? error : new Error(String(error)));\n\t}\n\n\tlet ownedPath = sessionPath;\n\tlet identity = ownerLock.identity;\n\tlet pendingMutation: DurableFileLock | undefined;\n\tlet released = false;\n\n\tfunction drainPendingMutation(): void {\n\t\tif (!pendingMutation) return;\n\t\tpendingMutation.release();\n\t\tpendingMutation = undefined;\n\t}\n\n\tconst lease: SessionOwnerLease = {\n\t\tget sessionPath() {\n\t\t\treturn sessionPath;\n\t\t},\n\t\tget identity() {\n\t\t\treturn identity;\n\t\t},\n\t\tget released() {\n\t\t\treturn released;\n\t\t},\n\t\towns(candidate: string): boolean {\n\t\t\treturn !released && identitiesOverlap(identity, resolveDurableFileIdentity(candidate));\n\t\t},\n\t\trefresh(mutationLockHeld = false): void {\n\t\t\tlease.transfer(sessionPath, mutationLockHeld);\n\t\t},\n\t\ttransfer(path, mutationLockHeld = false): void {\n\t\t\tif (released) throw new TypeError(\"Cannot transfer a released session owner lease\");\n\t\t\tconst nextPath = sessionPathFromPersistenceArtifact(path);\n\t\t\tdrainPendingMutation();\n\t\t\tif (ownedPath === nextPath && sessionPath !== nextPath) {\n\t\t\t\tsessionPath = nextPath;\n\t\t\t\tidentity = ownerLock.identity;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst nextMutation = mutationLockHeld ? undefined : acquireDurableFileMutationLockSync(nextPath);\n\t\t\tlet failure: unknown;\n\t\t\ttry {\n\t\t\t\townerLock.transfer(nextPath, { timeoutMs: 0 });\n\t\t\t\townedPath = nextPath;\n\t\t\t} catch (error) {\n\t\t\t\tfailure =\n\t\t\t\t\terror instanceof DurableFileLockBusyError\n\t\t\t\t\t\t? new SessionOwnerLeaseHeldError(inspectSessionOwnerLeaseUnlockedSync(nextPath))\n\t\t\t\t\t\t: error;\n\t\t\t}\n\t\t\tif (nextMutation) {\n\t\t\t\ttry {\n\t\t\t\t\tnextMutation.release();\n\t\t\t\t} catch (error) {\n\t\t\t\t\tpendingMutation = nextMutation;\n\t\t\t\t\tconst cleanupFailure = error instanceof Error ? error : new Error(String(error));\n\t\t\t\t\tfailure = failure\n\t\t\t\t\t\t? new AggregateError([failure, cleanupFailure], \"Session owner lease transfer failed\")\n\t\t\t\t\t\t: cleanupFailure;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (failure) throw failure;\n\t\t\tsessionPath = nextPath;\n\t\t\tidentity = ownerLock.identity;\n\t\t},\n\t\trelease(): void {\n\t\t\tif (released) return;\n\t\t\tdrainPendingMutation();\n\t\t\tconst releaseLock = acquireDurableFileMutationLockSync(ownedPath);\n\t\t\tlet failure: unknown;\n\t\t\ttry {\n\t\t\t\townerLock.release();\n\t\t\t} catch (error) {\n\t\t\t\tfailure = error instanceof Error ? error : new Error(String(error));\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treleaseLock.release();\n\t\t\t} catch (error) {\n\t\t\t\tpendingMutation = releaseLock;\n\t\t\t\tconst cleanupFailure = error instanceof Error ? error : new Error(String(error));\n\t\t\t\tfailure = failure\n\t\t\t\t\t? new AggregateError([failure, cleanupFailure], \"Session owner lease release failed\")\n\t\t\t\t\t: cleanupFailure;\n\t\t\t}\n\t\t\tif (failure) throw failure;\n\t\t\treleased = true;\n\t\t},\n\t};\n\treturn lease;\n}\n"]}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Pure, browser-safe session path security policy (task I34).
3
+ *
4
+ * This module is deliberately dependency-free: no imports, no node:/process/
5
+ * fs/path/Buffer, no timers, no Date.now, no Math.random, no crypto, and no I/O
6
+ * of any kind. Every decision is derived from plain-data evidence that the
7
+ * caller attests. The policy never touches the filesystem and never executes a
8
+ * repair; it only authorizes, denies, or describes what a caller may do.
9
+ *
10
+ * Inputs are validated strictly (plain data, exact keys, accessor-free) and the
11
+ * returned decision is a deep-frozen copy built from primitives, so a caller
12
+ * cannot mutate the decision's internals by mutating the evidence afterwards.
13
+ */
14
+ declare const SESSION_PATH_POLICY_SCHEMA_VERSION: 1;
15
+ /** Maximum number of entries accepted in an evidence chain (DoS guardrail). */
16
+ export declare const MAX_SESSION_PATH_CHAIN_ENTRIES = 256;
17
+ export type SessionPathPlatform = "posix" | "win32";
18
+ export type SessionPathIntent = "inspect_metadata" | "inspect_content" | "repair" | "repair_dry_run";
19
+ export type SessionPathLinkKind = "none" | "symlink" | "reparse";
20
+ export type SessionPathLockState = "absent" | "live" | "stale" | "foreign" | "unknown";
21
+ export type SessionPathReason = "authorized" | "metadata_only" | "lexical_invalid" | "evidence_mismatch" | "target_external" | "target_is_root" | "identity_required" | "lock_required" | "symlink_in_chain" | "outside_realpath" | "nonregular" | "nlink_not_one" | "owner_mismatch" | "stat_race" | "stat_after_mismatch" | "lock_live" | "lock_foreign" | "lock_unknown" | "lock_stale_ineligible";
22
+ export type SessionPathStatus = "authorized" | "metadata_only" | "rejected" | "blocked";
23
+ export type SessionPathClassification = "root" | "inside" | "external";
24
+ export interface SessionPathIdentity {
25
+ readonly owner: string;
26
+ }
27
+ export interface SessionPathStat {
28
+ readonly dev: string;
29
+ readonly ino: string;
30
+ readonly nlink: number;
31
+ readonly size: number;
32
+ readonly mtime: number;
33
+ readonly regular: boolean;
34
+ readonly owner: string;
35
+ }
36
+ export interface SessionPathChainEntry {
37
+ readonly lexical: string;
38
+ readonly realpath: string;
39
+ readonly linkKind: SessionPathLinkKind;
40
+ }
41
+ export interface SessionPathTarget {
42
+ readonly lexical: string;
43
+ readonly realpath: string;
44
+ }
45
+ export interface SessionPathOpened {
46
+ readonly dev: string;
47
+ readonly ino: string;
48
+ }
49
+ export interface SessionPathEvidence {
50
+ readonly schemaVersion: 1;
51
+ readonly platform: SessionPathPlatform;
52
+ readonly trustedRootLexical: string;
53
+ readonly trustedRootRealpath: string;
54
+ readonly target: SessionPathTarget;
55
+ readonly chain: readonly SessionPathChainEntry[];
56
+ readonly statBefore: SessionPathStat;
57
+ readonly statAfter: SessionPathStat | null;
58
+ readonly opened: SessionPathOpened;
59
+ }
60
+ export interface SessionPathLockEvidence {
61
+ readonly state: SessionPathLockState;
62
+ readonly sameHost: boolean;
63
+ readonly pidDefinitelyAbsent: boolean;
64
+ readonly holderPid: string | null;
65
+ }
66
+ export type SessionPathPlannedAction = {
67
+ readonly kind: "remove_stale_lock";
68
+ readonly holderPid: string | null;
69
+ };
70
+ export interface SessionPathCapabilities {
71
+ readonly canInspectMetadata: boolean;
72
+ readonly canReadContents: boolean;
73
+ readonly canRepair: boolean;
74
+ }
75
+ export interface SessionPathDecision {
76
+ readonly schemaVersion: typeof SESSION_PATH_POLICY_SCHEMA_VERSION;
77
+ readonly platform: SessionPathPlatform;
78
+ readonly intent: SessionPathIntent;
79
+ readonly status: SessionPathStatus;
80
+ readonly reason: SessionPathReason;
81
+ readonly dryRun: boolean;
82
+ readonly classification: SessionPathClassification;
83
+ readonly targetLexical: string;
84
+ readonly targetRealpath: string;
85
+ readonly trustedRootLexical: string;
86
+ readonly trustedRootRealpath: string;
87
+ readonly capabilities: SessionPathCapabilities;
88
+ readonly plannedActions: readonly SessionPathPlannedAction[];
89
+ readonly scheduledWrites: number;
90
+ }
91
+ export interface SessionPathAccessInput {
92
+ readonly platform: SessionPathPlatform;
93
+ readonly root: string;
94
+ readonly target: string;
95
+ readonly intent: SessionPathIntent;
96
+ readonly identity?: SessionPathIdentity;
97
+ readonly evidence: SessionPathEvidence;
98
+ readonly lock?: SessionPathLockEvidence;
99
+ }
100
+ /**
101
+ * Canonicalize a session path lexically for the given platform without any
102
+ * filesystem access. Returns null (fail-closed) for any disallowed form:
103
+ * non-string, empty, longer than 4096, C0/DEL control characters, POSIX
104
+ * non-absolute or above-root `..`, or any Win32 hazard (namespace prefixes,
105
+ * UNC, drive-relative, non-absolute, ADS colons, trailing dot/space, reserved
106
+ * device names, above-drive-root escape). Drive letters are lower-cased.
107
+ */
108
+ export declare function canonicalizeSessionPath(raw: unknown, platform: SessionPathPlatform): string | null;
109
+ /**
110
+ * True when `candidate` is within `root` at a segment boundary (root may equal
111
+ * candidate). Win32 compares segments case-insensitively; POSIX is exact.
112
+ * Cross-drive and any non-canonical input return false (fail-closed).
113
+ */
114
+ export declare function sessionPathContains(root: unknown, candidate: unknown, platform: SessionPathPlatform): boolean;
115
+ /**
116
+ * Decide session path access from attested plain-data evidence. Pure: never
117
+ * performs I/O and never executes a repair. The returned decision is a
118
+ * deep-frozen copy.
119
+ *
120
+ * Classification precedence (P0): the lexical classification of the request
121
+ * root/target is computed first and is authoritative for privileged intents.
122
+ * `inspect_metadata` always resolves to metadata_only after lexical/evidence
123
+ * binding. For `inspect_content`/`repair`/`repair_dry_run` the lexical target
124
+ * must be strictly inside the root (external => target_external, root =>
125
+ * target_is_root) before identity, and the realpath classification must
126
+ * independently be inside; an injected realpath can never upgrade an external
127
+ * lexical target. `repair_dry_run` shares repair's eligibility, status, reason,
128
+ * and planned actions, but reports canRepair=false and zero scheduled writes.
129
+ */
130
+ export declare function decideSessionPathAccess(rawInput: unknown): SessionPathDecision;
131
+ export {};
132
+ //# sourceMappingURL=session-path-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-path-policy.d.ts","sourceRoot":"","sources":["../../src/core/session-path-policy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure, browser-safe session path security policy (task I34).\n *\n * This module is deliberately dependency-free: no imports, no node:/process/\n * fs/path/Buffer, no timers, no Date.now, no Math.random, no crypto, and no I/O\n * of any kind. Every decision is derived from plain-data evidence that the\n * caller attests. The policy never touches the filesystem and never executes a\n * repair; it only authorizes, denies, or describes what a caller may do.\n *\n * Inputs are validated strictly (plain data, exact keys, accessor-free) and the\n * returned decision is a deep-frozen copy built from primitives, so a caller\n * cannot mutate the decision's internals by mutating the evidence afterwards.\n */\n\nconst SESSION_PATH_POLICY_SCHEMA_VERSION = 1 as const;\nconst SESSION_PATH_MAX_LENGTH = 4096;\n/** Maximum number of entries accepted in an evidence chain (DoS guardrail). */\nexport const MAX_SESSION_PATH_CHAIN_ENTRIES = 256;\nconst CONTROL_CHAR_RE = /[\\u0000-\\u001f\\u007f]/;\nconst CANONICAL_DECIMAL_RE = /^(0|[1-9]\\d{0,19})$/;\nconst WIN_DRIVE_ABSOLUTE_RE = /^([A-Za-z]):\\/+(.*)$/;\nconst WIN_CANONICAL_RE = /^([a-z]):\\/(.*)$/;\n\nexport type SessionPathPlatform = \"posix\" | \"win32\";\nexport type SessionPathIntent = \"inspect_metadata\" | \"inspect_content\" | \"repair\" | \"repair_dry_run\";\nexport type SessionPathLinkKind = \"none\" | \"symlink\" | \"reparse\";\nexport type SessionPathLockState = \"absent\" | \"live\" | \"stale\" | \"foreign\" | \"unknown\";\n\nexport type SessionPathReason =\n\t| \"authorized\"\n\t| \"metadata_only\"\n\t| \"lexical_invalid\"\n\t| \"evidence_mismatch\"\n\t| \"target_external\"\n\t| \"target_is_root\"\n\t| \"identity_required\"\n\t| \"lock_required\"\n\t| \"symlink_in_chain\"\n\t| \"outside_realpath\"\n\t| \"nonregular\"\n\t| \"nlink_not_one\"\n\t| \"owner_mismatch\"\n\t| \"stat_race\"\n\t| \"stat_after_mismatch\"\n\t| \"lock_live\"\n\t| \"lock_foreign\"\n\t| \"lock_unknown\"\n\t| \"lock_stale_ineligible\";\n\nexport type SessionPathStatus = \"authorized\" | \"metadata_only\" | \"rejected\" | \"blocked\";\nexport type SessionPathClassification = \"root\" | \"inside\" | \"external\";\n\nexport interface SessionPathIdentity {\n\treadonly owner: string;\n}\n\nexport interface SessionPathStat {\n\treadonly dev: string;\n\treadonly ino: string;\n\treadonly nlink: number;\n\treadonly size: number;\n\treadonly mtime: number;\n\treadonly regular: boolean;\n\treadonly owner: string;\n}\n\nexport interface SessionPathChainEntry {\n\treadonly lexical: string;\n\treadonly realpath: string;\n\treadonly linkKind: SessionPathLinkKind;\n}\n\nexport interface SessionPathTarget {\n\treadonly lexical: string;\n\treadonly realpath: string;\n}\n\nexport interface SessionPathOpened {\n\treadonly dev: string;\n\treadonly ino: string;\n}\n\nexport interface SessionPathEvidence {\n\treadonly schemaVersion: 1;\n\treadonly platform: SessionPathPlatform;\n\treadonly trustedRootLexical: string;\n\treadonly trustedRootRealpath: string;\n\treadonly target: SessionPathTarget;\n\treadonly chain: readonly SessionPathChainEntry[];\n\treadonly statBefore: SessionPathStat;\n\treadonly statAfter: SessionPathStat | null;\n\treadonly opened: SessionPathOpened;\n}\n\nexport interface SessionPathLockEvidence {\n\treadonly state: SessionPathLockState;\n\treadonly sameHost: boolean;\n\treadonly pidDefinitelyAbsent: boolean;\n\treadonly holderPid: string | null;\n}\n\nexport type SessionPathPlannedAction = {\n\treadonly kind: \"remove_stale_lock\";\n\treadonly holderPid: string | null;\n};\n\nexport interface SessionPathCapabilities {\n\treadonly canInspectMetadata: boolean;\n\treadonly canReadContents: boolean;\n\treadonly canRepair: boolean;\n}\n\nexport interface SessionPathDecision {\n\treadonly schemaVersion: typeof SESSION_PATH_POLICY_SCHEMA_VERSION;\n\treadonly platform: SessionPathPlatform;\n\treadonly intent: SessionPathIntent;\n\treadonly status: SessionPathStatus;\n\treadonly reason: SessionPathReason;\n\treadonly dryRun: boolean;\n\treadonly classification: SessionPathClassification;\n\treadonly targetLexical: string;\n\treadonly targetRealpath: string;\n\treadonly trustedRootLexical: string;\n\treadonly trustedRootRealpath: string;\n\treadonly capabilities: SessionPathCapabilities;\n\treadonly plannedActions: readonly SessionPathPlannedAction[];\n\treadonly scheduledWrites: number;\n}\n\nexport interface SessionPathAccessInput {\n\treadonly platform: SessionPathPlatform;\n\treadonly root: string;\n\treadonly target: string;\n\treadonly intent: SessionPathIntent;\n\treadonly identity?: SessionPathIdentity;\n\treadonly evidence: SessionPathEvidence;\n\treadonly lock?: SessionPathLockEvidence;\n}\n\nconst PLATFORMS = new Set<SessionPathPlatform>([\"posix\", \"win32\"]);\nconst INTENTS = new Set<SessionPathIntent>([\"inspect_metadata\", \"inspect_content\", \"repair\", \"repair_dry_run\"]);\nconst LINK_KINDS = new Set<SessionPathLinkKind>([\"none\", \"symlink\", \"reparse\"]);\nconst LOCK_STATES = new Set<SessionPathLockState>([\"absent\", \"live\", \"stale\", \"foreign\", \"unknown\"]);\n\nconst WIN_RESERVED_NAMES = (() => {\n\tconst set = new Set<string>();\n\tset.add(\"con\");\n\tset.add(\"prn\");\n\tset.add(\"aux\");\n\tset.add(\"nul\");\n\tfor (let i = 1; i <= 9; i++) {\n\t\tset.add(`com${i}`);\n\t\tset.add(`lpt${i}`);\n\t}\n\treturn set;\n})();\n\nconst EVIDENCE_KEYS = [\n\t\"schemaVersion\",\n\t\"platform\",\n\t\"trustedRootLexical\",\n\t\"trustedRootRealpath\",\n\t\"target\",\n\t\"chain\",\n\t\"statBefore\",\n\t\"statAfter\",\n\t\"opened\",\n] as const;\nconst STAT_KEYS = [\"dev\", \"ino\", \"nlink\", \"size\", \"mtime\", \"regular\", \"owner\"] as const;\nconst TARGET_KEYS = [\"lexical\", \"realpath\"] as const;\nconst OPENED_KEYS = [\"dev\", \"ino\"] as const;\nconst CHAIN_ENTRY_KEYS = [\"lexical\", \"realpath\", \"linkKind\"] as const;\nconst LOCK_KEYS = [\"state\", \"sameHost\", \"pidDefinitelyAbsent\", \"holderPid\"] as const;\nconst INPUT_ALLOWED_KEYS = new Set([\"platform\", \"root\", \"target\", \"intent\", \"identity\", \"evidence\", \"lock\"]);\n\n// -------------------------------------------------------------------------------------------------\n// Generic plain-data / exact-key / accessor-free guards.\n// -------------------------------------------------------------------------------------------------\n\nfunction isPlainDataObject(value: unknown): value is Record<string, unknown> {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n\tconst proto = Object.getPrototypeOf(value);\n\tif (proto !== null && proto !== Object.prototype) return false;\n\tif (Object.getOwnPropertySymbols(value).length > 0) return false;\n\tfor (const key of Object.keys(value)) {\n\t\tconst desc = Object.getOwnPropertyDescriptor(value, key);\n\t\tif (desc === undefined || desc.get !== undefined || desc.set !== undefined || !(\"value\" in desc)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\nfunction hasExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {\n\tconst present = Object.keys(value);\n\tif (present.length !== keys.length) return false;\n\tfor (const key of keys) {\n\t\tif (!Object.hasOwn(value, key)) return false;\n\t}\n\treturn true;\n}\n\nfunction isCanonicalDecimal(value: unknown): value is string {\n\treturn typeof value === \"string\" && CANONICAL_DECIMAL_RE.test(value);\n}\n\nfunction isSafeNonNegativeInteger(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isFinite(value) && Number.isSafeInteger(value) && (value as number) >= 0;\n}\n\nfunction deepFreeze<T>(value: T): T {\n\tif (typeof value !== \"object\" || value === null || Object.isFrozen(value)) return value;\n\tif (Array.isArray(value)) {\n\t\tfor (const item of value) deepFreeze(item);\n\t} else {\n\t\tfor (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child);\n\t}\n\treturn Object.freeze(value);\n}\n\nfunction assertPlainObject(value: unknown, label: string): asserts value is Record<string, unknown> {\n\tif (!isPlainDataObject(value)) {\n\t\tthrow new TypeError(`${label} must be a plain data object without accessors or a foreign prototype`);\n\t}\n}\n\nfunction assertExactKeys(value: Record<string, unknown>, keys: readonly string[], label: string): void {\n\tif (!hasExactKeys(value, keys)) {\n\t\tthrow new TypeError(`${label} must have exactly the keys: ${keys.join(\", \")}`);\n\t}\n}\n\nfunction assertString(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\") throw new TypeError(`${label} must be a string`);\n}\n\nfunction assertBoolean(value: unknown, label: string): asserts value is boolean {\n\tif (typeof value !== \"boolean\") throw new TypeError(`${label} must be a boolean`);\n}\n\nfunction assertCanonicalDecimal(value: unknown, label: string): asserts value is string {\n\tif (!isCanonicalDecimal(value)) {\n\t\tthrow new TypeError(\n\t\t\t`${label} must be a canonical decimal string (\"0\" or a nonzero decimal without leading zeros, up to 20 digits)`,\n\t\t);\n\t}\n}\n\nfunction assertSafeNonNegInt(value: unknown, label: string): asserts value is number {\n\tif (!isSafeNonNegativeInteger(value)) throw new TypeError(`${label} must be a finite safe non-negative integer`);\n}\n\n/**\n * Assert a path-shaped evidence string: a string within the 4096 cap and free\n * of C0/DEL control characters. This caps every lexical/realpath evidence\n * string at validation time (fail-closed) before any canonicalization runs.\n */\nfunction assertPathString(value: unknown, label: string): asserts value is string {\n\tif (typeof value !== \"string\") throw new TypeError(`${label} must be a string`);\n\tif (value.length > SESSION_PATH_MAX_LENGTH) {\n\t\tthrow new TypeError(`${label} exceeds the ${SESSION_PATH_MAX_LENGTH} character cap`);\n\t}\n\tif (CONTROL_CHAR_RE.test(value)) throw new TypeError(`${label} must not contain C0/DEL control characters`);\n}\n\n/**\n * Strictly validate a plain dense array before any indexed value access: it\n * must be a real Array whose prototype is exactly Array.prototype, carry no\n * symbol properties or extra own properties, and expose every index 0..length-1\n * as an own data descriptor (no holes, no getters/setters). Accessor indices are\n * rejected without their getters ever executing. The length is captured once.\n */\nfunction assertStrictPlainArray(value: unknown, label: string): asserts value is unknown[] {\n\tif (typeof value !== \"object\" || value === null || !Array.isArray(value)) {\n\t\tthrow new TypeError(`${label} must be an array`);\n\t}\n\tif (Object.getPrototypeOf(value) !== Array.prototype) {\n\t\tthrow new TypeError(`${label} must use the Array prototype`);\n\t}\n\tif (Object.getOwnPropertySymbols(value).length > 0) {\n\t\tthrow new TypeError(`${label} must not carry symbol properties`);\n\t}\n\tconst length = value.length;\n\tfor (const name of Object.getOwnPropertyNames(value)) {\n\t\tif (name === \"length\") continue;\n\t\tconst index = Number(name);\n\t\tif (!Number.isSafeInteger(index) || index < 0 || index >= length || String(index) !== name) {\n\t\t\tthrow new TypeError(`${label} must not carry extra own properties`);\n\t\t}\n\t}\n\tfor (let i = 0; i < length; i++) {\n\t\tconst descriptor = Object.getOwnPropertyDescriptor(value, String(i));\n\t\tif (descriptor === undefined) throw new TypeError(`${label} must be dense (no holes)`);\n\t\tif (descriptor.get !== undefined || descriptor.set !== undefined || !(\"value\" in descriptor)) {\n\t\t\tthrow new TypeError(`${label} must not expose accessor or non-data element descriptors`);\n\t\t}\n\t}\n}\n\n// -------------------------------------------------------------------------------------------------\n// Stage 1: pure lexical canonicalizer + segment-boundary containment.\n// -------------------------------------------------------------------------------------------------\n\nfunction splitSegments(value: string): string[] {\n\treturn value.split(\"/\").filter((segment) => segment.length > 0);\n}\n\n/**\n * Canonicalize a session path lexically for the given platform without any\n * filesystem access. Returns null (fail-closed) for any disallowed form:\n * non-string, empty, longer than 4096, C0/DEL control characters, POSIX\n * non-absolute or above-root `..`, or any Win32 hazard (namespace prefixes,\n * UNC, drive-relative, non-absolute, ADS colons, trailing dot/space, reserved\n * device names, above-drive-root escape). Drive letters are lower-cased.\n */\nexport function canonicalizeSessionPath(raw: unknown, platform: SessionPathPlatform): string | null {\n\tif (typeof raw !== \"string\") return null;\n\tif (raw.length === 0 || raw.length > SESSION_PATH_MAX_LENGTH) return null;\n\tif (CONTROL_CHAR_RE.test(raw)) return null;\n\tif (platform === \"posix\") return canonicalizePosix(raw);\n\tif (platform === \"win32\") return canonicalizeWin32(raw);\n\treturn null;\n}\n\nfunction canonicalizePosix(raw: string): string | null {\n\tif (raw.charCodeAt(0) !== 47 /* '/' */) return null;\n\tconst collapsed: string[] = [];\n\tfor (const segment of splitSegments(raw)) {\n\t\tif (segment === \".\") continue;\n\t\tif (segment === \"..\") {\n\t\t\tif (collapsed.length === 0) return null;\n\t\t\tcollapsed.pop();\n\t\t\tcontinue;\n\t\t}\n\t\tcollapsed.push(segment);\n\t}\n\treturn `/${collapsed.join(\"/\")}`;\n}\n\nfunction canonicalizeWin32(raw: string): string | null {\n\tconst normalized = raw.replace(/\\\\/g, \"/\");\n\tif (normalized.startsWith(\"//?/\") || normalized.startsWith(\"//./\")) return null;\n\tif (normalized.startsWith(\"//\")) return null;\n\tconst match = WIN_DRIVE_ABSOLUTE_RE.exec(normalized);\n\tif (match === null) return null;\n\tconst drive = match[1].toLowerCase();\n\tconst collapsed: string[] = [];\n\tfor (const segment of splitSegments(match[2])) {\n\t\tif (segment === \".\") continue;\n\t\tif (segment === \"..\") {\n\t\t\tif (collapsed.length === 0) return null;\n\t\t\tcollapsed.pop();\n\t\t\tcontinue;\n\t\t}\n\t\tif (segment.includes(\":\")) return null;\n\t\tif (segment.endsWith(\".\") || segment.endsWith(\" \")) return null;\n\t\tconst stem = segment.split(\".\", 1)[0].toLowerCase();\n\t\tif (WIN_RESERVED_NAMES.has(stem)) return null;\n\t\tcollapsed.push(segment);\n\t}\n\treturn `${drive}:/${collapsed.join(\"/\")}`;\n}\n\ninterface CanonicalParts {\n\treadonly drive: string | null;\n\treadonly segments: readonly string[];\n}\n\nfunction parseCanonical(canonical: string, platform: SessionPathPlatform): CanonicalParts | null {\n\tif (platform === \"posix\") {\n\t\tif (canonical.charCodeAt(0) !== 47) return null;\n\t\treturn { drive: null, segments: splitSegments(canonical) };\n\t}\n\tif (platform === \"win32\") {\n\t\tconst match = WIN_CANONICAL_RE.exec(canonical);\n\t\tif (match === null) return null;\n\t\treturn { drive: match[1], segments: splitSegments(match[2]) };\n\t}\n\treturn null;\n}\n\nfunction segmentEqual(left: string, right: string, platform: SessionPathPlatform): boolean {\n\treturn platform === \"win32\" ? left.toLowerCase() === right.toLowerCase() : left === right;\n}\n\nfunction partsContain(root: CanonicalParts, candidate: CanonicalParts, platform: SessionPathPlatform): boolean {\n\tif (root.drive !== candidate.drive) return false;\n\tif (candidate.segments.length < root.segments.length) return false;\n\tfor (let i = 0; i < root.segments.length; i++) {\n\t\tif (!segmentEqual(root.segments[i], candidate.segments[i], platform)) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Canonical path equality: compares drive and every segment using platform\n * semantics (win32 case-insensitive, posix exact). Returns false if either\n * input is null or non-canonical. Unlike a raw string compare, two win32 paths\n * that differ only in segment case are equal here, so mixed-case evidence binds\n * correctly while the canonical output still preserves segment case.\n */\nfunction canonicalPathEqual(a: string | null, b: string | null, platform: SessionPathPlatform): boolean {\n\tif (a === null || b === null) return false;\n\tconst aParts = parseCanonical(a, platform);\n\tconst bParts = parseCanonical(b, platform);\n\tif (aParts === null || bParts === null) return false;\n\tif (aParts.drive !== bParts.drive) return false;\n\tif (aParts.segments.length !== bParts.segments.length) return false;\n\tfor (let i = 0; i < aParts.segments.length; i++) {\n\t\tif (!segmentEqual(aParts.segments[i], bParts.segments[i], platform)) return false;\n\t}\n\treturn true;\n}\n\n/**\n * True when `candidate` is within `root` at a segment boundary (root may equal\n * candidate). Win32 compares segments case-insensitively; POSIX is exact.\n * Cross-drive and any non-canonical input return false (fail-closed).\n */\nexport function sessionPathContains(root: unknown, candidate: unknown, platform: SessionPathPlatform): boolean {\n\tconst rootCanonical = canonicalizeSessionPath(root, platform);\n\tconst candidateCanonical = canonicalizeSessionPath(candidate, platform);\n\tif (rootCanonical === null || candidateCanonical === null) return false;\n\tconst rootParts = parseCanonical(rootCanonical, platform);\n\tconst candidateParts = parseCanonical(candidateCanonical, platform);\n\tif (rootParts === null || candidateParts === null) return false;\n\treturn partsContain(rootParts, candidateParts, platform);\n}\n\nfunction classifyTarget(\n\trootCanonical: string,\n\ttargetCanonical: string,\n\tplatform: SessionPathPlatform,\n): SessionPathClassification {\n\tconst rootParts = parseCanonical(rootCanonical, platform);\n\tconst targetParts = parseCanonical(targetCanonical, platform);\n\tif (rootParts === null || targetParts === null) return \"external\";\n\tif (!partsContain(rootParts, targetParts, platform)) return \"external\";\n\tif (rootParts.drive === targetParts.drive && rootParts.segments.length === targetParts.segments.length) {\n\t\treturn \"root\";\n\t}\n\treturn \"inside\";\n}\n\n// -------------------------------------------------------------------------------------------------\n// Strict evidence / lock validation. Identity is normalized leniently.\n// -------------------------------------------------------------------------------------------------\n\nfunction validateStat(value: unknown, label: string): SessionPathStat {\n\tassertPlainObject(value, label);\n\tassertExactKeys(value, STAT_KEYS, label);\n\tassertCanonicalDecimal(value.dev, `${label}.dev`);\n\tassertCanonicalDecimal(value.ino, `${label}.ino`);\n\tassertSafeNonNegInt(value.nlink, `${label}.nlink`);\n\tassertSafeNonNegInt(value.size, `${label}.size`);\n\tassertSafeNonNegInt(value.mtime, `${label}.mtime`);\n\tassertBoolean(value.regular, `${label}.regular`);\n\tassertCanonicalDecimal(value.owner, `${label}.owner`);\n\treturn {\n\t\tdev: value.dev,\n\t\tino: value.ino,\n\t\tnlink: value.nlink,\n\t\tsize: value.size,\n\t\tmtime: value.mtime,\n\t\tregular: value.regular,\n\t\towner: value.owner,\n\t};\n}\n\nfunction validateChainEntry(value: unknown, label: string): SessionPathChainEntry {\n\tassertPlainObject(value, label);\n\tassertExactKeys(value, CHAIN_ENTRY_KEYS, label);\n\tassertPathString(value.lexical, `${label}.lexical`);\n\tassertPathString(value.realpath, `${label}.realpath`);\n\tif (!LINK_KINDS.has(value.linkKind as SessionPathLinkKind)) {\n\t\tthrow new TypeError(`${label}.linkKind must be none|symlink|reparse`);\n\t}\n\treturn { lexical: value.lexical, realpath: value.realpath, linkKind: value.linkKind as SessionPathLinkKind };\n}\n\nfunction validateEvidence(evidence: unknown): SessionPathEvidence {\n\tassertPlainObject(evidence, \"evidence\");\n\tassertExactKeys(evidence, EVIDENCE_KEYS, \"evidence\");\n\tif (evidence.schemaVersion !== 1) throw new TypeError(\"evidence.schemaVersion must be 1\");\n\tif (!PLATFORMS.has(evidence.platform as SessionPathPlatform)) {\n\t\tthrow new TypeError(\"evidence.platform must be posix|win32\");\n\t}\n\tassertPathString(evidence.trustedRootLexical, \"evidence.trustedRootLexical\");\n\tassertPathString(evidence.trustedRootRealpath, \"evidence.trustedRootRealpath\");\n\n\tassertPlainObject(evidence.target, \"evidence.target\");\n\tassertExactKeys(evidence.target, TARGET_KEYS, \"evidence.target\");\n\tassertPathString(evidence.target.lexical, \"evidence.target.lexical\");\n\tassertPathString(evidence.target.realpath, \"evidence.target.realpath\");\n\n\tif (!Array.isArray(evidence.chain)) throw new TypeError(\"evidence.chain must be an array\");\n\tconst chainLength = evidence.chain.length;\n\tif (chainLength === 0) throw new TypeError(\"evidence.chain must be non-empty\");\n\tif (chainLength > MAX_SESSION_PATH_CHAIN_ENTRIES) {\n\t\tthrow new TypeError(`evidence.chain exceeds MAX_SESSION_PATH_CHAIN_ENTRIES (${MAX_SESSION_PATH_CHAIN_ENTRIES})`);\n\t}\n\tassertStrictPlainArray(evidence.chain, \"evidence.chain\");\n\tconst chain: SessionPathChainEntry[] = [];\n\tfor (let index = 0; index < chainLength; index++) {\n\t\tchain.push(validateChainEntry(evidence.chain[index], `evidence.chain[${index}]`));\n\t}\n\n\tconst statBefore = validateStat(evidence.statBefore, \"evidence.statBefore\");\n\tlet statAfter: SessionPathStat | null = null;\n\tif (evidence.statAfter !== null) {\n\t\tstatAfter = validateStat(evidence.statAfter, \"evidence.statAfter\");\n\t}\n\n\tassertPlainObject(evidence.opened, \"evidence.opened\");\n\tassertExactKeys(evidence.opened, OPENED_KEYS, \"evidence.opened\");\n\tassertCanonicalDecimal(evidence.opened.dev, \"evidence.opened.dev\");\n\tassertCanonicalDecimal(evidence.opened.ino, \"evidence.opened.ino\");\n\n\treturn {\n\t\tschemaVersion: 1,\n\t\tplatform: evidence.platform as SessionPathPlatform,\n\t\ttrustedRootLexical: evidence.trustedRootLexical,\n\t\ttrustedRootRealpath: evidence.trustedRootRealpath,\n\t\ttarget: { lexical: evidence.target.lexical, realpath: evidence.target.realpath },\n\t\tchain,\n\t\tstatBefore,\n\t\tstatAfter,\n\t\topened: { dev: evidence.opened.dev, ino: evidence.opened.ino },\n\t};\n}\n\nfunction validateLock(lock: unknown): SessionPathLockEvidence {\n\tassertPlainObject(lock, \"lock\");\n\tassertExactKeys(lock, LOCK_KEYS, \"lock\");\n\tif (!LOCK_STATES.has(lock.state as SessionPathLockState)) {\n\t\tthrow new TypeError(\"lock.state must be absent|live|stale|foreign|unknown\");\n\t}\n\tassertBoolean(lock.sameHost, \"lock.sameHost\");\n\tassertBoolean(lock.pidDefinitelyAbsent, \"lock.pidDefinitelyAbsent\");\n\tif (lock.holderPid !== null) {\n\t\tassertCanonicalDecimal(lock.holderPid, \"lock.holderPid\");\n\t}\n\treturn {\n\t\tstate: lock.state as SessionPathLockState,\n\t\tsameHost: lock.sameHost,\n\t\tpidDefinitelyAbsent: lock.pidDefinitelyAbsent,\n\t\tholderPid: lock.holderPid === null ? null : lock.holderPid,\n\t};\n}\n\n/**\n * Identity is normalized leniently: a missing or structurally incomplete\n * identity yields undefined, which the policy treats as \"not provided\". This\n * matches the requirement that missing/incomplete identity *blocks* privileged\n * intents rather than throwing.\n */\nfunction normalizeIdentity(value: unknown): SessionPathIdentity | undefined {\n\tif (!isPlainDataObject(value)) return undefined;\n\tif (!hasExactKeys(value, [\"owner\"])) return undefined;\n\tif (!isCanonicalDecimal(value.owner)) return undefined;\n\treturn { owner: value.owner };\n}\n\nfunction validateInput(rawInput: unknown): SessionPathAccessInput {\n\tassertPlainObject(rawInput, \"input\");\n\tfor (const key of Object.keys(rawInput)) {\n\t\tif (!INPUT_ALLOWED_KEYS.has(key)) throw new TypeError(`input has an unknown key: ${key}`);\n\t}\n\tif (!PLATFORMS.has(rawInput.platform as SessionPathPlatform)) {\n\t\tthrow new TypeError(\"input.platform must be posix|win32\");\n\t}\n\tassertString(rawInput.root, \"input.root\");\n\tassertString(rawInput.target, \"input.target\");\n\tif (!INTENTS.has(rawInput.intent as SessionPathIntent)) {\n\t\tthrow new TypeError(\"input.intent must be inspect_metadata|inspect_content|repair|repair_dry_run\");\n\t}\n\tif (!Object.hasOwn(rawInput, \"evidence\")) {\n\t\tthrow new TypeError(\"input.evidence is required\");\n\t}\n\tconst evidence = validateEvidence(rawInput.evidence);\n\tlet identity: SessionPathIdentity | undefined;\n\tif (Object.hasOwn(rawInput, \"identity\") && rawInput.identity !== undefined) {\n\t\tidentity = normalizeIdentity(rawInput.identity);\n\t}\n\tlet lock: SessionPathLockEvidence | undefined;\n\tif (Object.hasOwn(rawInput, \"lock\") && rawInput.lock !== undefined) {\n\t\tlock = validateLock(rawInput.lock);\n\t}\n\treturn {\n\t\tplatform: rawInput.platform as SessionPathPlatform,\n\t\troot: rawInput.root,\n\t\ttarget: rawInput.target,\n\t\tintent: rawInput.intent as SessionPathIntent,\n\t\tidentity,\n\t\tevidence,\n\t\tlock,\n\t};\n}\n\n// -------------------------------------------------------------------------------------------------\n// Decision assembly.\n// -------------------------------------------------------------------------------------------------\n\nfunction statEqual(a: SessionPathStat, b: SessionPathStat): boolean {\n\treturn (\n\t\ta.dev === b.dev &&\n\t\ta.ino === b.ino &&\n\t\ta.nlink === b.nlink &&\n\t\ta.size === b.size &&\n\t\ta.mtime === b.mtime &&\n\t\ta.regular === b.regular &&\n\t\ta.owner === b.owner\n\t);\n}\n\nfunction buildDecision(fields: {\n\tplatform: SessionPathPlatform;\n\tintent: SessionPathIntent;\n\tstatus: SessionPathStatus;\n\treason: SessionPathReason;\n\tclassification: SessionPathClassification;\n\tevidence: SessionPathEvidence;\n\tcanInspectMetadata: boolean;\n\tcanReadContents: boolean;\n\tcanRepair: boolean;\n\tplannedActions: readonly SessionPathPlannedAction[];\n\tscheduledWrites: number;\n}): SessionPathDecision {\n\tconst decision: SessionPathDecision = {\n\t\tschemaVersion: SESSION_PATH_POLICY_SCHEMA_VERSION,\n\t\tplatform: fields.platform,\n\t\tintent: fields.intent,\n\t\tstatus: fields.status,\n\t\treason: fields.reason,\n\t\tdryRun: fields.intent === \"repair_dry_run\",\n\t\tclassification: fields.classification,\n\t\ttargetLexical: fields.evidence.target.lexical,\n\t\ttargetRealpath: fields.evidence.target.realpath,\n\t\ttrustedRootLexical: fields.evidence.trustedRootLexical,\n\t\ttrustedRootRealpath: fields.evidence.trustedRootRealpath,\n\t\tcapabilities: {\n\t\t\tcanInspectMetadata: fields.canInspectMetadata,\n\t\t\tcanReadContents: fields.canReadContents,\n\t\t\tcanRepair: fields.canRepair,\n\t\t},\n\t\tplannedActions: fields.plannedActions.map((action) => ({ kind: action.kind, holderPid: action.holderPid })),\n\t\tscheduledWrites: fields.scheduledWrites,\n\t};\n\treturn deepFreeze(decision);\n}\n\nfunction rejected(\n\tplatform: SessionPathPlatform,\n\tintent: SessionPathIntent,\n\treason: SessionPathReason,\n\tevidence: SessionPathEvidence,\n\tclassification: SessionPathClassification = \"external\",\n): SessionPathDecision {\n\treturn buildDecision({\n\t\tplatform,\n\t\tintent,\n\t\tstatus: \"rejected\",\n\t\treason,\n\t\tclassification,\n\t\tevidence,\n\t\tcanInspectMetadata: false,\n\t\tcanReadContents: false,\n\t\tcanRepair: false,\n\t\tplannedActions: [],\n\t\tscheduledWrites: 0,\n\t});\n}\n\nfunction blocked(\n\tplatform: SessionPathPlatform,\n\tintent: SessionPathIntent,\n\treason: SessionPathReason,\n\tevidence: SessionPathEvidence,\n\tclassification: SessionPathClassification,\n): SessionPathDecision {\n\treturn buildDecision({\n\t\tplatform,\n\t\tintent,\n\t\tstatus: \"blocked\",\n\t\treason,\n\t\tclassification,\n\t\tevidence,\n\t\tcanInspectMetadata: false,\n\t\tcanReadContents: false,\n\t\tcanRepair: false,\n\t\tplannedActions: [],\n\t\tscheduledWrites: 0,\n\t});\n}\n\nfunction metadataOnly(\n\tplatform: SessionPathPlatform,\n\tintent: SessionPathIntent,\n\tevidence: SessionPathEvidence,\n\tclassification: SessionPathClassification,\n): SessionPathDecision {\n\treturn buildDecision({\n\t\tplatform,\n\t\tintent,\n\t\tstatus: \"metadata_only\",\n\t\treason: \"metadata_only\",\n\t\tclassification,\n\t\tevidence,\n\t\tcanInspectMetadata: true,\n\t\tcanReadContents: false,\n\t\tcanRepair: false,\n\t\tplannedActions: [],\n\t\tscheduledWrites: 0,\n\t});\n}\n\nfunction authorized(\n\tplatform: SessionPathPlatform,\n\tintent: SessionPathIntent,\n\tevidence: SessionPathEvidence,\n\tclassification: SessionPathClassification,\n\tcanReadContents: boolean,\n\tcanRepair: boolean,\n\tplannedActions: readonly SessionPathPlannedAction[],\n\tscheduledWrites: number,\n): SessionPathDecision {\n\treturn buildDecision({\n\t\tplatform,\n\t\tintent,\n\t\tstatus: \"authorized\",\n\t\treason: \"authorized\",\n\t\tclassification,\n\t\tevidence,\n\t\tcanInspectMetadata: true,\n\t\tcanReadContents,\n\t\tcanRepair,\n\t\tplannedActions,\n\t\tscheduledWrites,\n\t});\n}\n\n/**\n * True when `childCanonical` is exactly one segment deeper than\n * `parentCanonical` and shares its entire prefix (a direct child at a segment\n * boundary). Win32 compares segments case-insensitively.\n */\nfunction isDirectChild(parentCanonical: string, childCanonical: string, platform: SessionPathPlatform): boolean {\n\tconst parentParts = parseCanonical(parentCanonical, platform);\n\tconst childParts = parseCanonical(childCanonical, platform);\n\tif (parentParts === null || childParts === null) return false;\n\tif (parentParts.drive !== childParts.drive) return false;\n\tif (childParts.segments.length !== parentParts.segments.length + 1) return false;\n\tfor (let i = 0; i < parentParts.segments.length; i++) {\n\t\tif (!segmentEqual(parentParts.segments[i], childParts.segments[i], platform)) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Strict chain binding (P3). The configured root is conservative (no symlinks),\n * so its lexical and real canonical forms must be identical. The chain must\n * start at the roots, end at the target lexical/real, have exactly the lexical\n * depth-delta plus one entries, and every adjacent lexical path and realpath\n * must be a direct child at a segment boundary. Every entry's lexical path must\n * stay inside the lexical root and every realpath inside the real root. Any\n * linkKind other than \"none\" blocks. Returns the first closed reason, or null\n * when the chain is fully bound.\n */\nfunction bindChain(\n\tevidence: SessionPathEvidence,\n\trootCanonical: string,\n\trootRealpathCanonical: string,\n\ttargetCanonical: string,\n\ttargetRealpathCanonical: string,\n\tplatform: SessionPathPlatform,\n): SessionPathReason | null {\n\tif (!canonicalPathEqual(rootCanonical, rootRealpathCanonical, platform)) return \"evidence_mismatch\";\n\n\tconst rootParts = parseCanonical(rootCanonical, platform);\n\tconst targetLexicalParts = parseCanonical(targetCanonical, platform);\n\tif (rootParts === null || targetLexicalParts === null) return \"evidence_mismatch\";\n\n\tconst expectedLength = targetLexicalParts.segments.length - rootParts.segments.length + 1;\n\tif (evidence.chain.length !== expectedLength) return \"evidence_mismatch\";\n\n\tconst firstLexical = canonicalizeSessionPath(evidence.chain[0].lexical, platform);\n\tconst firstReal = canonicalizeSessionPath(evidence.chain[0].realpath, platform);\n\tif (firstLexical === null || firstReal === null) return \"evidence_mismatch\";\n\tif (\n\t\t!canonicalPathEqual(firstLexical, rootCanonical, platform) ||\n\t\t!canonicalPathEqual(firstReal, rootRealpathCanonical, platform)\n\t)\n\t\treturn \"evidence_mismatch\";\n\n\tconst last = evidence.chain[evidence.chain.length - 1];\n\tconst lastLexical = canonicalizeSessionPath(last.lexical, platform);\n\tconst lastReal = canonicalizeSessionPath(last.realpath, platform);\n\tif (\n\t\t!canonicalPathEqual(lastLexical, targetCanonical, platform) ||\n\t\t!canonicalPathEqual(lastReal, targetRealpathCanonical, platform)\n\t)\n\t\treturn \"evidence_mismatch\";\n\n\tlet prevLexical = firstLexical;\n\tlet prevReal = firstReal;\n\tfor (let index = 0; index < evidence.chain.length; index++) {\n\t\tconst entry = evidence.chain[index];\n\t\tif (entry.linkKind !== \"none\") return \"symlink_in_chain\";\n\t\tconst entryLexical = canonicalizeSessionPath(entry.lexical, platform);\n\t\tconst entryReal = canonicalizeSessionPath(entry.realpath, platform);\n\t\tif (entryLexical === null || entryReal === null) return \"evidence_mismatch\";\n\t\tconst entryLexicalParts = parseCanonical(entryLexical, platform);\n\t\tconst entryRealParts = parseCanonical(entryReal, platform);\n\t\tif (entryLexicalParts === null || entryRealParts === null) return \"evidence_mismatch\";\n\t\tif (!partsContain(rootParts, entryLexicalParts, platform)) return \"evidence_mismatch\";\n\t\tif (!partsContain(rootParts, entryRealParts, platform)) return \"outside_realpath\";\n\t\tif (index > 0) {\n\t\t\tif (!isDirectChild(prevLexical, entryLexical, platform)) return \"evidence_mismatch\";\n\t\t\tif (!isDirectChild(prevReal, entryReal, platform)) return \"evidence_mismatch\";\n\t\t}\n\t\tprevLexical = entryLexical;\n\t\tprevReal = entryReal;\n\t}\n\treturn null;\n}\n\n/**\n * Lock coherence, fail-closed (P5). Each state is only meaningful with the\n * field values that justify it; a self-contradictory lock is treated as\n * untrustworthy and closed. `absent` and an eligible `stale` return null\n * (proceed); every other state (including any contradiction) returns a closed\n * reason rather than throwing.\n */\nfunction evaluateLock(lock: SessionPathLockEvidence): SessionPathReason | null {\n\tswitch (lock.state) {\n\t\tcase \"absent\":\n\t\t\treturn lock.holderPid === null && lock.pidDefinitelyAbsent ? null : \"lock_unknown\";\n\t\tcase \"live\":\n\t\t\treturn lock.sameHost && lock.holderPid !== null && !lock.pidDefinitelyAbsent ? \"lock_live\" : \"lock_unknown\";\n\t\tcase \"foreign\":\n\t\t\treturn !lock.sameHost ? \"lock_foreign\" : \"lock_unknown\";\n\t\tcase \"stale\":\n\t\t\treturn lock.sameHost && lock.holderPid !== null && lock.pidDefinitelyAbsent ? null : \"lock_stale_ineligible\";\n\t\tcase \"unknown\":\n\t\t\treturn \"lock_unknown\";\n\t\tdefault:\n\t\t\treturn \"lock_unknown\";\n\t}\n}\n\nfunction plannedActionsForLock(lock: SessionPathLockEvidence): SessionPathPlannedAction[] {\n\tif (lock.state === \"stale\" && lock.holderPid !== null) {\n\t\treturn [{ kind: \"remove_stale_lock\", holderPid: lock.holderPid }];\n\t}\n\treturn [];\n}\n\n/**\n * Decide session path access from attested plain-data evidence. Pure: never\n * performs I/O and never executes a repair. The returned decision is a\n * deep-frozen copy.\n *\n * Classification precedence (P0): the lexical classification of the request\n * root/target is computed first and is authoritative for privileged intents.\n * `inspect_metadata` always resolves to metadata_only after lexical/evidence\n * binding. For `inspect_content`/`repair`/`repair_dry_run` the lexical target\n * must be strictly inside the root (external => target_external, root =>\n * target_is_root) before identity, and the realpath classification must\n * independently be inside; an injected realpath can never upgrade an external\n * lexical target. `repair_dry_run` shares repair's eligibility, status, reason,\n * and planned actions, but reports canRepair=false and zero scheduled writes.\n */\nexport function decideSessionPathAccess(rawInput: unknown): SessionPathDecision {\n\tconst { platform, root, target, intent, identity, evidence, lock } = validateInput(rawInput);\n\n\tconst rootCanonical = canonicalizeSessionPath(root, platform);\n\tconst targetCanonical = canonicalizeSessionPath(target, platform);\n\tif (rootCanonical === null || targetCanonical === null) {\n\t\treturn rejected(platform, intent, \"lexical_invalid\", evidence);\n\t}\n\n\tif (evidence.platform !== platform) {\n\t\treturn blocked(platform, intent, \"evidence_mismatch\", evidence, \"external\");\n\t}\n\tconst evidenceRootCanonical = canonicalizeSessionPath(evidence.trustedRootLexical, platform);\n\tconst evidenceTargetCanonical = canonicalizeSessionPath(evidence.target.lexical, platform);\n\tconst rootRealpathCanonical = canonicalizeSessionPath(evidence.trustedRootRealpath, platform);\n\tconst targetRealpathCanonical = canonicalizeSessionPath(evidence.target.realpath, platform);\n\tif (\n\t\t!canonicalPathEqual(evidenceRootCanonical, rootCanonical, platform) ||\n\t\t!canonicalPathEqual(evidenceTargetCanonical, targetCanonical, platform) ||\n\t\trootRealpathCanonical === null ||\n\t\ttargetRealpathCanonical === null\n\t) {\n\t\treturn blocked(platform, intent, \"evidence_mismatch\", evidence, \"external\");\n\t}\n\n\tconst lexicalClassification = classifyTarget(rootCanonical, targetCanonical, platform);\n\tconst isDryRun = intent === \"repair_dry_run\";\n\tconst privilegedIntent: SessionPathIntent = isDryRun ? \"repair\" : intent;\n\n\tif (privilegedIntent === \"inspect_metadata\") {\n\t\treturn metadataOnly(platform, intent, evidence, lexicalClassification);\n\t}\n\n\t// inspect_content, repair, repair_dry_run: lexical classification is primary (P0).\n\tif (lexicalClassification === \"external\") {\n\t\treturn rejected(platform, intent, \"target_external\", evidence, \"external\");\n\t}\n\tif (lexicalClassification === \"root\") {\n\t\treturn rejected(platform, intent, \"target_is_root\", evidence, \"root\");\n\t}\n\n\t// P0: independently require the realpath classification to be strictly inside;\n\t// an injected realpath can never upgrade an inside lexical target.\n\tconst realClassification = classifyTarget(rootRealpathCanonical, targetRealpathCanonical, platform);\n\tif (realClassification !== \"inside\") {\n\t\treturn blocked(platform, intent, \"outside_realpath\", evidence, \"inside\");\n\t}\n\n\tif (identity === undefined) {\n\t\treturn blocked(platform, intent, \"identity_required\", evidence, \"inside\");\n\t}\n\tif (lock === undefined) {\n\t\treturn blocked(platform, intent, \"lock_required\", evidence, \"inside\");\n\t}\n\n\tconst chainReason = bindChain(\n\t\tevidence,\n\t\trootCanonical,\n\t\trootRealpathCanonical,\n\t\ttargetCanonical,\n\t\ttargetRealpathCanonical,\n\t\tplatform,\n\t);\n\tif (chainReason !== null) {\n\t\treturn blocked(platform, intent, chainReason, evidence, \"inside\");\n\t}\n\tif (!evidence.statBefore.regular) {\n\t\treturn blocked(platform, intent, \"nonregular\", evidence, \"inside\");\n\t}\n\tif (evidence.statBefore.nlink !== 1) {\n\t\treturn blocked(platform, intent, \"nlink_not_one\", evidence, \"inside\");\n\t}\n\tif (identity.owner !== evidence.statBefore.owner) {\n\t\treturn blocked(platform, intent, \"owner_mismatch\", evidence, \"inside\");\n\t}\n\tif (evidence.opened.dev !== evidence.statBefore.dev || evidence.opened.ino !== evidence.statBefore.ino) {\n\t\treturn blocked(platform, intent, \"stat_race\", evidence, \"inside\");\n\t}\n\tif (evidence.statAfter === null || !statEqual(evidence.statBefore, evidence.statAfter)) {\n\t\treturn blocked(platform, intent, \"stat_after_mismatch\", evidence, \"inside\");\n\t}\n\n\tconst lockBlock = evaluateLock(lock);\n\tif (lockBlock !== null) {\n\t\treturn blocked(platform, intent, lockBlock, evidence, \"inside\");\n\t}\n\n\tif (privilegedIntent === \"inspect_content\") {\n\t\treturn authorized(platform, intent, evidence, \"inside\", true, false, [], 0);\n\t}\n\n\tconst plannedActions = plannedActionsForLock(lock);\n\tconst scheduledWrites = isDryRun ? 0 : plannedActions.length;\n\treturn authorized(platform, intent, evidence, \"inside\", false, !isDryRun, plannedActions, scheduledWrites);\n}\n"]}