open-multi-agent-kit 0.90.7 → 0.90.9

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 (331) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +17 -10
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +5 -1
  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 +116 -2
  31. package/dist/core/agent-session.d.ts.map +1 -1
  32. package/dist/core/agent-session.js +1015 -71
  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/compaction/compaction.d.ts +6 -0
  43. package/dist/core/compaction/compaction.d.ts.map +1 -1
  44. package/dist/core/compaction/compaction.js +12 -1
  45. package/dist/core/compaction/compaction.js.map +1 -1
  46. package/dist/core/compaction/hysteresis.d.ts +27 -0
  47. package/dist/core/compaction/hysteresis.d.ts.map +1 -0
  48. package/dist/core/compaction/hysteresis.js +84 -0
  49. package/dist/core/compaction/hysteresis.js.map +1 -0
  50. package/dist/core/compaction/index.d.ts +2 -0
  51. package/dist/core/compaction/index.d.ts.map +1 -1
  52. package/dist/core/compaction/index.js +2 -0
  53. package/dist/core/compaction/index.js.map +1 -1
  54. package/dist/core/compaction/transaction.d.ts +172 -0
  55. package/dist/core/compaction/transaction.d.ts.map +1 -0
  56. package/dist/core/compaction/transaction.js +824 -0
  57. package/dist/core/compaction/transaction.js.map +1 -0
  58. package/dist/core/context-budget-governor-v2.d.ts +2 -1
  59. package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
  60. package/dist/core/context-budget-governor-v2.js +2 -1
  61. package/dist/core/context-budget-governor-v2.js.map +1 -1
  62. package/dist/core/context-budget-headroom.d.ts.map +1 -1
  63. package/dist/core/context-budget-headroom.js +5 -1
  64. package/dist/core/context-budget-headroom.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 +8 -2
  78. package/dist/core/context-budget-v2-cache-provider.d.ts.map +1 -1
  79. package/dist/core/context-budget-v2-cache-provider.js +28 -5
  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-selection.d.ts.map +1 -1
  88. package/dist/core/context-budget-v2-selection.js +9 -0
  89. package/dist/core/context-budget-v2-selection.js.map +1 -1
  90. package/dist/core/context-budget-v2-types.d.ts +4 -0
  91. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  92. package/dist/core/context-budget-v2-types.js.map +1 -1
  93. package/dist/core/durable-file-directory.d.ts +4 -0
  94. package/dist/core/durable-file-directory.d.ts.map +1 -0
  95. package/dist/core/durable-file-directory.js +76 -0
  96. package/dist/core/durable-file-directory.js.map +1 -0
  97. package/dist/core/durable-file-identity.d.ts +26 -0
  98. package/dist/core/durable-file-identity.d.ts.map +1 -0
  99. package/dist/core/durable-file-identity.js +118 -0
  100. package/dist/core/durable-file-identity.js.map +1 -0
  101. package/dist/core/durable-file-io.d.ts +29 -0
  102. package/dist/core/durable-file-io.d.ts.map +1 -0
  103. package/dist/core/durable-file-io.js +179 -0
  104. package/dist/core/durable-file-io.js.map +1 -0
  105. package/dist/core/durable-file-lock-cleanup.d.ts +12 -0
  106. package/dist/core/durable-file-lock-cleanup.d.ts.map +1 -0
  107. package/dist/core/durable-file-lock-cleanup.js +39 -0
  108. package/dist/core/durable-file-lock-cleanup.js.map +1 -0
  109. package/dist/core/durable-file-lock-owner.d.ts +18 -0
  110. package/dist/core/durable-file-lock-owner.d.ts.map +1 -0
  111. package/dist/core/durable-file-lock-owner.js +78 -0
  112. package/dist/core/durable-file-lock-owner.js.map +1 -0
  113. package/dist/core/durable-file-lock-storage.d.ts +35 -0
  114. package/dist/core/durable-file-lock-storage.d.ts.map +1 -0
  115. package/dist/core/durable-file-lock-storage.js +263 -0
  116. package/dist/core/durable-file-lock-storage.js.map +1 -0
  117. package/dist/core/durable-file-mode.d.ts +8 -0
  118. package/dist/core/durable-file-mode.d.ts.map +1 -0
  119. package/dist/core/durable-file-mode.js +41 -0
  120. package/dist/core/durable-file-mode.js.map +1 -0
  121. package/dist/core/extensions/loader.d.ts +12 -0
  122. package/dist/core/extensions/loader.d.ts.map +1 -1
  123. package/dist/core/extensions/loader.js +50 -37
  124. package/dist/core/extensions/loader.js.map +1 -1
  125. package/dist/core/extensions/types.d.ts +2 -0
  126. package/dist/core/extensions/types.d.ts.map +1 -1
  127. package/dist/core/extensions/types.js.map +1 -1
  128. package/dist/core/index.d.ts +6 -2
  129. package/dist/core/index.d.ts.map +1 -1
  130. package/dist/core/index.js +5 -0
  131. package/dist/core/index.js.map +1 -1
  132. package/dist/core/loop-gates.d.ts.map +1 -1
  133. package/dist/core/loop-gates.js +25 -1
  134. package/dist/core/loop-gates.js.map +1 -1
  135. package/dist/core/model-resolver.d.ts.map +1 -1
  136. package/dist/core/model-resolver.js +1 -1
  137. package/dist/core/model-resolver.js.map +1 -1
  138. package/dist/core/pi-package-intake.d.ts +1 -0
  139. package/dist/core/pi-package-intake.d.ts.map +1 -1
  140. package/dist/core/pi-package-intake.js +7 -0
  141. package/dist/core/pi-package-intake.js.map +1 -1
  142. package/dist/core/redaction.d.ts +8 -0
  143. package/dist/core/redaction.d.ts.map +1 -0
  144. package/dist/core/redaction.js +31 -0
  145. package/dist/core/redaction.js.map +1 -0
  146. package/dist/core/resource-loader.d.ts.map +1 -1
  147. package/dist/core/resource-loader.js +36 -0
  148. package/dist/core/resource-loader.js.map +1 -1
  149. package/dist/core/run-journal-store.d.ts +53 -0
  150. package/dist/core/run-journal-store.d.ts.map +1 -0
  151. package/dist/core/run-journal-store.js +278 -0
  152. package/dist/core/run-journal-store.js.map +1 -0
  153. package/dist/core/run-journal.d.ts +172 -0
  154. package/dist/core/run-journal.d.ts.map +1 -0
  155. package/dist/core/run-journal.js +1053 -0
  156. package/dist/core/run-journal.js.map +1 -0
  157. package/dist/core/sdk.d.ts +7 -0
  158. package/dist/core/sdk.d.ts.map +1 -1
  159. package/dist/core/sdk.js +94 -3
  160. package/dist/core/sdk.js.map +1 -1
  161. package/dist/core/session-doctor-plan.d.ts +128 -0
  162. package/dist/core/session-doctor-plan.d.ts.map +1 -0
  163. package/dist/core/session-doctor-plan.js +833 -0
  164. package/dist/core/session-doctor-plan.js.map +1 -0
  165. package/dist/core/session-file-compaction.d.ts +6 -0
  166. package/dist/core/session-file-compaction.d.ts.map +1 -0
  167. package/dist/core/session-file-compaction.js +169 -0
  168. package/dist/core/session-file-compaction.js.map +1 -0
  169. package/dist/core/session-file-head.d.ts +10 -0
  170. package/dist/core/session-file-head.d.ts.map +1 -0
  171. package/dist/core/session-file-head.js +78 -0
  172. package/dist/core/session-file-head.js.map +1 -0
  173. package/dist/core/session-file-persistence.d.ts +30 -0
  174. package/dist/core/session-file-persistence.d.ts.map +1 -0
  175. package/dist/core/session-file-persistence.js +106 -0
  176. package/dist/core/session-file-persistence.js.map +1 -0
  177. package/dist/core/session-integrity.d.ts +43 -0
  178. package/dist/core/session-integrity.d.ts.map +1 -0
  179. package/dist/core/session-integrity.js +420 -0
  180. package/dist/core/session-integrity.js.map +1 -0
  181. package/dist/core/session-manager.d.ts +24 -4
  182. package/dist/core/session-manager.d.ts.map +1 -1
  183. package/dist/core/session-manager.js +242 -125
  184. package/dist/core/session-manager.js.map +1 -1
  185. package/dist/core/session-owner-lease.d.ts +22 -0
  186. package/dist/core/session-owner-lease.d.ts.map +1 -0
  187. package/dist/core/session-owner-lease.js +176 -0
  188. package/dist/core/session-owner-lease.js.map +1 -0
  189. package/dist/core/session-path-policy.d.ts +132 -0
  190. package/dist/core/session-path-policy.d.ts.map +1 -0
  191. package/dist/core/session-path-policy.js +780 -0
  192. package/dist/core/session-path-policy.js.map +1 -0
  193. package/dist/core/session-repair-plan.d.ts +49 -0
  194. package/dist/core/session-repair-plan.d.ts.map +1 -0
  195. package/dist/core/session-repair-plan.js +202 -0
  196. package/dist/core/session-repair-plan.js.map +1 -0
  197. package/dist/core/session-termination.d.ts +100 -0
  198. package/dist/core/session-termination.d.ts.map +1 -0
  199. package/dist/core/session-termination.js +403 -0
  200. package/dist/core/session-termination.js.map +1 -0
  201. package/dist/core/settings-manager.d.ts +32 -0
  202. package/dist/core/settings-manager.d.ts.map +1 -1
  203. package/dist/core/settings-manager.js +52 -5
  204. package/dist/core/settings-manager.js.map +1 -1
  205. package/dist/core/system-prompt.d.ts.map +1 -1
  206. package/dist/core/system-prompt.js +1 -1
  207. package/dist/core/system-prompt.js.map +1 -1
  208. package/dist/core/verified-bash-adapter.d.ts +36 -0
  209. package/dist/core/verified-bash-adapter.d.ts.map +1 -0
  210. package/dist/core/verified-bash-adapter.js +90 -0
  211. package/dist/core/verified-bash-adapter.js.map +1 -0
  212. package/dist/guardrails/command-redaction.d.ts +32 -0
  213. package/dist/guardrails/command-redaction.d.ts.map +1 -0
  214. package/dist/guardrails/command-redaction.js +776 -0
  215. package/dist/guardrails/command-redaction.js.map +1 -0
  216. package/dist/guardrails/evidence-attestation.d.ts +31 -0
  217. package/dist/guardrails/evidence-attestation.d.ts.map +1 -0
  218. package/dist/guardrails/evidence-attestation.js +178 -0
  219. package/dist/guardrails/evidence-attestation.js.map +1 -0
  220. package/dist/guardrails/evidence-receipt-store.d.ts +33 -0
  221. package/dist/guardrails/evidence-receipt-store.d.ts.map +1 -0
  222. package/dist/guardrails/evidence-receipt-store.js +446 -0
  223. package/dist/guardrails/evidence-receipt-store.js.map +1 -0
  224. package/dist/guardrails/evidence-receipt.d.ts +58 -0
  225. package/dist/guardrails/evidence-receipt.d.ts.map +1 -0
  226. package/dist/guardrails/evidence-receipt.js +389 -0
  227. package/dist/guardrails/evidence-receipt.js.map +1 -0
  228. package/dist/guardrails/evidence-system.d.ts +40 -9
  229. package/dist/guardrails/evidence-system.d.ts.map +1 -1
  230. package/dist/guardrails/evidence-system.js +379 -83
  231. package/dist/guardrails/evidence-system.js.map +1 -1
  232. package/dist/guardrails/replay-ledger-io.d.ts +6 -0
  233. package/dist/guardrails/replay-ledger-io.d.ts.map +1 -0
  234. package/dist/guardrails/replay-ledger-io.js +42 -0
  235. package/dist/guardrails/replay-ledger-io.js.map +1 -0
  236. package/dist/guardrails/replay-ledger-lock-owner.d.ts +32 -0
  237. package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -0
  238. package/dist/guardrails/replay-ledger-lock-owner.js +138 -0
  239. package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -0
  240. package/dist/guardrails/replay-ledger-lock.d.ts +24 -0
  241. package/dist/guardrails/replay-ledger-lock.d.ts.map +1 -0
  242. package/dist/guardrails/replay-ledger-lock.js +135 -0
  243. package/dist/guardrails/replay-ledger-lock.js.map +1 -0
  244. package/dist/guardrails/replay-ledger-mutation-gate.d.ts +24 -0
  245. package/dist/guardrails/replay-ledger-mutation-gate.d.ts.map +1 -0
  246. package/dist/guardrails/replay-ledger-mutation-gate.js +205 -0
  247. package/dist/guardrails/replay-ledger-mutation-gate.js.map +1 -0
  248. package/dist/guardrails/replay-ledger-quarantine.d.ts +2 -0
  249. package/dist/guardrails/replay-ledger-quarantine.d.ts.map +1 -0
  250. package/dist/guardrails/replay-ledger-quarantine.js +19 -0
  251. package/dist/guardrails/replay-ledger-quarantine.js.map +1 -0
  252. package/dist/guardrails/replay-ledger-store.d.ts +32 -0
  253. package/dist/guardrails/replay-ledger-store.d.ts.map +1 -0
  254. package/dist/guardrails/replay-ledger-store.js +211 -0
  255. package/dist/guardrails/replay-ledger-store.js.map +1 -0
  256. package/dist/guardrails/shell-command-name-glob.d.ts +3 -0
  257. package/dist/guardrails/shell-command-name-glob.d.ts.map +1 -0
  258. package/dist/guardrails/shell-command-name-glob.js +233 -0
  259. package/dist/guardrails/shell-command-name-glob.js.map +1 -0
  260. package/dist/guardrails/shell-command-option.d.ts +7 -0
  261. package/dist/guardrails/shell-command-option.d.ts.map +1 -0
  262. package/dist/guardrails/shell-command-option.js +24 -0
  263. package/dist/guardrails/shell-command-option.js.map +1 -0
  264. package/dist/guardrails/shell-command-tokenizer.d.ts +29 -0
  265. package/dist/guardrails/shell-command-tokenizer.d.ts.map +1 -0
  266. package/dist/guardrails/shell-command-tokenizer.js +145 -0
  267. package/dist/guardrails/shell-command-tokenizer.js.map +1 -0
  268. package/dist/guardrails/shell-command-wrapper.d.ts +18 -0
  269. package/dist/guardrails/shell-command-wrapper.d.ts.map +1 -0
  270. package/dist/guardrails/shell-command-wrapper.js +42 -0
  271. package/dist/guardrails/shell-command-wrapper.js.map +1 -0
  272. package/dist/guardrails/verified-executor.d.ts +65 -0
  273. package/dist/guardrails/verified-executor.d.ts.map +1 -0
  274. package/dist/guardrails/verified-executor.js +171 -0
  275. package/dist/guardrails/verified-executor.js.map +1 -0
  276. package/dist/guardrails/workspace-fingerprint.d.ts +23 -0
  277. package/dist/guardrails/workspace-fingerprint.d.ts.map +1 -0
  278. package/dist/guardrails/workspace-fingerprint.js +634 -0
  279. package/dist/guardrails/workspace-fingerprint.js.map +1 -0
  280. package/dist/index.d.ts +15 -3
  281. package/dist/index.d.ts.map +1 -1
  282. package/dist/index.js +13 -1
  283. package/dist/index.js.map +1 -1
  284. package/dist/main.d.ts.map +1 -1
  285. package/dist/main.js +12 -0
  286. package/dist/main.js.map +1 -1
  287. package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
  288. package/dist/modes/interactive/components/control-panel-layout.js +1 -1
  289. package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
  290. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  291. package/dist/modes/interactive/components/footer.js +1 -1
  292. package/dist/modes/interactive/components/footer.js.map +1 -1
  293. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  294. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  295. package/dist/modes/interactive/interactive-mode.js +59 -9
  296. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  297. package/dist/modes/print-mode.d.ts.map +1 -1
  298. package/dist/modes/print-mode.js +43 -2
  299. package/dist/modes/print-mode.js.map +1 -1
  300. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  301. package/dist/modes/rpc/rpc-mode.js +23 -5
  302. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  303. package/dist/modes/rpc/rpc-types.d.ts +3 -0
  304. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  305. package/dist/modes/rpc/rpc-types.js.map +1 -1
  306. package/dist/types/evidence.d.ts +215 -1
  307. package/dist/types/evidence.d.ts.map +1 -1
  308. package/dist/types/evidence.js.map +1 -1
  309. package/dist/verify-ci.d.ts +3 -0
  310. package/dist/verify-ci.d.ts.map +1 -0
  311. package/dist/verify-ci.js +30 -0
  312. package/dist/verify-ci.js.map +1 -0
  313. package/docs/compaction.md +44 -4
  314. package/docs/extensions.md +2 -0
  315. package/docs/providers.md +6 -0
  316. package/docs/sdk.md +238 -1
  317. package/docs/settings.md +67 -4
  318. package/docs/usage.md +38 -0
  319. package/examples/extensions/correctness-wall/README.md +6 -6
  320. package/examples/extensions/correctness-wall/index.ts +37 -11
  321. package/examples/extensions/custom-provider-anthropic/package-lock.json +5 -5
  322. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  323. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  324. package/examples/extensions/gondolin/package-lock.json +2 -2
  325. package/examples/extensions/gondolin/package.json +1 -1
  326. package/examples/extensions/sandbox/package-lock.json +2 -2
  327. package/examples/extensions/sandbox/package.json +1 -1
  328. package/examples/extensions/with-deps/package-lock.json +4 -4
  329. package/examples/extensions/with-deps/package.json +1 -1
  330. package/npm-shrinkwrap.json +12 -12
  331. package/package.json +4 -4
@@ -0,0 +1,780 @@
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
+ const SESSION_PATH_POLICY_SCHEMA_VERSION = 1;
15
+ const SESSION_PATH_MAX_LENGTH = 4096;
16
+ /** Maximum number of entries accepted in an evidence chain (DoS guardrail). */
17
+ export const MAX_SESSION_PATH_CHAIN_ENTRIES = 256;
18
+ const CONTROL_CHAR_RE = /[\u0000-\u001f\u007f]/;
19
+ const CANONICAL_DECIMAL_RE = /^(0|[1-9]\d{0,19})$/;
20
+ const WIN_DRIVE_ABSOLUTE_RE = /^([A-Za-z]):\/+(.*)$/;
21
+ const WIN_CANONICAL_RE = /^([a-z]):\/(.*)$/;
22
+ const PLATFORMS = new Set(["posix", "win32"]);
23
+ const INTENTS = new Set(["inspect_metadata", "inspect_content", "repair", "repair_dry_run"]);
24
+ const LINK_KINDS = new Set(["none", "symlink", "reparse"]);
25
+ const LOCK_STATES = new Set(["absent", "live", "stale", "foreign", "unknown"]);
26
+ const WIN_RESERVED_NAMES = (() => {
27
+ const set = new Set();
28
+ set.add("con");
29
+ set.add("prn");
30
+ set.add("aux");
31
+ set.add("nul");
32
+ for (let i = 1; i <= 9; i++) {
33
+ set.add(`com${i}`);
34
+ set.add(`lpt${i}`);
35
+ }
36
+ return set;
37
+ })();
38
+ const EVIDENCE_KEYS = [
39
+ "schemaVersion",
40
+ "platform",
41
+ "trustedRootLexical",
42
+ "trustedRootRealpath",
43
+ "target",
44
+ "chain",
45
+ "statBefore",
46
+ "statAfter",
47
+ "opened",
48
+ ];
49
+ const STAT_KEYS = ["dev", "ino", "nlink", "size", "mtime", "regular", "owner"];
50
+ const TARGET_KEYS = ["lexical", "realpath"];
51
+ const OPENED_KEYS = ["dev", "ino"];
52
+ const CHAIN_ENTRY_KEYS = ["lexical", "realpath", "linkKind"];
53
+ const LOCK_KEYS = ["state", "sameHost", "pidDefinitelyAbsent", "holderPid"];
54
+ const INPUT_ALLOWED_KEYS = new Set(["platform", "root", "target", "intent", "identity", "evidence", "lock"]);
55
+ // -------------------------------------------------------------------------------------------------
56
+ // Generic plain-data / exact-key / accessor-free guards.
57
+ // -------------------------------------------------------------------------------------------------
58
+ function isPlainDataObject(value) {
59
+ if (typeof value !== "object" || value === null || Array.isArray(value))
60
+ return false;
61
+ const proto = Object.getPrototypeOf(value);
62
+ if (proto !== null && proto !== Object.prototype)
63
+ return false;
64
+ if (Object.getOwnPropertySymbols(value).length > 0)
65
+ return false;
66
+ for (const key of Object.keys(value)) {
67
+ const desc = Object.getOwnPropertyDescriptor(value, key);
68
+ if (desc === undefined || desc.get !== undefined || desc.set !== undefined || !("value" in desc)) {
69
+ return false;
70
+ }
71
+ }
72
+ return true;
73
+ }
74
+ function hasExactKeys(value, keys) {
75
+ const present = Object.keys(value);
76
+ if (present.length !== keys.length)
77
+ return false;
78
+ for (const key of keys) {
79
+ if (!Object.hasOwn(value, key))
80
+ return false;
81
+ }
82
+ return true;
83
+ }
84
+ function isCanonicalDecimal(value) {
85
+ return typeof value === "string" && CANONICAL_DECIMAL_RE.test(value);
86
+ }
87
+ function isSafeNonNegativeInteger(value) {
88
+ return typeof value === "number" && Number.isFinite(value) && Number.isSafeInteger(value) && value >= 0;
89
+ }
90
+ function deepFreeze(value) {
91
+ if (typeof value !== "object" || value === null || Object.isFrozen(value))
92
+ return value;
93
+ if (Array.isArray(value)) {
94
+ for (const item of value)
95
+ deepFreeze(item);
96
+ }
97
+ else {
98
+ for (const child of Object.values(value))
99
+ deepFreeze(child);
100
+ }
101
+ return Object.freeze(value);
102
+ }
103
+ function assertPlainObject(value, label) {
104
+ if (!isPlainDataObject(value)) {
105
+ throw new TypeError(`${label} must be a plain data object without accessors or a foreign prototype`);
106
+ }
107
+ }
108
+ function assertExactKeys(value, keys, label) {
109
+ if (!hasExactKeys(value, keys)) {
110
+ throw new TypeError(`${label} must have exactly the keys: ${keys.join(", ")}`);
111
+ }
112
+ }
113
+ function assertString(value, label) {
114
+ if (typeof value !== "string")
115
+ throw new TypeError(`${label} must be a string`);
116
+ }
117
+ function assertBoolean(value, label) {
118
+ if (typeof value !== "boolean")
119
+ throw new TypeError(`${label} must be a boolean`);
120
+ }
121
+ function assertCanonicalDecimal(value, label) {
122
+ if (!isCanonicalDecimal(value)) {
123
+ throw new TypeError(`${label} must be a canonical decimal string ("0" or a nonzero decimal without leading zeros, up to 20 digits)`);
124
+ }
125
+ }
126
+ function assertSafeNonNegInt(value, label) {
127
+ if (!isSafeNonNegativeInteger(value))
128
+ throw new TypeError(`${label} must be a finite safe non-negative integer`);
129
+ }
130
+ /**
131
+ * Assert a path-shaped evidence string: a string within the 4096 cap and free
132
+ * of C0/DEL control characters. This caps every lexical/realpath evidence
133
+ * string at validation time (fail-closed) before any canonicalization runs.
134
+ */
135
+ function assertPathString(value, label) {
136
+ if (typeof value !== "string")
137
+ throw new TypeError(`${label} must be a string`);
138
+ if (value.length > SESSION_PATH_MAX_LENGTH) {
139
+ throw new TypeError(`${label} exceeds the ${SESSION_PATH_MAX_LENGTH} character cap`);
140
+ }
141
+ if (CONTROL_CHAR_RE.test(value))
142
+ throw new TypeError(`${label} must not contain C0/DEL control characters`);
143
+ }
144
+ /**
145
+ * Strictly validate a plain dense array before any indexed value access: it
146
+ * must be a real Array whose prototype is exactly Array.prototype, carry no
147
+ * symbol properties or extra own properties, and expose every index 0..length-1
148
+ * as an own data descriptor (no holes, no getters/setters). Accessor indices are
149
+ * rejected without their getters ever executing. The length is captured once.
150
+ */
151
+ function assertStrictPlainArray(value, label) {
152
+ if (typeof value !== "object" || value === null || !Array.isArray(value)) {
153
+ throw new TypeError(`${label} must be an array`);
154
+ }
155
+ if (Object.getPrototypeOf(value) !== Array.prototype) {
156
+ throw new TypeError(`${label} must use the Array prototype`);
157
+ }
158
+ if (Object.getOwnPropertySymbols(value).length > 0) {
159
+ throw new TypeError(`${label} must not carry symbol properties`);
160
+ }
161
+ const length = value.length;
162
+ for (const name of Object.getOwnPropertyNames(value)) {
163
+ if (name === "length")
164
+ continue;
165
+ const index = Number(name);
166
+ if (!Number.isSafeInteger(index) || index < 0 || index >= length || String(index) !== name) {
167
+ throw new TypeError(`${label} must not carry extra own properties`);
168
+ }
169
+ }
170
+ for (let i = 0; i < length; i++) {
171
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
172
+ if (descriptor === undefined)
173
+ throw new TypeError(`${label} must be dense (no holes)`);
174
+ if (descriptor.get !== undefined || descriptor.set !== undefined || !("value" in descriptor)) {
175
+ throw new TypeError(`${label} must not expose accessor or non-data element descriptors`);
176
+ }
177
+ }
178
+ }
179
+ // -------------------------------------------------------------------------------------------------
180
+ // Stage 1: pure lexical canonicalizer + segment-boundary containment.
181
+ // -------------------------------------------------------------------------------------------------
182
+ function splitSegments(value) {
183
+ return value.split("/").filter((segment) => segment.length > 0);
184
+ }
185
+ /**
186
+ * Canonicalize a session path lexically for the given platform without any
187
+ * filesystem access. Returns null (fail-closed) for any disallowed form:
188
+ * non-string, empty, longer than 4096, C0/DEL control characters, POSIX
189
+ * non-absolute or above-root `..`, or any Win32 hazard (namespace prefixes,
190
+ * UNC, drive-relative, non-absolute, ADS colons, trailing dot/space, reserved
191
+ * device names, above-drive-root escape). Drive letters are lower-cased.
192
+ */
193
+ export function canonicalizeSessionPath(raw, platform) {
194
+ if (typeof raw !== "string")
195
+ return null;
196
+ if (raw.length === 0 || raw.length > SESSION_PATH_MAX_LENGTH)
197
+ return null;
198
+ if (CONTROL_CHAR_RE.test(raw))
199
+ return null;
200
+ if (platform === "posix")
201
+ return canonicalizePosix(raw);
202
+ if (platform === "win32")
203
+ return canonicalizeWin32(raw);
204
+ return null;
205
+ }
206
+ function canonicalizePosix(raw) {
207
+ if (raw.charCodeAt(0) !== 47 /* '/' */)
208
+ return null;
209
+ const collapsed = [];
210
+ for (const segment of splitSegments(raw)) {
211
+ if (segment === ".")
212
+ continue;
213
+ if (segment === "..") {
214
+ if (collapsed.length === 0)
215
+ return null;
216
+ collapsed.pop();
217
+ continue;
218
+ }
219
+ collapsed.push(segment);
220
+ }
221
+ return `/${collapsed.join("/")}`;
222
+ }
223
+ function canonicalizeWin32(raw) {
224
+ const normalized = raw.replace(/\\/g, "/");
225
+ if (normalized.startsWith("//?/") || normalized.startsWith("//./"))
226
+ return null;
227
+ if (normalized.startsWith("//"))
228
+ return null;
229
+ const match = WIN_DRIVE_ABSOLUTE_RE.exec(normalized);
230
+ if (match === null)
231
+ return null;
232
+ const drive = match[1].toLowerCase();
233
+ const collapsed = [];
234
+ for (const segment of splitSegments(match[2])) {
235
+ if (segment === ".")
236
+ continue;
237
+ if (segment === "..") {
238
+ if (collapsed.length === 0)
239
+ return null;
240
+ collapsed.pop();
241
+ continue;
242
+ }
243
+ if (segment.includes(":"))
244
+ return null;
245
+ if (segment.endsWith(".") || segment.endsWith(" "))
246
+ return null;
247
+ const stem = segment.split(".", 1)[0].toLowerCase();
248
+ if (WIN_RESERVED_NAMES.has(stem))
249
+ return null;
250
+ collapsed.push(segment);
251
+ }
252
+ return `${drive}:/${collapsed.join("/")}`;
253
+ }
254
+ function parseCanonical(canonical, platform) {
255
+ if (platform === "posix") {
256
+ if (canonical.charCodeAt(0) !== 47)
257
+ return null;
258
+ return { drive: null, segments: splitSegments(canonical) };
259
+ }
260
+ if (platform === "win32") {
261
+ const match = WIN_CANONICAL_RE.exec(canonical);
262
+ if (match === null)
263
+ return null;
264
+ return { drive: match[1], segments: splitSegments(match[2]) };
265
+ }
266
+ return null;
267
+ }
268
+ function segmentEqual(left, right, platform) {
269
+ return platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
270
+ }
271
+ function partsContain(root, candidate, platform) {
272
+ if (root.drive !== candidate.drive)
273
+ return false;
274
+ if (candidate.segments.length < root.segments.length)
275
+ return false;
276
+ for (let i = 0; i < root.segments.length; i++) {
277
+ if (!segmentEqual(root.segments[i], candidate.segments[i], platform))
278
+ return false;
279
+ }
280
+ return true;
281
+ }
282
+ /**
283
+ * Canonical path equality: compares drive and every segment using platform
284
+ * semantics (win32 case-insensitive, posix exact). Returns false if either
285
+ * input is null or non-canonical. Unlike a raw string compare, two win32 paths
286
+ * that differ only in segment case are equal here, so mixed-case evidence binds
287
+ * correctly while the canonical output still preserves segment case.
288
+ */
289
+ function canonicalPathEqual(a, b, platform) {
290
+ if (a === null || b === null)
291
+ return false;
292
+ const aParts = parseCanonical(a, platform);
293
+ const bParts = parseCanonical(b, platform);
294
+ if (aParts === null || bParts === null)
295
+ return false;
296
+ if (aParts.drive !== bParts.drive)
297
+ return false;
298
+ if (aParts.segments.length !== bParts.segments.length)
299
+ return false;
300
+ for (let i = 0; i < aParts.segments.length; i++) {
301
+ if (!segmentEqual(aParts.segments[i], bParts.segments[i], platform))
302
+ return false;
303
+ }
304
+ return true;
305
+ }
306
+ /**
307
+ * True when `candidate` is within `root` at a segment boundary (root may equal
308
+ * candidate). Win32 compares segments case-insensitively; POSIX is exact.
309
+ * Cross-drive and any non-canonical input return false (fail-closed).
310
+ */
311
+ export function sessionPathContains(root, candidate, platform) {
312
+ const rootCanonical = canonicalizeSessionPath(root, platform);
313
+ const candidateCanonical = canonicalizeSessionPath(candidate, platform);
314
+ if (rootCanonical === null || candidateCanonical === null)
315
+ return false;
316
+ const rootParts = parseCanonical(rootCanonical, platform);
317
+ const candidateParts = parseCanonical(candidateCanonical, platform);
318
+ if (rootParts === null || candidateParts === null)
319
+ return false;
320
+ return partsContain(rootParts, candidateParts, platform);
321
+ }
322
+ function classifyTarget(rootCanonical, targetCanonical, platform) {
323
+ const rootParts = parseCanonical(rootCanonical, platform);
324
+ const targetParts = parseCanonical(targetCanonical, platform);
325
+ if (rootParts === null || targetParts === null)
326
+ return "external";
327
+ if (!partsContain(rootParts, targetParts, platform))
328
+ return "external";
329
+ if (rootParts.drive === targetParts.drive && rootParts.segments.length === targetParts.segments.length) {
330
+ return "root";
331
+ }
332
+ return "inside";
333
+ }
334
+ // -------------------------------------------------------------------------------------------------
335
+ // Strict evidence / lock validation. Identity is normalized leniently.
336
+ // -------------------------------------------------------------------------------------------------
337
+ function validateStat(value, label) {
338
+ assertPlainObject(value, label);
339
+ assertExactKeys(value, STAT_KEYS, label);
340
+ assertCanonicalDecimal(value.dev, `${label}.dev`);
341
+ assertCanonicalDecimal(value.ino, `${label}.ino`);
342
+ assertSafeNonNegInt(value.nlink, `${label}.nlink`);
343
+ assertSafeNonNegInt(value.size, `${label}.size`);
344
+ assertSafeNonNegInt(value.mtime, `${label}.mtime`);
345
+ assertBoolean(value.regular, `${label}.regular`);
346
+ assertCanonicalDecimal(value.owner, `${label}.owner`);
347
+ return {
348
+ dev: value.dev,
349
+ ino: value.ino,
350
+ nlink: value.nlink,
351
+ size: value.size,
352
+ mtime: value.mtime,
353
+ regular: value.regular,
354
+ owner: value.owner,
355
+ };
356
+ }
357
+ function validateChainEntry(value, label) {
358
+ assertPlainObject(value, label);
359
+ assertExactKeys(value, CHAIN_ENTRY_KEYS, label);
360
+ assertPathString(value.lexical, `${label}.lexical`);
361
+ assertPathString(value.realpath, `${label}.realpath`);
362
+ if (!LINK_KINDS.has(value.linkKind)) {
363
+ throw new TypeError(`${label}.linkKind must be none|symlink|reparse`);
364
+ }
365
+ return { lexical: value.lexical, realpath: value.realpath, linkKind: value.linkKind };
366
+ }
367
+ function validateEvidence(evidence) {
368
+ assertPlainObject(evidence, "evidence");
369
+ assertExactKeys(evidence, EVIDENCE_KEYS, "evidence");
370
+ if (evidence.schemaVersion !== 1)
371
+ throw new TypeError("evidence.schemaVersion must be 1");
372
+ if (!PLATFORMS.has(evidence.platform)) {
373
+ throw new TypeError("evidence.platform must be posix|win32");
374
+ }
375
+ assertPathString(evidence.trustedRootLexical, "evidence.trustedRootLexical");
376
+ assertPathString(evidence.trustedRootRealpath, "evidence.trustedRootRealpath");
377
+ assertPlainObject(evidence.target, "evidence.target");
378
+ assertExactKeys(evidence.target, TARGET_KEYS, "evidence.target");
379
+ assertPathString(evidence.target.lexical, "evidence.target.lexical");
380
+ assertPathString(evidence.target.realpath, "evidence.target.realpath");
381
+ if (!Array.isArray(evidence.chain))
382
+ throw new TypeError("evidence.chain must be an array");
383
+ const chainLength = evidence.chain.length;
384
+ if (chainLength === 0)
385
+ throw new TypeError("evidence.chain must be non-empty");
386
+ if (chainLength > MAX_SESSION_PATH_CHAIN_ENTRIES) {
387
+ throw new TypeError(`evidence.chain exceeds MAX_SESSION_PATH_CHAIN_ENTRIES (${MAX_SESSION_PATH_CHAIN_ENTRIES})`);
388
+ }
389
+ assertStrictPlainArray(evidence.chain, "evidence.chain");
390
+ const chain = [];
391
+ for (let index = 0; index < chainLength; index++) {
392
+ chain.push(validateChainEntry(evidence.chain[index], `evidence.chain[${index}]`));
393
+ }
394
+ const statBefore = validateStat(evidence.statBefore, "evidence.statBefore");
395
+ let statAfter = null;
396
+ if (evidence.statAfter !== null) {
397
+ statAfter = validateStat(evidence.statAfter, "evidence.statAfter");
398
+ }
399
+ assertPlainObject(evidence.opened, "evidence.opened");
400
+ assertExactKeys(evidence.opened, OPENED_KEYS, "evidence.opened");
401
+ assertCanonicalDecimal(evidence.opened.dev, "evidence.opened.dev");
402
+ assertCanonicalDecimal(evidence.opened.ino, "evidence.opened.ino");
403
+ return {
404
+ schemaVersion: 1,
405
+ platform: evidence.platform,
406
+ trustedRootLexical: evidence.trustedRootLexical,
407
+ trustedRootRealpath: evidence.trustedRootRealpath,
408
+ target: { lexical: evidence.target.lexical, realpath: evidence.target.realpath },
409
+ chain,
410
+ statBefore,
411
+ statAfter,
412
+ opened: { dev: evidence.opened.dev, ino: evidence.opened.ino },
413
+ };
414
+ }
415
+ function validateLock(lock) {
416
+ assertPlainObject(lock, "lock");
417
+ assertExactKeys(lock, LOCK_KEYS, "lock");
418
+ if (!LOCK_STATES.has(lock.state)) {
419
+ throw new TypeError("lock.state must be absent|live|stale|foreign|unknown");
420
+ }
421
+ assertBoolean(lock.sameHost, "lock.sameHost");
422
+ assertBoolean(lock.pidDefinitelyAbsent, "lock.pidDefinitelyAbsent");
423
+ if (lock.holderPid !== null) {
424
+ assertCanonicalDecimal(lock.holderPid, "lock.holderPid");
425
+ }
426
+ return {
427
+ state: lock.state,
428
+ sameHost: lock.sameHost,
429
+ pidDefinitelyAbsent: lock.pidDefinitelyAbsent,
430
+ holderPid: lock.holderPid === null ? null : lock.holderPid,
431
+ };
432
+ }
433
+ /**
434
+ * Identity is normalized leniently: a missing or structurally incomplete
435
+ * identity yields undefined, which the policy treats as "not provided". This
436
+ * matches the requirement that missing/incomplete identity *blocks* privileged
437
+ * intents rather than throwing.
438
+ */
439
+ function normalizeIdentity(value) {
440
+ if (!isPlainDataObject(value))
441
+ return undefined;
442
+ if (!hasExactKeys(value, ["owner"]))
443
+ return undefined;
444
+ if (!isCanonicalDecimal(value.owner))
445
+ return undefined;
446
+ return { owner: value.owner };
447
+ }
448
+ function validateInput(rawInput) {
449
+ assertPlainObject(rawInput, "input");
450
+ for (const key of Object.keys(rawInput)) {
451
+ if (!INPUT_ALLOWED_KEYS.has(key))
452
+ throw new TypeError(`input has an unknown key: ${key}`);
453
+ }
454
+ if (!PLATFORMS.has(rawInput.platform)) {
455
+ throw new TypeError("input.platform must be posix|win32");
456
+ }
457
+ assertString(rawInput.root, "input.root");
458
+ assertString(rawInput.target, "input.target");
459
+ if (!INTENTS.has(rawInput.intent)) {
460
+ throw new TypeError("input.intent must be inspect_metadata|inspect_content|repair|repair_dry_run");
461
+ }
462
+ if (!Object.hasOwn(rawInput, "evidence")) {
463
+ throw new TypeError("input.evidence is required");
464
+ }
465
+ const evidence = validateEvidence(rawInput.evidence);
466
+ let identity;
467
+ if (Object.hasOwn(rawInput, "identity") && rawInput.identity !== undefined) {
468
+ identity = normalizeIdentity(rawInput.identity);
469
+ }
470
+ let lock;
471
+ if (Object.hasOwn(rawInput, "lock") && rawInput.lock !== undefined) {
472
+ lock = validateLock(rawInput.lock);
473
+ }
474
+ return {
475
+ platform: rawInput.platform,
476
+ root: rawInput.root,
477
+ target: rawInput.target,
478
+ intent: rawInput.intent,
479
+ identity,
480
+ evidence,
481
+ lock,
482
+ };
483
+ }
484
+ // -------------------------------------------------------------------------------------------------
485
+ // Decision assembly.
486
+ // -------------------------------------------------------------------------------------------------
487
+ function statEqual(a, b) {
488
+ return (a.dev === b.dev &&
489
+ a.ino === b.ino &&
490
+ a.nlink === b.nlink &&
491
+ a.size === b.size &&
492
+ a.mtime === b.mtime &&
493
+ a.regular === b.regular &&
494
+ a.owner === b.owner);
495
+ }
496
+ function buildDecision(fields) {
497
+ const decision = {
498
+ schemaVersion: SESSION_PATH_POLICY_SCHEMA_VERSION,
499
+ platform: fields.platform,
500
+ intent: fields.intent,
501
+ status: fields.status,
502
+ reason: fields.reason,
503
+ dryRun: fields.intent === "repair_dry_run",
504
+ classification: fields.classification,
505
+ targetLexical: fields.evidence.target.lexical,
506
+ targetRealpath: fields.evidence.target.realpath,
507
+ trustedRootLexical: fields.evidence.trustedRootLexical,
508
+ trustedRootRealpath: fields.evidence.trustedRootRealpath,
509
+ capabilities: {
510
+ canInspectMetadata: fields.canInspectMetadata,
511
+ canReadContents: fields.canReadContents,
512
+ canRepair: fields.canRepair,
513
+ },
514
+ plannedActions: fields.plannedActions.map((action) => ({ kind: action.kind, holderPid: action.holderPid })),
515
+ scheduledWrites: fields.scheduledWrites,
516
+ };
517
+ return deepFreeze(decision);
518
+ }
519
+ function rejected(platform, intent, reason, evidence, classification = "external") {
520
+ return buildDecision({
521
+ platform,
522
+ intent,
523
+ status: "rejected",
524
+ reason,
525
+ classification,
526
+ evidence,
527
+ canInspectMetadata: false,
528
+ canReadContents: false,
529
+ canRepair: false,
530
+ plannedActions: [],
531
+ scheduledWrites: 0,
532
+ });
533
+ }
534
+ function blocked(platform, intent, reason, evidence, classification) {
535
+ return buildDecision({
536
+ platform,
537
+ intent,
538
+ status: "blocked",
539
+ reason,
540
+ classification,
541
+ evidence,
542
+ canInspectMetadata: false,
543
+ canReadContents: false,
544
+ canRepair: false,
545
+ plannedActions: [],
546
+ scheduledWrites: 0,
547
+ });
548
+ }
549
+ function metadataOnly(platform, intent, evidence, classification) {
550
+ return buildDecision({
551
+ platform,
552
+ intent,
553
+ status: "metadata_only",
554
+ reason: "metadata_only",
555
+ classification,
556
+ evidence,
557
+ canInspectMetadata: true,
558
+ canReadContents: false,
559
+ canRepair: false,
560
+ plannedActions: [],
561
+ scheduledWrites: 0,
562
+ });
563
+ }
564
+ function authorized(platform, intent, evidence, classification, canReadContents, canRepair, plannedActions, scheduledWrites) {
565
+ return buildDecision({
566
+ platform,
567
+ intent,
568
+ status: "authorized",
569
+ reason: "authorized",
570
+ classification,
571
+ evidence,
572
+ canInspectMetadata: true,
573
+ canReadContents,
574
+ canRepair,
575
+ plannedActions,
576
+ scheduledWrites,
577
+ });
578
+ }
579
+ /**
580
+ * True when `childCanonical` is exactly one segment deeper than
581
+ * `parentCanonical` and shares its entire prefix (a direct child at a segment
582
+ * boundary). Win32 compares segments case-insensitively.
583
+ */
584
+ function isDirectChild(parentCanonical, childCanonical, platform) {
585
+ const parentParts = parseCanonical(parentCanonical, platform);
586
+ const childParts = parseCanonical(childCanonical, platform);
587
+ if (parentParts === null || childParts === null)
588
+ return false;
589
+ if (parentParts.drive !== childParts.drive)
590
+ return false;
591
+ if (childParts.segments.length !== parentParts.segments.length + 1)
592
+ return false;
593
+ for (let i = 0; i < parentParts.segments.length; i++) {
594
+ if (!segmentEqual(parentParts.segments[i], childParts.segments[i], platform))
595
+ return false;
596
+ }
597
+ return true;
598
+ }
599
+ /**
600
+ * Strict chain binding (P3). The configured root is conservative (no symlinks),
601
+ * so its lexical and real canonical forms must be identical. The chain must
602
+ * start at the roots, end at the target lexical/real, have exactly the lexical
603
+ * depth-delta plus one entries, and every adjacent lexical path and realpath
604
+ * must be a direct child at a segment boundary. Every entry's lexical path must
605
+ * stay inside the lexical root and every realpath inside the real root. Any
606
+ * linkKind other than "none" blocks. Returns the first closed reason, or null
607
+ * when the chain is fully bound.
608
+ */
609
+ function bindChain(evidence, rootCanonical, rootRealpathCanonical, targetCanonical, targetRealpathCanonical, platform) {
610
+ if (!canonicalPathEqual(rootCanonical, rootRealpathCanonical, platform))
611
+ return "evidence_mismatch";
612
+ const rootParts = parseCanonical(rootCanonical, platform);
613
+ const targetLexicalParts = parseCanonical(targetCanonical, platform);
614
+ if (rootParts === null || targetLexicalParts === null)
615
+ return "evidence_mismatch";
616
+ const expectedLength = targetLexicalParts.segments.length - rootParts.segments.length + 1;
617
+ if (evidence.chain.length !== expectedLength)
618
+ return "evidence_mismatch";
619
+ const firstLexical = canonicalizeSessionPath(evidence.chain[0].lexical, platform);
620
+ const firstReal = canonicalizeSessionPath(evidence.chain[0].realpath, platform);
621
+ if (firstLexical === null || firstReal === null)
622
+ return "evidence_mismatch";
623
+ if (!canonicalPathEqual(firstLexical, rootCanonical, platform) ||
624
+ !canonicalPathEqual(firstReal, rootRealpathCanonical, platform))
625
+ return "evidence_mismatch";
626
+ const last = evidence.chain[evidence.chain.length - 1];
627
+ const lastLexical = canonicalizeSessionPath(last.lexical, platform);
628
+ const lastReal = canonicalizeSessionPath(last.realpath, platform);
629
+ if (!canonicalPathEqual(lastLexical, targetCanonical, platform) ||
630
+ !canonicalPathEqual(lastReal, targetRealpathCanonical, platform))
631
+ return "evidence_mismatch";
632
+ let prevLexical = firstLexical;
633
+ let prevReal = firstReal;
634
+ for (let index = 0; index < evidence.chain.length; index++) {
635
+ const entry = evidence.chain[index];
636
+ if (entry.linkKind !== "none")
637
+ return "symlink_in_chain";
638
+ const entryLexical = canonicalizeSessionPath(entry.lexical, platform);
639
+ const entryReal = canonicalizeSessionPath(entry.realpath, platform);
640
+ if (entryLexical === null || entryReal === null)
641
+ return "evidence_mismatch";
642
+ const entryLexicalParts = parseCanonical(entryLexical, platform);
643
+ const entryRealParts = parseCanonical(entryReal, platform);
644
+ if (entryLexicalParts === null || entryRealParts === null)
645
+ return "evidence_mismatch";
646
+ if (!partsContain(rootParts, entryLexicalParts, platform))
647
+ return "evidence_mismatch";
648
+ if (!partsContain(rootParts, entryRealParts, platform))
649
+ return "outside_realpath";
650
+ if (index > 0) {
651
+ if (!isDirectChild(prevLexical, entryLexical, platform))
652
+ return "evidence_mismatch";
653
+ if (!isDirectChild(prevReal, entryReal, platform))
654
+ return "evidence_mismatch";
655
+ }
656
+ prevLexical = entryLexical;
657
+ prevReal = entryReal;
658
+ }
659
+ return null;
660
+ }
661
+ /**
662
+ * Lock coherence, fail-closed (P5). Each state is only meaningful with the
663
+ * field values that justify it; a self-contradictory lock is treated as
664
+ * untrustworthy and closed. `absent` and an eligible `stale` return null
665
+ * (proceed); every other state (including any contradiction) returns a closed
666
+ * reason rather than throwing.
667
+ */
668
+ function evaluateLock(lock) {
669
+ switch (lock.state) {
670
+ case "absent":
671
+ return lock.holderPid === null && lock.pidDefinitelyAbsent ? null : "lock_unknown";
672
+ case "live":
673
+ return lock.sameHost && lock.holderPid !== null && !lock.pidDefinitelyAbsent ? "lock_live" : "lock_unknown";
674
+ case "foreign":
675
+ return !lock.sameHost ? "lock_foreign" : "lock_unknown";
676
+ case "stale":
677
+ return lock.sameHost && lock.holderPid !== null && lock.pidDefinitelyAbsent ? null : "lock_stale_ineligible";
678
+ case "unknown":
679
+ return "lock_unknown";
680
+ default:
681
+ return "lock_unknown";
682
+ }
683
+ }
684
+ function plannedActionsForLock(lock) {
685
+ if (lock.state === "stale" && lock.holderPid !== null) {
686
+ return [{ kind: "remove_stale_lock", holderPid: lock.holderPid }];
687
+ }
688
+ return [];
689
+ }
690
+ /**
691
+ * Decide session path access from attested plain-data evidence. Pure: never
692
+ * performs I/O and never executes a repair. The returned decision is a
693
+ * deep-frozen copy.
694
+ *
695
+ * Classification precedence (P0): the lexical classification of the request
696
+ * root/target is computed first and is authoritative for privileged intents.
697
+ * `inspect_metadata` always resolves to metadata_only after lexical/evidence
698
+ * binding. For `inspect_content`/`repair`/`repair_dry_run` the lexical target
699
+ * must be strictly inside the root (external => target_external, root =>
700
+ * target_is_root) before identity, and the realpath classification must
701
+ * independently be inside; an injected realpath can never upgrade an external
702
+ * lexical target. `repair_dry_run` shares repair's eligibility, status, reason,
703
+ * and planned actions, but reports canRepair=false and zero scheduled writes.
704
+ */
705
+ export function decideSessionPathAccess(rawInput) {
706
+ const { platform, root, target, intent, identity, evidence, lock } = validateInput(rawInput);
707
+ const rootCanonical = canonicalizeSessionPath(root, platform);
708
+ const targetCanonical = canonicalizeSessionPath(target, platform);
709
+ if (rootCanonical === null || targetCanonical === null) {
710
+ return rejected(platform, intent, "lexical_invalid", evidence);
711
+ }
712
+ if (evidence.platform !== platform) {
713
+ return blocked(platform, intent, "evidence_mismatch", evidence, "external");
714
+ }
715
+ const evidenceRootCanonical = canonicalizeSessionPath(evidence.trustedRootLexical, platform);
716
+ const evidenceTargetCanonical = canonicalizeSessionPath(evidence.target.lexical, platform);
717
+ const rootRealpathCanonical = canonicalizeSessionPath(evidence.trustedRootRealpath, platform);
718
+ const targetRealpathCanonical = canonicalizeSessionPath(evidence.target.realpath, platform);
719
+ if (!canonicalPathEqual(evidenceRootCanonical, rootCanonical, platform) ||
720
+ !canonicalPathEqual(evidenceTargetCanonical, targetCanonical, platform) ||
721
+ rootRealpathCanonical === null ||
722
+ targetRealpathCanonical === null) {
723
+ return blocked(platform, intent, "evidence_mismatch", evidence, "external");
724
+ }
725
+ const lexicalClassification = classifyTarget(rootCanonical, targetCanonical, platform);
726
+ const isDryRun = intent === "repair_dry_run";
727
+ const privilegedIntent = isDryRun ? "repair" : intent;
728
+ if (privilegedIntent === "inspect_metadata") {
729
+ return metadataOnly(platform, intent, evidence, lexicalClassification);
730
+ }
731
+ // inspect_content, repair, repair_dry_run: lexical classification is primary (P0).
732
+ if (lexicalClassification === "external") {
733
+ return rejected(platform, intent, "target_external", evidence, "external");
734
+ }
735
+ if (lexicalClassification === "root") {
736
+ return rejected(platform, intent, "target_is_root", evidence, "root");
737
+ }
738
+ // P0: independently require the realpath classification to be strictly inside;
739
+ // an injected realpath can never upgrade an inside lexical target.
740
+ const realClassification = classifyTarget(rootRealpathCanonical, targetRealpathCanonical, platform);
741
+ if (realClassification !== "inside") {
742
+ return blocked(platform, intent, "outside_realpath", evidence, "inside");
743
+ }
744
+ if (identity === undefined) {
745
+ return blocked(platform, intent, "identity_required", evidence, "inside");
746
+ }
747
+ if (lock === undefined) {
748
+ return blocked(platform, intent, "lock_required", evidence, "inside");
749
+ }
750
+ const chainReason = bindChain(evidence, rootCanonical, rootRealpathCanonical, targetCanonical, targetRealpathCanonical, platform);
751
+ if (chainReason !== null) {
752
+ return blocked(platform, intent, chainReason, evidence, "inside");
753
+ }
754
+ if (!evidence.statBefore.regular) {
755
+ return blocked(platform, intent, "nonregular", evidence, "inside");
756
+ }
757
+ if (evidence.statBefore.nlink !== 1) {
758
+ return blocked(platform, intent, "nlink_not_one", evidence, "inside");
759
+ }
760
+ if (identity.owner !== evidence.statBefore.owner) {
761
+ return blocked(platform, intent, "owner_mismatch", evidence, "inside");
762
+ }
763
+ if (evidence.opened.dev !== evidence.statBefore.dev || evidence.opened.ino !== evidence.statBefore.ino) {
764
+ return blocked(platform, intent, "stat_race", evidence, "inside");
765
+ }
766
+ if (evidence.statAfter === null || !statEqual(evidence.statBefore, evidence.statAfter)) {
767
+ return blocked(platform, intent, "stat_after_mismatch", evidence, "inside");
768
+ }
769
+ const lockBlock = evaluateLock(lock);
770
+ if (lockBlock !== null) {
771
+ return blocked(platform, intent, lockBlock, evidence, "inside");
772
+ }
773
+ if (privilegedIntent === "inspect_content") {
774
+ return authorized(platform, intent, evidence, "inside", true, false, [], 0);
775
+ }
776
+ const plannedActions = plannedActionsForLock(lock);
777
+ const scheduledWrites = isDryRun ? 0 : plannedActions.length;
778
+ return authorized(platform, intent, evidence, "inside", false, !isDryRun, plannedActions, scheduledWrites);
779
+ }
780
+ //# sourceMappingURL=session-path-policy.js.map