@rryando/arcs 3.11.0 → 4.0.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 (191) hide show
  1. package/dist/cli/commands/hooks.d.ts +46 -6
  2. package/dist/cli/commands/hooks.d.ts.map +1 -1
  3. package/dist/cli/commands/hooks.js +190 -52
  4. package/dist/cli/commands/hooks.js.map +1 -1
  5. package/dist/cli/commands/web.js +14 -4
  6. package/dist/cli/commands/web.js.map +1 -1
  7. package/dist/cli/config.d.ts +14 -3
  8. package/dist/cli/config.d.ts.map +1 -1
  9. package/dist/cli/config.js +39 -38
  10. package/dist/cli/config.js.map +1 -1
  11. package/dist/cli/instructions.d.ts +13 -1
  12. package/dist/cli/instructions.d.ts.map +1 -1
  13. package/dist/cli/instructions.js +38 -3
  14. package/dist/cli/instructions.js.map +1 -1
  15. package/dist/cli/setup.d.ts.map +1 -1
  16. package/dist/cli/setup.js +76 -11
  17. package/dist/cli/setup.js.map +1 -1
  18. package/dist/shared/session-vocabulary.d.ts +88 -0
  19. package/dist/shared/session-vocabulary.d.ts.map +1 -0
  20. package/dist/shared/session-vocabulary.js +114 -0
  21. package/dist/shared/session-vocabulary.js.map +1 -0
  22. package/dist/utils/claude-code-hook-install.d.ts +1 -1
  23. package/dist/utils/claude-code-hook-install.d.ts.map +1 -1
  24. package/dist/utils/claude-code-hook-install.js +68 -3
  25. package/dist/utils/claude-code-hook-install.js.map +1 -1
  26. package/dist/utils/claude-transcript.d.ts +81 -2
  27. package/dist/utils/claude-transcript.d.ts.map +1 -1
  28. package/dist/utils/claude-transcript.js +33 -2
  29. package/dist/utils/claude-transcript.js.map +1 -1
  30. package/dist/utils/git.d.ts +21 -0
  31. package/dist/utils/git.d.ts.map +1 -1
  32. package/dist/utils/git.js +65 -1
  33. package/dist/utils/git.js.map +1 -1
  34. package/dist/utils/hook-contract.d.ts +36 -0
  35. package/dist/utils/hook-contract.d.ts.map +1 -0
  36. package/dist/utils/hook-contract.js +35 -0
  37. package/dist/utils/hook-contract.js.map +1 -0
  38. package/dist/utils/hook-token-store.d.ts +39 -0
  39. package/dist/utils/hook-token-store.d.ts.map +1 -1
  40. package/dist/utils/hook-token-store.js +46 -3
  41. package/dist/utils/hook-token-store.js.map +1 -1
  42. package/dist/utils/session-store.d.ts +226 -0
  43. package/dist/utils/session-store.d.ts.map +1 -1
  44. package/dist/utils/session-store.js +266 -1
  45. package/dist/utils/session-store.js.map +1 -1
  46. package/dist/web-client/assets/{GraphCanvas-dNyZ458L.js → GraphCanvas-CTyf_XXQ.js} +1 -1
  47. package/dist/web-client/assets/{MarkdownEditor-BmU9mdkN.js → MarkdownEditor-af2vwQOX.js} +1 -1
  48. package/dist/web-client/assets/{abnfDiagram-VRR7QNED-D1BFBoeF.js → abnfDiagram-VRR7QNED-CdxcKX9t.js} +1 -1
  49. package/dist/web-client/assets/architecture-TIHT7OUA-Bt2-DmfV.js +1 -0
  50. package/dist/web-client/assets/{architectureDiagram-ZJ3FMSHR-CA8hTWUK.js → architectureDiagram-ZJ3FMSHR-DOISDv6o.js} +1 -1
  51. package/dist/web-client/assets/{blockDiagram-677ZJIJ3-CQpb_KwX.js → blockDiagram-677ZJIJ3-DKwtbttM.js} +1 -1
  52. package/dist/web-client/assets/{c4Diagram-LMCZKHZV-BhpHX84V.js → c4Diagram-LMCZKHZV-CilqK-Mm.js} +1 -1
  53. package/dist/web-client/assets/channel-C2mb9np_.js +1 -0
  54. package/dist/web-client/assets/{chunk-32BRIVSS-BUusQQa_.js → chunk-32BRIVSS-C79m1mkG.js} +1 -1
  55. package/dist/web-client/assets/{chunk-52WLFC77-nNYwlftl.js → chunk-52WLFC77-C6WelGWJ.js} +1 -1
  56. package/dist/web-client/assets/{chunk-C7G6YPKG-D0a-yqnK.js → chunk-C7G6YPKG-DLg7ryWI.js} +1 -1
  57. package/dist/web-client/assets/{chunk-EX3LRPZG-IDuRMN-4.js → chunk-EX3LRPZG-DjWgo4gL.js} +1 -1
  58. package/dist/web-client/assets/{chunk-FWX5IMBZ-D_spTcqi.js → chunk-FWX5IMBZ-BgS9p_zy.js} +2 -2
  59. package/dist/web-client/assets/{chunk-HOUHSVGY-C-bcxwZS.js → chunk-HOUHSVGY-DuVR7dZO.js} +1 -1
  60. package/dist/web-client/assets/{chunk-ICXQ74PX-CjmK2bAM.js → chunk-ICXQ74PX-Y8DlnIJM.js} +1 -1
  61. package/dist/web-client/assets/{chunk-MOJQB5TN-Bkc08KWY.js → chunk-MOJQB5TN-BEM3QgeD.js} +1 -1
  62. package/dist/web-client/assets/{chunk-OGEWGWER-BAAYYvG9.js → chunk-OGEWGWER-DlM8LxGr.js} +1 -1
  63. package/dist/web-client/assets/{chunk-PUDLZKDR-WXbPY7NM.js → chunk-PUDLZKDR-c6cqNVTx.js} +1 -1
  64. package/dist/web-client/assets/{chunk-Q4XR5HBZ-DcbnjxQE.js → chunk-Q4XR5HBZ-C5lNmcka.js} +1 -1
  65. package/dist/web-client/assets/{chunk-V7JOEXUC-C6t75PAp.js → chunk-V7JOEXUC-nSswxvSG.js} +1 -1
  66. package/dist/web-client/assets/{chunk-VAUOI2AC-DVrJ0Ic7.js → chunk-VAUOI2AC-CqJkCkT0.js} +1 -1
  67. package/dist/web-client/assets/{chunk-VR4S4FIN-MOOFvGS0.js → chunk-VR4S4FIN-D3pFchin.js} +1 -1
  68. package/dist/web-client/assets/{chunk-WYO6CB5R-sK7Y2NZD.js → chunk-WYO6CB5R-BI9c-NzI.js} +1 -1
  69. package/dist/web-client/assets/{chunk-ZGVPDNZ5-a13RQsku.js → chunk-ZGVPDNZ5-CKTF2kLR.js} +1 -1
  70. package/dist/web-client/assets/classDiagram-OUVF2IWQ-CPd_w3Zr.js +1 -0
  71. package/dist/web-client/assets/classDiagram-v2-EOCWNBFH-CPd_w3Zr.js +1 -0
  72. package/dist/web-client/assets/{cynefin-VYW2F7L2-D8xaH-wO.js → cynefin-VYW2F7L2-BrJrnfh1.js} +1 -1
  73. package/dist/web-client/assets/{cynefinDiagram-TSTJHNR4-Bt__EqJW.js → cynefinDiagram-TSTJHNR4-9jYzPWJZ.js} +1 -1
  74. package/dist/web-client/assets/{dagre-VKFMJZFB-Bwgjwflz.js → dagre-VKFMJZFB-B705Djpu.js} +1 -1
  75. package/dist/web-client/assets/{diagram-FQU43EPY-CaPDVUq2.js → diagram-FQU43EPY-BfQAlUlJ.js} +1 -1
  76. package/dist/web-client/assets/{diagram-G47NLZAW-BQLB9YYA.js → diagram-G47NLZAW-s1YDe06A.js} +1 -1
  77. package/dist/web-client/assets/{diagram-NH7WQ7WH-BWo84w8Y.js → diagram-NH7WQ7WH-Dy3z11Hc.js} +1 -1
  78. package/dist/web-client/assets/{diagram-OA4YK3LP-bKn6Pz5s.js → diagram-OA4YK3LP-Bius2xUN.js} +1 -1
  79. package/dist/web-client/assets/{diagram-WEI45ONY-BRqV5Oy6.js → diagram-WEI45ONY-D7x4VcHM.js} +1 -1
  80. package/dist/web-client/assets/{ebnfDiagram-CCIWWBDH-DhUXL1-7.js → ebnfDiagram-CCIWWBDH-CBg1xrmD.js} +1 -1
  81. package/dist/web-client/assets/{erDiagram-Q63AITRT-HXAQQ-_F.js → erDiagram-Q63AITRT-CJRvTFvd.js} +1 -1
  82. package/dist/web-client/assets/eventmodeling-45OFAUF4-CTKRC2Yj.js +1 -0
  83. package/dist/web-client/assets/flowDiagram-23GEKE2U-CaZjFyU1.js +1 -0
  84. package/dist/web-client/assets/{ganttDiagram-NO4QXBWP-D-Ddf_Ii.js → ganttDiagram-NO4QXBWP-C_LsypZ4.js} +1 -1
  85. package/dist/web-client/assets/{gitGraph-TEB2WS4Q-BmHzs0uF.js → gitGraph-TEB2WS4Q-Dx2XxdGk.js} +1 -1
  86. package/dist/web-client/assets/{gitGraphDiagram-IHSO6WYX-YnQWrlh_.js → gitGraphDiagram-IHSO6WYX-DiKWkGWQ.js} +1 -1
  87. package/dist/web-client/assets/{index-DCWxuIeQ.js → index-CYwhkPtc.js} +38 -36
  88. package/dist/web-client/assets/index-wSzUPvml.css +2 -0
  89. package/dist/web-client/assets/{info-DKCQHKI2-DCT_B7RN.js → info-DKCQHKI2-DORwHenK.js} +1 -1
  90. package/dist/web-client/assets/{infoDiagram-FWYZ7A6U-D-le1Zhq.js → infoDiagram-FWYZ7A6U-CQecXS1E.js} +1 -1
  91. package/dist/web-client/assets/{ishikawaDiagram-FXEZZL3T-Jr1x2VJB.js → ishikawaDiagram-FXEZZL3T-CWhj60Zp.js} +1 -1
  92. package/dist/web-client/assets/{journeyDiagram-5HDEW3XC-BF9ELxj-.js → journeyDiagram-5HDEW3XC-Cj3z2U8u.js} +1 -1
  93. package/dist/web-client/assets/{kanban-definition-HUTT4EX6-C4fJqAxu.js → kanban-definition-HUTT4EX6-DFmBRenP.js} +1 -1
  94. package/dist/web-client/assets/{line-7N7ikFxa.js → line-C_Hxz9xb.js} +1 -1
  95. package/dist/web-client/assets/{mermaid-parser.core-QbC1icPt.js → mermaid-parser.core-E3xRdzPn.js} +3 -3
  96. package/dist/web-client/assets/{mermaid.core-C26d_UJm.js → mermaid.core-C2_CcotH.js} +3 -3
  97. package/dist/web-client/assets/{mindmap-definition-LN4V7U3C-D6TV1JDf.js → mindmap-definition-LN4V7U3C-Bs7VxPid.js} +1 -1
  98. package/dist/web-client/assets/{packet-7NZHBO7P-CR1vrGj3.js → packet-7NZHBO7P-D2nqso7i.js} +1 -1
  99. package/dist/web-client/assets/{pegDiagram-2B236MQR-xOMBBtfV.js → pegDiagram-2B236MQR-Cepo1sIu.js} +1 -1
  100. package/dist/web-client/assets/{pie-RZYD4A2V-BbWuhjwy.js → pie-RZYD4A2V-Sx0IGbXt.js} +1 -1
  101. package/dist/web-client/assets/{pieDiagram-ENE6RG2P-MsfnsqgW.js → pieDiagram-ENE6RG2P-D-PMDNhk.js} +1 -1
  102. package/dist/web-client/assets/{quadrantDiagram-ABIIQ3AL-BoI7zKXF.js → quadrantDiagram-ABIIQ3AL-BNGC7Fms.js} +1 -1
  103. package/dist/web-client/assets/{radar-I7S5WNFK-CbYXKToJ.js → radar-I7S5WNFK-CFf_-2OC.js} +1 -1
  104. package/dist/web-client/assets/{railroad-3IZDKUUU-6LxHDkLe.js → railroad-3IZDKUUU-BvZeVRGh.js} +1 -1
  105. package/dist/web-client/assets/railroad-abnf-AHOZXSZD-DnGh6x2O.js +1 -0
  106. package/dist/web-client/assets/railroad-ebnf-EBAXGLYW-CGfgCoaE.js +1 -0
  107. package/dist/web-client/assets/railroad-peg-LSFZ7HO6-BzQxYdqp.js +1 -0
  108. package/dist/web-client/assets/{railroadDiagram-RFXS5EU6-D6RUoUki.js → railroadDiagram-RFXS5EU6-D0iK_k84.js} +1 -1
  109. package/dist/web-client/assets/{requirementDiagram-TGXJPOKE-B6k4BDpE.js → requirementDiagram-TGXJPOKE-BPdsw9tz.js} +1 -1
  110. package/dist/web-client/assets/{sankeyDiagram-HTMAVEWB-BUDF-UFr.js → sankeyDiagram-HTMAVEWB-maA6ukOJ.js} +1 -1
  111. package/dist/web-client/assets/{sequenceDiagram-DBY2YBRQ-D6GqcsUi.js → sequenceDiagram-DBY2YBRQ-DKmAjmgr.js} +1 -1
  112. package/dist/web-client/assets/{stateDiagram-2N3HPSRC-WfJCQAK5.js → stateDiagram-2N3HPSRC-esxmzk3w.js} +1 -1
  113. package/dist/web-client/assets/stateDiagram-v2-6OUMAXLB-hcBqlOLc.js +1 -0
  114. package/dist/web-client/assets/{swimlanes-5IMT3BWC-BtMo82mC.js → swimlanes-5IMT3BWC-DFd-wGBv.js} +1 -1
  115. package/dist/web-client/assets/swimlanesDiagram-G3AALYLV-DbSrPBYr.js +8 -0
  116. package/dist/web-client/assets/{timeline-definition-FHXFAJF6-CoAmv2Sn.js → timeline-definition-FHXFAJF6-C7SG8MQH.js} +1 -1
  117. package/dist/web-client/assets/{treeView-QDETBFTQ-BWsKzE1s.js → treeView-QDETBFTQ-M0rhxbs2.js} +1 -1
  118. package/dist/web-client/assets/{treemap-6X3UGDF4-i_qGtB3o.js → treemap-6X3UGDF4-BQfkQxSh.js} +1 -1
  119. package/dist/web-client/assets/{vennDiagram-L72KCM5P-DYkiLe-P.js → vennDiagram-L72KCM5P-CyIlb8ju.js} +1 -1
  120. package/dist/web-client/assets/{wardley-OPB4EBWU-Daaqr1Vp.js → wardley-OPB4EBWU-EE-7q8M1.js} +1 -1
  121. package/dist/web-client/assets/{wardleyDiagram-EHGQE667-Bofbsg3J.js → wardleyDiagram-EHGQE667-bY8XdYYS.js} +1 -1
  122. package/dist/web-client/assets/{xychartDiagram-FW5EYKEG-KNF4VTfL.js → xychartDiagram-FW5EYKEG-AhuXJ3de.js} +1 -1
  123. package/dist/web-client/index.html +2 -2
  124. package/dist/web-server/app.d.ts +4 -1
  125. package/dist/web-server/app.d.ts.map +1 -1
  126. package/dist/web-server/app.js +19 -0
  127. package/dist/web-server/app.js.map +1 -1
  128. package/dist/web-server/claude-runner.d.ts +109 -7
  129. package/dist/web-server/claude-runner.d.ts.map +1 -1
  130. package/dist/web-server/claude-runner.js +329 -50
  131. package/dist/web-server/claude-runner.js.map +1 -1
  132. package/dist/web-server/index.d.ts +2 -2
  133. package/dist/web-server/index.d.ts.map +1 -1
  134. package/dist/web-server/index.js +3 -2
  135. package/dist/web-server/index.js.map +1 -1
  136. package/dist/web-server/permission-policy.d.ts +46 -0
  137. package/dist/web-server/permission-policy.d.ts.map +1 -0
  138. package/dist/web-server/permission-policy.js +96 -0
  139. package/dist/web-server/permission-policy.js.map +1 -0
  140. package/dist/web-server/prompt-assembly.d.ts +454 -0
  141. package/dist/web-server/prompt-assembly.d.ts.map +1 -0
  142. package/dist/web-server/prompt-assembly.js +1122 -0
  143. package/dist/web-server/prompt-assembly.js.map +1 -0
  144. package/dist/web-server/routes/hook-events.d.ts +28 -3
  145. package/dist/web-server/routes/hook-events.d.ts.map +1 -1
  146. package/dist/web-server/routes/hook-events.js +209 -24
  147. package/dist/web-server/routes/hook-events.js.map +1 -1
  148. package/dist/web-server/routes/sessions.d.ts +7 -5
  149. package/dist/web-server/routes/sessions.d.ts.map +1 -1
  150. package/dist/web-server/routes/sessions.js +1030 -174
  151. package/dist/web-server/routes/sessions.js.map +1 -1
  152. package/dist/web-server/routes/workspace.d.ts +27 -0
  153. package/dist/web-server/routes/workspace.d.ts.map +1 -0
  154. package/dist/web-server/routes/workspace.js +280 -0
  155. package/dist/web-server/routes/workspace.js.map +1 -0
  156. package/dist/web-server/run-event-log.d.ts +167 -0
  157. package/dist/web-server/run-event-log.d.ts.map +1 -0
  158. package/dist/web-server/run-event-log.js +468 -0
  159. package/dist/web-server/run-event-log.js.map +1 -0
  160. package/dist/web-server/session-reconciler.d.ts +162 -0
  161. package/dist/web-server/session-reconciler.d.ts.map +1 -0
  162. package/dist/web-server/session-reconciler.js +363 -0
  163. package/dist/web-server/session-reconciler.js.map +1 -0
  164. package/dist/web-server/static.d.ts +7 -0
  165. package/dist/web-server/static.d.ts.map +1 -1
  166. package/dist/web-server/static.js +46 -3
  167. package/dist/web-server/static.js.map +1 -1
  168. package/dist/web-server/web-auth.d.ts +17 -0
  169. package/dist/web-server/web-auth.d.ts.map +1 -0
  170. package/dist/web-server/web-auth.js +33 -0
  171. package/dist/web-server/web-auth.js.map +1 -0
  172. package/dist/web-server/web-token.d.ts +39 -0
  173. package/dist/web-server/web-token.d.ts.map +1 -0
  174. package/dist/web-server/web-token.js +71 -0
  175. package/dist/web-server/web-token.js.map +1 -0
  176. package/opencode/arcs/manifest.json +8 -8
  177. package/package.json +1 -1
  178. package/scripts/claude-code-session-hook.mjs +49 -3
  179. package/scripts/deploy-claudecode-bundle.mjs +24 -3
  180. package/dist/web-client/assets/architecture-TIHT7OUA-CJqI5wNI.js +0 -1
  181. package/dist/web-client/assets/channel-C8DlmyVe.js +0 -1
  182. package/dist/web-client/assets/classDiagram-OUVF2IWQ-p32N1P_G.js +0 -1
  183. package/dist/web-client/assets/classDiagram-v2-EOCWNBFH-p32N1P_G.js +0 -1
  184. package/dist/web-client/assets/eventmodeling-45OFAUF4-Bj5P8mZJ.js +0 -1
  185. package/dist/web-client/assets/flowDiagram-23GEKE2U-37BztFri.js +0 -1
  186. package/dist/web-client/assets/index-3mNPVkix.css +0 -2
  187. package/dist/web-client/assets/railroad-abnf-AHOZXSZD-2Dg9wu0J.js +0 -1
  188. package/dist/web-client/assets/railroad-ebnf-EBAXGLYW-C_E2ot0R.js +0 -1
  189. package/dist/web-client/assets/railroad-peg-LSFZ7HO6-Bs9UQR1b.js +0 -1
  190. package/dist/web-client/assets/stateDiagram-v2-6OUMAXLB-BCMWGnsJ.js +0 -1
  191. package/dist/web-client/assets/swimlanesDiagram-G3AALYLV-DmudmLcK.js +0 -8
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Session liveness reconciliation — the world's opinion on top of the store's.
3
+ *
4
+ * `deriveSessionPhase` (session-store) answers "what does this record claim" from
5
+ * persisted evidence alone. This module answers "is that still true", using the
6
+ * two liveness sources that exist outside the DAG:
7
+ *
8
+ * - a run claim's pid, probed with signal 0. ARCS-driven runs are headless
9
+ * `claude -p` children ARCS spawned itself, so their liveness is a local
10
+ * process fact — and the persisted pid is what makes it readable from a
11
+ * server process that did not spawn them.
12
+ * - `claude agents --json`, which lists the live interactive/background
13
+ * sessions a terminal is driving. That is the only way to tell a session ARCS
14
+ * merely observes from one whose terminal has since been closed.
15
+ *
16
+ * Two shapes of work, deliberately separated:
17
+ *
18
+ * - `settleOrphanedRuns` WRITES: a claim whose process is gone settles as
19
+ * outcome `interrupted`, the claim is cleared and the session's run event
20
+ * logs are pruned. Runs once at startup (see app.ts), because a claim
21
+ * persisted by a previous process can never be live in this one unless its
22
+ * child outlived the restart.
23
+ * - `reconcileSessionPhases` READS: it returns demoted phases and never writes.
24
+ * On-demand only — there is no timer here, and no poller.
25
+ *
26
+ * Everything that touches the outside world is tolerant by construction: the
27
+ * probe is a subprocess of a binary that may not exist, running a command whose
28
+ * output shape is claude's to change. A missing binary, a non-zero exit, an
29
+ * empty stdout or unparsable JSON all degrade to the store-derived phase and
30
+ * NEVER throw — a reconciler that can fail would take the sessions UI down with
31
+ * every claude release.
32
+ */
33
+ import { execFile } from "node:child_process";
34
+ import { resolve } from "node:path";
35
+ import { readRootMeta } from "../utils/dag.js";
36
+ import { getDataDir } from "../utils/paths.js";
37
+ import { deriveSessionPhase, listSessions, sessionRunClaim, settleSessionRun, } from "../utils/session-store.js";
38
+ import { normalizeIdentifier } from "../utils/slug.js";
39
+ import { pruneRunEventLogs } from "./run-event-log.js";
40
+ const AGENTS_PROBE_TIMEOUT_MS = 5_000;
41
+ const AGENTS_PROBE_MAX_BUFFER = 4 * 1024 * 1024;
42
+ /** Keys an `agents --json` payload might wrap its array in. */
43
+ const AGENT_LIST_KEYS = ["agents", "sessions", "data", "items"];
44
+ function defaultAgentsProbe() {
45
+ return new Promise((resolveProbe) => {
46
+ try {
47
+ execFile("claude", ["agents", "--json"], {
48
+ timeout: AGENTS_PROBE_TIMEOUT_MS,
49
+ maxBuffer: AGENTS_PROBE_MAX_BUFFER,
50
+ encoding: "utf-8",
51
+ }, (error, stdout) => {
52
+ // ENOENT (no binary), a non-zero exit and the timeout kill all arrive
53
+ // here as an error — none of them is a usable answer, and none of
54
+ // them is fatal.
55
+ if (error)
56
+ resolveProbe({ ok: false });
57
+ else
58
+ resolveProbe({ ok: true, stdout: String(stdout) });
59
+ });
60
+ }
61
+ catch {
62
+ // Some spawn failures surface synchronously depending on platform.
63
+ resolveProbe({ ok: false });
64
+ }
65
+ });
66
+ }
67
+ function readString(node, ...keys) {
68
+ for (const key of keys) {
69
+ const value = node[key];
70
+ if (typeof value === "string" && value !== "")
71
+ return value;
72
+ }
73
+ return undefined;
74
+ }
75
+ function readPid(node) {
76
+ const value = node.pid;
77
+ if (typeof value === "number" && Number.isInteger(value) && value > 0)
78
+ return value;
79
+ // Tolerated: some CLIs stringify pids.
80
+ if (typeof value === "string") {
81
+ const parsed = Number.parseInt(value, 10);
82
+ if (Number.isInteger(parsed) && parsed > 0)
83
+ return parsed;
84
+ }
85
+ return undefined;
86
+ }
87
+ /** The array of rows inside a payload, or `null` when there is no usable one. */
88
+ function agentRows(value) {
89
+ if (Array.isArray(value))
90
+ return value;
91
+ if (typeof value !== "object" || value === null)
92
+ return null;
93
+ const node = value;
94
+ for (const key of AGENT_LIST_KEYS) {
95
+ if (Array.isArray(node[key]))
96
+ return node[key];
97
+ }
98
+ return null;
99
+ }
100
+ function toLiveAgent(row) {
101
+ if (typeof row !== "object" || row === null || Array.isArray(row))
102
+ return undefined;
103
+ const node = row;
104
+ const pid = readPid(node);
105
+ const sessionId = readString(node, "sessionId", "session_id");
106
+ // A row that identifies neither a process nor a session cannot answer a
107
+ // liveness question, so it is dropped rather than counted as an agent.
108
+ if (pid === undefined && sessionId === undefined)
109
+ return undefined;
110
+ const cwd = readString(node, "cwd", "directory");
111
+ const name = readString(node, "name");
112
+ const status = readString(node, "status", "state");
113
+ return {
114
+ ...(pid !== undefined && { pid }),
115
+ ...(cwd !== undefined && { cwd }),
116
+ ...(sessionId !== undefined && { sessionId }),
117
+ ...(name !== undefined && { name }),
118
+ ...(status !== undefined && { status }),
119
+ };
120
+ }
121
+ /**
122
+ * Parses `claude agents --json` stdout into live agents.
123
+ *
124
+ * `null` means "the probe answered nothing usable" (empty output, non-JSON, a
125
+ * JSON shape carrying no array) and is NOT the same as `[]`, which means "the
126
+ * probe worked and no agent is live". Only the second may demote a session:
127
+ * conflating them would mark every observed session idle the day claude changes
128
+ * its output.
129
+ */
130
+ export function parseLiveAgents(stdout) {
131
+ const text = stdout.trim();
132
+ if (text === "")
133
+ return null;
134
+ let value;
135
+ try {
136
+ value = JSON.parse(text);
137
+ }
138
+ catch {
139
+ return null;
140
+ }
141
+ const rows = agentRows(value);
142
+ if (rows === null)
143
+ return null;
144
+ const agents = [];
145
+ for (const row of rows) {
146
+ const agent = toLiveAgent(row);
147
+ if (agent !== undefined)
148
+ agents.push(agent);
149
+ }
150
+ return agents;
151
+ }
152
+ /**
153
+ * Runs the probe and parses it. `null` on every degraded path — missing binary,
154
+ * non-zero exit, timeout, unparsable output, or a probe that threw.
155
+ */
156
+ export async function listLiveClaudeAgents(options = {}) {
157
+ const probe = options.probe ?? defaultAgentsProbe;
158
+ let result;
159
+ try {
160
+ result = await probe();
161
+ }
162
+ catch {
163
+ return null;
164
+ }
165
+ if (!result.ok)
166
+ return null;
167
+ return parseLiveAgents(result.stdout);
168
+ }
169
+ // ---------------------------------------------------------------------------
170
+ // Process liveness
171
+ // ---------------------------------------------------------------------------
172
+ /**
173
+ * Whether a pid is a live process, via signal 0 (no signal delivered).
174
+ *
175
+ * SHORTCUT: pids are reused, so a recycled pid reads alive and keeps an orphan
176
+ * claim standing; upgrade to a start-time check (`/proc/<pid>/stat` field 22 or
177
+ * `ps -o lstart`) when a stuck claim is ever actually observed.
178
+ */
179
+ export function isProcessAlive(pid) {
180
+ if (!Number.isInteger(pid) || pid <= 0)
181
+ return false;
182
+ try {
183
+ process.kill(pid, 0);
184
+ return true;
185
+ }
186
+ catch (err) {
187
+ // EPERM means the process exists but belongs to another user — alive.
188
+ return err?.code === "EPERM";
189
+ }
190
+ }
191
+ /** Does this agent report the session behind this record? */
192
+ function agentMatchesSession(session, agent) {
193
+ const reported = agent.sessionId;
194
+ if (reported === undefined)
195
+ return false;
196
+ if (reported === session.runtimeSessionId)
197
+ return true;
198
+ if (normalizeIdentifier(reported) === session.normalizedId)
199
+ return true;
200
+ // An ARCS thread runs under its own claude-facing uuid, which is what the
201
+ // agent list reports — the ARCS thread id never reaches claude.
202
+ const claudeSessionId = session.metadata?.claudeSessionId;
203
+ return typeof claudeSessionId === "string" && reported === claudeSessionId;
204
+ }
205
+ /**
206
+ * The store-derived phase, demoted when the world disagrees.
207
+ *
208
+ * Demotion is one-way: this can turn `running` into `idle`, never the reverse.
209
+ * Nothing outside the record may promote a session — the DAG stays the source
210
+ * of truth and the probe is only ever allowed to take evidence away.
211
+ *
212
+ * Which evidence applies depends on what the record claims, not on its origin:
213
+ * - a run claim is checked against its pid. `claude agents` never lists a
214
+ * headless `claude -p` child, so asking it about an ARCS run would demote
215
+ * every healthy run.
216
+ * - everything else is checked against the agent list. `agents === null` (the
217
+ * probe degraded) leaves the `lastCheckpointAt`-derived phase untouched.
218
+ */
219
+ export function reconcilePhase(session, agents, options = {}) {
220
+ const derived = deriveSessionPhase(session, { now: options.now });
221
+ if (derived !== "running")
222
+ return derived;
223
+ if (sessionRunClaim(session) !== undefined) {
224
+ const pid = session.currentRunPid;
225
+ // No pid to probe (the spawn produced none) — the heartbeat stands alone.
226
+ if (typeof pid !== "number")
227
+ return derived;
228
+ return (options.isAlive ?? isProcessAlive)(pid) ? "running" : "idle";
229
+ }
230
+ if (agents === null)
231
+ return derived;
232
+ return agents.some((agent) => agentMatchesSession(session, agent)) ? "running" : "idle";
233
+ }
234
+ /**
235
+ * Whether this record's phase can depend on the agent list at all.
236
+ *
237
+ * Exactly the one branch of `reconcilePhase` that reads `agents`: a derived
238
+ * `running` with no run claim. Everything else — terminal, idle, or claimed —
239
+ * answers from the record alone and never looks at the list.
240
+ */
241
+ function needsAgentList(session, now) {
242
+ return (deriveSessionPhase(session, { now }) === "running" && sessionRunClaim(session) === undefined);
243
+ }
244
+ /**
245
+ * Reconciled phase for every session in a project. Read-only: no writes, no
246
+ * timer, and at most ONE `claude agents --json` for the whole index — never one
247
+ * per session.
248
+ *
249
+ * The probe is LAZY: `claude agents --json` costs ~0.35s of wall clock and a
250
+ * whole subprocess, and its answer is read at a single branch of
251
+ * `reconcilePhase`. Every terminal record, every idle one and every ARCS run
252
+ * holding a claim decides without it, so a probe fired before the phases are
253
+ * derived is a subprocess whose result is thrown away — on EVERY sessions
254
+ * request, list and detail alike, at whatever rate the watcher invalidates the
255
+ * query. So the derivation runs first and the probe only follows when some
256
+ * record actually reaches that branch.
257
+ *
258
+ * Skipping it passes `null` — the same value a degraded probe yields, and the
259
+ * value no session left in this pass can read: reaching the `agents` branch is
260
+ * precisely the condition that would have run the probe.
261
+ */
262
+ export async function reconcileSessionPhases(projectDir, options = {}) {
263
+ const sessions = await listSessions(projectDir);
264
+ // One clock for both passes, so the derivation that decides whether to probe
265
+ // is the same one reconcilePhase answers from.
266
+ const now = options.now ?? Date.now();
267
+ const scoped = { ...options, now };
268
+ const agents = sessions.some((session) => needsAgentList(session, now))
269
+ ? await listLiveClaudeAgents({ probe: options.probe })
270
+ : null;
271
+ return sessions.map((session) => ({
272
+ sessionId: session.normalizedId,
273
+ phase: reconcilePhase(session, agents, scoped),
274
+ }));
275
+ }
276
+ function orphanError(pid) {
277
+ return pid === undefined
278
+ ? "run was interrupted: no pid was recorded for it, so nothing could report how it ended"
279
+ : `run was interrupted: its process (pid ${pid}) is gone and never reported an outcome`;
280
+ }
281
+ /**
282
+ * Settles every run claim in a project whose process is gone.
283
+ *
284
+ * A claim only survives when its pid is still alive — a run spawned by a
285
+ * previous server process either outlived the restart (leave it be; it still
286
+ * holds the write-target) or died with it, and a dead claim that is never
287
+ * cleared is a session stuck on `running` for good. The settled run keeps
288
+ * whatever `metadata.run` already held and gains `outcome: "interrupted"`,
289
+ * which no live run can ever produce.
290
+ *
291
+ * A settle here also prunes the session's run event logs, exactly as the route's
292
+ * own write-back does — this is the ONLY settle an interrupted run ever gets, so
293
+ * it is the only place that can bound the logs of a session whose runs never
294
+ * settle normally.
295
+ *
296
+ * Never throws: a project whose index cannot be read is skipped, and a record
297
+ * deleted mid-sweep is simply not settled.
298
+ */
299
+ export async function settleOrphanedRuns(projectDir, options = {}) {
300
+ const alive = options.isAlive ?? isProcessAlive;
301
+ let sessions;
302
+ try {
303
+ sessions = await listSessions(projectDir);
304
+ }
305
+ catch {
306
+ return [];
307
+ }
308
+ const settled = [];
309
+ for (const session of sessions) {
310
+ const runId = sessionRunClaim(session);
311
+ if (runId === undefined)
312
+ continue;
313
+ const pid = typeof session.currentRunPid === "number" ? session.currentRunPid : undefined;
314
+ if (pid !== undefined && alive(pid))
315
+ continue;
316
+ try {
317
+ await settleSessionRun(projectDir, session.normalizedId, {
318
+ runId,
319
+ outcome: "interrupted",
320
+ error: orphanError(pid),
321
+ endedAt: options.now,
322
+ });
323
+ settled.push({ sessionId: session.normalizedId, runId, ...(pid !== undefined && { pid }) });
324
+ // Retention belongs to every settle, and this is the one settle path that
325
+ // is not a run's own write-back. A server that dies mid-run never reaches
326
+ // `writeBackRun`'s prune, so a session whose runs are always interrupted
327
+ // would otherwise accumulate one RUN_EVENT_LOG_MAX_BYTES log per run
328
+ // forever — this sweep is the only thing that ever visits those runs
329
+ // again. Never throws and its failure is not the sweep's business: the
330
+ // claim is already released above whatever retention manages to delete.
331
+ await pruneRunEventLogs(projectDir, session.normalizedId);
332
+ }
333
+ catch {
334
+ // Record vanished between the read and the settle — nothing to settle.
335
+ }
336
+ }
337
+ return settled;
338
+ }
339
+ /**
340
+ * Startup sweep across every registered project.
341
+ *
342
+ * Composition-time work (app.ts calls it once, fire-and-forget) but the logic
343
+ * lives here. One pass, no timer: liveness questions after startup are answered
344
+ * on demand by `reconcileSessionPhases`.
345
+ *
346
+ * Never throws — a data dir without a readable meta.json settles nothing and
347
+ * reports it, rather than taking the server down at boot.
348
+ */
349
+ export async function settleOrphanedRunsOnStartup(dataDir = getDataDir(), options = {}) {
350
+ let slugs;
351
+ try {
352
+ slugs = (await readRootMeta(dataDir)).projects.map((project) => project.id);
353
+ }
354
+ catch {
355
+ return [];
356
+ }
357
+ const settled = [];
358
+ for (const slug of slugs) {
359
+ settled.push(...(await settleOrphanedRuns(resolve(dataDir, "projects", slug), options)));
360
+ }
361
+ return settled;
362
+ }
363
+ //# sourceMappingURL=session-reconciler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-reconciler.js","sourceRoot":"","sources":["../../src/web-server/session-reconciler.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,13 @@
4
4
  * The Vite build output lives at dist/web-client (resolved by the caller).
5
5
  * If the bundle is missing, the server runs API-only and explains how to
6
6
  * build the client.
7
+ *
8
+ * index.html is never streamed verbatim: it is read and transformed so the SPA
9
+ * boots holding this server's mutation token (see web-token.ts) without a
10
+ * second fetch. There are TWO paths that answer with index.html — the direct
11
+ * request for `/` and the SPA fallback that backs every deep link — and both go
12
+ * through the same transform, because a shell served without the token is a
13
+ * client that cannot write.
7
14
  */
8
15
  import type { Hono } from "hono";
9
16
  export declare function registerStaticServing(app: Hono, root: string): void;
@@ -1 +1 @@
1
- {"version":3,"file":"static.d.ts","sourceRoot":"","sources":["../../src/web-server/static.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAMjC,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAuBnE"}
1
+ {"version":3,"file":"static.d.ts","sourceRoot":"","sources":["../../src/web-server/static.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,KAAK,EAAW,IAAI,EAAE,MAAM,MAAM,CAAC;AA0C1C,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CA0BnE"}
@@ -4,12 +4,51 @@
4
4
  * The Vite build output lives at dist/web-client (resolved by the caller).
5
5
  * If the bundle is missing, the server runs API-only and explains how to
6
6
  * build the client.
7
+ *
8
+ * index.html is never streamed verbatim: it is read and transformed so the SPA
9
+ * boots holding this server's mutation token (see web-token.ts) without a
10
+ * second fetch. There are TWO paths that answer with index.html — the direct
11
+ * request for `/` and the SPA fallback that backs every deep link — and both go
12
+ * through the same transform, because a shell served without the token is a
13
+ * client that cannot write.
7
14
  */
8
- import { existsSync } from "node:fs";
15
+ import { existsSync, readFileSync } from "node:fs";
9
16
  import { join } from "node:path";
10
17
  import { serveStatic } from "@hono/node-server/serve-static";
11
18
  import { fail } from "./respond.js";
19
+ import { currentWebToken } from "./web-token.js";
12
20
  const NOT_BUILT_HINT = "ARCS web client is not built. Run `npm run build:web` in the arcs repo (or reinstall @rryando/arcs).";
21
+ /**
22
+ * Client contract: the SPA reads its token from
23
+ * `<meta name="arcs-web-token" content="…">` in the document head and sends it
24
+ * back as the `X-ARCS-Token` header on every mutating request.
25
+ */
26
+ const TOKEN_META_NAME = "arcs-web-token";
27
+ /** Request paths that resolve to the SPA shell rather than a bundled asset. */
28
+ const INDEX_PATHS = new Set(["/", "/index.html"]);
29
+ /**
30
+ * Reads the built shell and injects this server's token as a head meta tag.
31
+ *
32
+ * The token is hex, so it needs no HTML escaping, and the source is always the
33
+ * untouched Vite output — the server only ever reads index.html, never writes
34
+ * it back, so the injection cannot accumulate across restarts. `no-store` keeps
35
+ * a rotated token from being served from a stale cache after a restart.
36
+ */
37
+ function serveIndexHtml(c, root) {
38
+ let shell;
39
+ try {
40
+ shell = readFileSync(join(root, "index.html"), "utf-8");
41
+ }
42
+ catch {
43
+ // The bundle vanished after startup — same answer as never having built it.
44
+ return c.text(NOT_BUILT_HINT, 404);
45
+ }
46
+ const meta = `<meta name="${TOKEN_META_NAME}" content="${currentWebToken() ?? ""}" />`;
47
+ const injected = shell.includes("</head>")
48
+ ? shell.replace("</head>", ` ${meta}\n </head>`)
49
+ : `${meta}\n${shell}`;
50
+ return c.html(injected, 200, { "cache-control": "no-store" });
51
+ }
13
52
  export function registerStaticServing(app, root) {
14
53
  if (!existsSync(join(root, "index.html"))) {
15
54
  app.notFound((c) => {
@@ -23,14 +62,18 @@ export function registerStaticServing(app, root) {
23
62
  app.use("*", async (c, next) => {
24
63
  if (c.req.path.startsWith("/api/"))
25
64
  return next();
65
+ // Transform point 1 — the direct shell request.
66
+ if (INDEX_PATHS.has(c.req.path))
67
+ return serveIndexHtml(c, root);
26
68
  return serveStatic({ root })(c, next);
27
69
  });
28
70
  app.notFound((c) => {
29
71
  if (c.req.path.startsWith("/api/")) {
30
72
  return c.json(fail("not_found", `No route: ${c.req.method} ${c.req.path}`), 404);
31
73
  }
32
- // SPA fallback client-side routing takes over.
33
- return serveStatic({ path: join(root, "index.html") })(c, async () => { });
74
+ // Transform point 2 — SPA fallback, where client-side routing takes over.
75
+ // Deep links land here and need the token just as much as `/` does.
76
+ return serveIndexHtml(c, root);
34
77
  });
35
78
  }
36
79
  //# sourceMappingURL=static.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"static.js","sourceRoot":"","sources":["../../src/web-server/static.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAE7D,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEpC,MAAM,cAAc,GAClB,sGAAsG,CAAC;AAEzG,MAAM,UAAU,qBAAqB,CAAC,GAAS,EAAE,IAAY;IAC3D,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC;QAC1C,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE;YACjB,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;gBACnC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;YACnF,CAAC;YACD,OAAO,CAAC,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;QACrC,CAAC,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QAC7B,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,EAAE,CAAC;QAClD,OAAO,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE;QACjB,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YACnC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;QACnF,CAAC;QACD,iDAAiD;QACjD,OAAO,WAAW,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC,CAAsB,CAAC;IACjG,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"static.js","sourceRoot":"","sources":["../../src/web-server/static.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `X-ARCS-Token` gate for mutating API routes.
3
+ *
4
+ * Layered on top of (never instead of) the global loopback-only
5
+ * `secureLocalRequest`: loopback proves the caller is on this machine, the
6
+ * token proves it is a page this server itself served (see web-token.ts).
7
+ *
8
+ * Deny-by-default across `/api/*` rather than an allowlist of known paths: a
9
+ * mutating route added later is gated the moment it is registered, with exactly
10
+ * one exemption — `/api/hook/*`, whose caller is a Claude Code hook subprocess
11
+ * and not the browser, and which carries its own independent bearer token
12
+ * (hook-auth.ts). Reads are untouched: every GET/HEAD/OPTIONS passes straight
13
+ * through, so `/api/events` and the whole read surface behave exactly as before.
14
+ */
15
+ import type { MiddlewareHandler } from "hono";
16
+ export declare const requireWebToken: MiddlewareHandler;
17
+ //# sourceMappingURL=web-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-auth.d.ts","sourceRoot":"","sources":["../../src/web-server/web-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAc9C,eAAO,MAAM,eAAe,EAAE,iBAW7B,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `X-ARCS-Token` gate for mutating API routes.
3
+ *
4
+ * Layered on top of (never instead of) the global loopback-only
5
+ * `secureLocalRequest`: loopback proves the caller is on this machine, the
6
+ * token proves it is a page this server itself served (see web-token.ts).
7
+ *
8
+ * Deny-by-default across `/api/*` rather than an allowlist of known paths: a
9
+ * mutating route added later is gated the moment it is registered, with exactly
10
+ * one exemption — `/api/hook/*`, whose caller is a Claude Code hook subprocess
11
+ * and not the browser, and which carries its own independent bearer token
12
+ * (hook-auth.ts). Reads are untouched: every GET/HEAD/OPTIONS passes straight
13
+ * through, so `/api/events` and the whole read surface behave exactly as before.
14
+ */
15
+ import { fail } from "./respond.js";
16
+ import { verifyWebToken } from "./web-token.js";
17
+ /** Mirrors the mutation set in security.ts. */
18
+ const MUTATION_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
19
+ /** The one prefix that authenticates with its own bearer token instead. */
20
+ const HOOK_PREFIX = "/api/hook/";
21
+ function needsWebToken(method, path) {
22
+ return MUTATION_METHODS.has(method.toUpperCase()) && !path.startsWith(HOOK_PREFIX);
23
+ }
24
+ export const requireWebToken = async (c, next) => {
25
+ if (needsWebToken(c.req.method, c.req.path) &&
26
+ !verifyWebToken(c.req.header("x-arcs-token") ?? "")) {
27
+ // A clean typed refusal, not a throw: respond.ts only maps DagError, and a
28
+ // missing token is an auth answer rather than a DAG failure.
29
+ return c.json(fail("web_unauthorized", "Missing or invalid ARCS web token"), 401);
30
+ }
31
+ await next();
32
+ };
33
+ //# sourceMappingURL=web-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-auth.js","sourceRoot":"","sources":["../../src/web-server/web-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACpC,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEhD,+CAA+C;AAC/C,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;AAErE,2EAA2E;AAC3E,MAAM,WAAW,GAAG,YAAY,CAAC;AAEjC,SAAS,aAAa,CAAC,MAAc,EAAE,IAAY;IACjD,OAAO,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,CAAC,MAAM,eAAe,GAAsB,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;IAClE,IACE,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;QACvC,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,EACnD,CAAC;QACD,2EAA2E;QAC3E,6DAA6D;QAC7D,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,mCAAmC,CAAC,EAAE,GAAG,CAAC,CAAC;IACpF,CAAC;IAED,MAAM,IAAI,EAAE,CAAC;AACf,CAAC,CAAC"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Per-server auth token for the browser-facing mutating API.
3
+ *
4
+ * The loopback + Origin checks in security.ts stop remote and cross-site
5
+ * callers, but they authenticate a NETWORK LOCATION, not a process: every other
6
+ * program on this machine shares localhost, and the mutating routes now spawn
7
+ * `claude` subprocesses. This token proves the caller is a page THIS server
8
+ * served — it is minted fresh at every server start and injected into the
9
+ * served index.html (see static.ts), so the SPA holds it from first paint and
10
+ * no other local process can guess it.
11
+ *
12
+ * Deliberately per-SERVER, not per-project (unlike hook-token-store, which
13
+ * authenticates a per-project hook install): it authenticates the browser tab,
14
+ * and one tab drives every project. Verification always reads the in-memory
15
+ * value, never the file, so a second server started against the same data dir
16
+ * cannot authorize requests to this one — it only replaces the on-disk copy.
17
+ * Exactly one token is live per PROCESS: a second createApp in the same process
18
+ * rotates it and every app then verifies against the newest value. ARCS runs
19
+ * one server per process, so that is a test-only nuance.
20
+ *
21
+ * The file exists for out-of-band consumers (scripts, tests) and is written
22
+ * 0o600: the token is never passed on an argv or in an env var a `ps` can read.
23
+ */
24
+ export declare function webTokenPath(): string;
25
+ /**
26
+ * Mints this server's token, replacing any previous one, and persists it 0o600.
27
+ * Called once from createApp, before any route can be served: the middleware
28
+ * and the index.html transform both read the value it installs.
29
+ */
30
+ export declare function mintWebToken(): string;
31
+ /** This server's live token, or undefined before mintWebToken() has run. */
32
+ export declare function currentWebToken(): string | undefined;
33
+ /**
34
+ * Constant-time token comparison. Returns false when no token has been minted
35
+ * yet, so a request that arrives before startup finished cannot be authorized
36
+ * by an empty header.
37
+ */
38
+ export declare function verifyWebToken(candidate: string): boolean;
39
+ //# sourceMappingURL=web-token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-token.d.ts","sourceRoot":"","sources":["../../src/web-server/web-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAkBH,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAerC;AAED,4EAA4E;AAC5E,wBAAgB,eAAe,IAAI,MAAM,GAAG,SAAS,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAOzD"}