@rryando/arcs 3.11.0 → 4.1.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.
- package/README.md +1 -1
- package/dist/cli/commands/hooks.d.ts +46 -6
- package/dist/cli/commands/hooks.d.ts.map +1 -1
- package/dist/cli/commands/hooks.js +190 -52
- package/dist/cli/commands/hooks.js.map +1 -1
- package/dist/cli/commands/web.js +14 -4
- package/dist/cli/commands/web.js.map +1 -1
- package/dist/cli/config.d.ts +14 -3
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +39 -38
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/instructions.d.ts +13 -1
- package/dist/cli/instructions.d.ts.map +1 -1
- package/dist/cli/instructions.js +38 -3
- package/dist/cli/instructions.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +76 -11
- package/dist/cli/setup.js.map +1 -1
- package/dist/shared/session-vocabulary.d.ts +88 -0
- package/dist/shared/session-vocabulary.d.ts.map +1 -0
- package/dist/shared/session-vocabulary.js +114 -0
- package/dist/shared/session-vocabulary.js.map +1 -0
- package/dist/utils/claude-code-hook-install.d.ts +1 -1
- package/dist/utils/claude-code-hook-install.d.ts.map +1 -1
- package/dist/utils/claude-code-hook-install.js +71 -5
- package/dist/utils/claude-code-hook-install.js.map +1 -1
- package/dist/utils/claude-transcript.d.ts +81 -2
- package/dist/utils/claude-transcript.d.ts.map +1 -1
- package/dist/utils/claude-transcript.js +33 -2
- package/dist/utils/claude-transcript.js.map +1 -1
- package/dist/utils/git.d.ts +21 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +65 -1
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/hook-contract.d.ts +36 -0
- package/dist/utils/hook-contract.d.ts.map +1 -0
- package/dist/utils/hook-contract.js +35 -0
- package/dist/utils/hook-contract.js.map +1 -0
- package/dist/utils/hook-token-store.d.ts +44 -3
- package/dist/utils/hook-token-store.d.ts.map +1 -1
- package/dist/utils/hook-token-store.js +51 -6
- package/dist/utils/hook-token-store.js.map +1 -1
- package/dist/utils/session-store.d.ts +207 -29
- package/dist/utils/session-store.d.ts.map +1 -1
- package/dist/utils/session-store.js +236 -44
- package/dist/utils/session-store.js.map +1 -1
- package/dist/utils/storage-utils.d.ts +1 -1
- package/dist/utils/storage-utils.d.ts.map +1 -1
- package/dist/utils/storage-utils.js +1 -1
- package/dist/utils/storage-utils.js.map +1 -1
- package/dist/web-client/assets/{GraphCanvas-dNyZ458L.js → GraphCanvas-BPDgvsyT.js} +1 -1
- package/dist/web-client/assets/{MarkdownEditor-BmU9mdkN.js → MarkdownEditor-D7TLp78z.js} +1 -1
- package/dist/web-client/assets/{abnfDiagram-VRR7QNED-D1BFBoeF.js → abnfDiagram-VRR7QNED-CyuP2N9t.js} +1 -1
- package/dist/web-client/assets/architecture-TIHT7OUA-Bdo2Yvm9.js +1 -0
- package/dist/web-client/assets/{architectureDiagram-ZJ3FMSHR-CA8hTWUK.js → architectureDiagram-ZJ3FMSHR-DZ0ul9QX.js} +1 -1
- package/dist/web-client/assets/{blockDiagram-677ZJIJ3-CQpb_KwX.js → blockDiagram-677ZJIJ3-LLGzlc9l.js} +1 -1
- package/dist/web-client/assets/{c4Diagram-LMCZKHZV-BhpHX84V.js → c4Diagram-LMCZKHZV-CViu3CTc.js} +1 -1
- package/dist/web-client/assets/channel-DBNmizpo.js +1 -0
- package/dist/web-client/assets/{chunk-32BRIVSS-BUusQQa_.js → chunk-32BRIVSS-Bw_IuJCM.js} +1 -1
- package/dist/web-client/assets/{chunk-52WLFC77-nNYwlftl.js → chunk-52WLFC77-C29h440W.js} +1 -1
- package/dist/web-client/assets/{chunk-C7G6YPKG-D0a-yqnK.js → chunk-C7G6YPKG-hhOrvw5w.js} +1 -1
- package/dist/web-client/assets/{chunk-EX3LRPZG-IDuRMN-4.js → chunk-EX3LRPZG-COMzol-M.js} +1 -1
- package/dist/web-client/assets/{chunk-FWX5IMBZ-D_spTcqi.js → chunk-FWX5IMBZ-6vdX9EUn.js} +2 -2
- package/dist/web-client/assets/{chunk-HOUHSVGY-C-bcxwZS.js → chunk-HOUHSVGY-DWDW6sxp.js} +1 -1
- package/dist/web-client/assets/{chunk-ICXQ74PX-CjmK2bAM.js → chunk-ICXQ74PX-BdMYglo2.js} +1 -1
- package/dist/web-client/assets/{chunk-MOJQB5TN-Bkc08KWY.js → chunk-MOJQB5TN-C0LAX_dC.js} +1 -1
- package/dist/web-client/assets/{chunk-OGEWGWER-BAAYYvG9.js → chunk-OGEWGWER-CBx8MB7f.js} +1 -1
- package/dist/web-client/assets/{chunk-PUDLZKDR-WXbPY7NM.js → chunk-PUDLZKDR-DKssR1nf.js} +1 -1
- package/dist/web-client/assets/{chunk-Q4XR5HBZ-DcbnjxQE.js → chunk-Q4XR5HBZ-B3kcxFE-.js} +1 -1
- package/dist/web-client/assets/{chunk-V7JOEXUC-C6t75PAp.js → chunk-V7JOEXUC-CAlymndy.js} +1 -1
- package/dist/web-client/assets/{chunk-VAUOI2AC-DVrJ0Ic7.js → chunk-VAUOI2AC-BowfsmTW.js} +1 -1
- package/dist/web-client/assets/{chunk-VR4S4FIN-MOOFvGS0.js → chunk-VR4S4FIN-BBOydgvt.js} +1 -1
- package/dist/web-client/assets/{chunk-WYO6CB5R-sK7Y2NZD.js → chunk-WYO6CB5R-DcymFbES.js} +1 -1
- package/dist/web-client/assets/{chunk-ZGVPDNZ5-a13RQsku.js → chunk-ZGVPDNZ5--uKFP-Lr.js} +1 -1
- package/dist/web-client/assets/classDiagram-OUVF2IWQ-CB3HiA1_.js +1 -0
- package/dist/web-client/assets/classDiagram-v2-EOCWNBFH-CB3HiA1_.js +1 -0
- package/dist/web-client/assets/{cynefin-VYW2F7L2-D8xaH-wO.js → cynefin-VYW2F7L2-CjboUOMA.js} +1 -1
- package/dist/web-client/assets/{cynefinDiagram-TSTJHNR4-Bt__EqJW.js → cynefinDiagram-TSTJHNR4-BcxygBP7.js} +1 -1
- package/dist/web-client/assets/{dagre-VKFMJZFB-Bwgjwflz.js → dagre-VKFMJZFB-D-tiERQE.js} +1 -1
- package/dist/web-client/assets/{diagram-FQU43EPY-CaPDVUq2.js → diagram-FQU43EPY-ChPXczaS.js} +1 -1
- package/dist/web-client/assets/{diagram-G47NLZAW-BQLB9YYA.js → diagram-G47NLZAW-CVL3Y91h.js} +1 -1
- package/dist/web-client/assets/{diagram-NH7WQ7WH-BWo84w8Y.js → diagram-NH7WQ7WH-DsaNA9Lh.js} +1 -1
- package/dist/web-client/assets/{diagram-OA4YK3LP-bKn6Pz5s.js → diagram-OA4YK3LP-CXhrhdhU.js} +1 -1
- package/dist/web-client/assets/{diagram-WEI45ONY-BRqV5Oy6.js → diagram-WEI45ONY-BTVPnk4E.js} +1 -1
- package/dist/web-client/assets/{ebnfDiagram-CCIWWBDH-DhUXL1-7.js → ebnfDiagram-CCIWWBDH-BAyrRBtM.js} +1 -1
- package/dist/web-client/assets/{erDiagram-Q63AITRT-HXAQQ-_F.js → erDiagram-Q63AITRT-Qm24Wepm.js} +1 -1
- package/dist/web-client/assets/eventmodeling-45OFAUF4-DoTBIvl5.js +1 -0
- package/dist/web-client/assets/flowDiagram-23GEKE2U-BEH23L1A.js +1 -0
- package/dist/web-client/assets/{ganttDiagram-NO4QXBWP-D-Ddf_Ii.js → ganttDiagram-NO4QXBWP-D8h7l3XJ.js} +1 -1
- package/dist/web-client/assets/{gitGraph-TEB2WS4Q-BmHzs0uF.js → gitGraph-TEB2WS4Q-DIBml1SB.js} +1 -1
- package/dist/web-client/assets/{gitGraphDiagram-IHSO6WYX-YnQWrlh_.js → gitGraphDiagram-IHSO6WYX-CtkYoXjn.js} +1 -1
- package/dist/web-client/assets/{index-DCWxuIeQ.js → index-DOSH4Q9H.js} +38 -36
- package/dist/web-client/assets/index-wSzUPvml.css +2 -0
- package/dist/web-client/assets/{info-DKCQHKI2-DCT_B7RN.js → info-DKCQHKI2-DLEUtV5Q.js} +1 -1
- package/dist/web-client/assets/{infoDiagram-FWYZ7A6U-D-le1Zhq.js → infoDiagram-FWYZ7A6U-BJQ7aQux.js} +1 -1
- package/dist/web-client/assets/{ishikawaDiagram-FXEZZL3T-Jr1x2VJB.js → ishikawaDiagram-FXEZZL3T-BPM11FvG.js} +1 -1
- package/dist/web-client/assets/{journeyDiagram-5HDEW3XC-BF9ELxj-.js → journeyDiagram-5HDEW3XC-C0aX2z3c.js} +1 -1
- package/dist/web-client/assets/{kanban-definition-HUTT4EX6-C4fJqAxu.js → kanban-definition-HUTT4EX6-C56F29Ib.js} +1 -1
- package/dist/web-client/assets/{line-7N7ikFxa.js → line-BLFHLF2N.js} +1 -1
- package/dist/web-client/assets/{mermaid-parser.core-QbC1icPt.js → mermaid-parser.core-BLC8FhgU.js} +3 -3
- package/dist/web-client/assets/{mermaid.core-C26d_UJm.js → mermaid.core-BBqkKuXt.js} +3 -3
- package/dist/web-client/assets/{mindmap-definition-LN4V7U3C-D6TV1JDf.js → mindmap-definition-LN4V7U3C-aVZbsoPc.js} +1 -1
- package/dist/web-client/assets/{packet-7NZHBO7P-CR1vrGj3.js → packet-7NZHBO7P-D4aqSQfB.js} +1 -1
- package/dist/web-client/assets/{pegDiagram-2B236MQR-xOMBBtfV.js → pegDiagram-2B236MQR-DjfyNI0U.js} +1 -1
- package/dist/web-client/assets/{pie-RZYD4A2V-BbWuhjwy.js → pie-RZYD4A2V-ChCwYsYj.js} +1 -1
- package/dist/web-client/assets/{pieDiagram-ENE6RG2P-MsfnsqgW.js → pieDiagram-ENE6RG2P-BeHLKkXC.js} +1 -1
- package/dist/web-client/assets/{quadrantDiagram-ABIIQ3AL-BoI7zKXF.js → quadrantDiagram-ABIIQ3AL-stga3gvq.js} +1 -1
- package/dist/web-client/assets/{radar-I7S5WNFK-CbYXKToJ.js → radar-I7S5WNFK-DOGheiwT.js} +1 -1
- package/dist/web-client/assets/{railroad-3IZDKUUU-6LxHDkLe.js → railroad-3IZDKUUU-_JnU7M6L.js} +1 -1
- package/dist/web-client/assets/railroad-abnf-AHOZXSZD-nhNub7LE.js +1 -0
- package/dist/web-client/assets/railroad-ebnf-EBAXGLYW-BlQYe7Yf.js +1 -0
- package/dist/web-client/assets/railroad-peg-LSFZ7HO6-B3E8pRVN.js +1 -0
- package/dist/web-client/assets/{railroadDiagram-RFXS5EU6-D6RUoUki.js → railroadDiagram-RFXS5EU6-C0CkMsOd.js} +1 -1
- package/dist/web-client/assets/{requirementDiagram-TGXJPOKE-B6k4BDpE.js → requirementDiagram-TGXJPOKE-DuImwoRD.js} +1 -1
- package/dist/web-client/assets/{sankeyDiagram-HTMAVEWB-BUDF-UFr.js → sankeyDiagram-HTMAVEWB-kprq0XF9.js} +1 -1
- package/dist/web-client/assets/{sequenceDiagram-DBY2YBRQ-D6GqcsUi.js → sequenceDiagram-DBY2YBRQ-DiXKJMF6.js} +1 -1
- package/dist/web-client/assets/{stateDiagram-2N3HPSRC-WfJCQAK5.js → stateDiagram-2N3HPSRC-D5qbVStE.js} +1 -1
- package/dist/web-client/assets/stateDiagram-v2-6OUMAXLB-DWwTAG1r.js +1 -0
- package/dist/web-client/assets/{swimlanes-5IMT3BWC-BtMo82mC.js → swimlanes-5IMT3BWC-DCbw389c.js} +1 -1
- package/dist/web-client/assets/swimlanesDiagram-G3AALYLV-DabrCsjZ.js +8 -0
- package/dist/web-client/assets/{timeline-definition-FHXFAJF6-CoAmv2Sn.js → timeline-definition-FHXFAJF6-CQeaYN_9.js} +1 -1
- package/dist/web-client/assets/{treeView-QDETBFTQ-BWsKzE1s.js → treeView-QDETBFTQ-Cf7Sq3qo.js} +1 -1
- package/dist/web-client/assets/{treemap-6X3UGDF4-i_qGtB3o.js → treemap-6X3UGDF4-BovzvoTU.js} +1 -1
- package/dist/web-client/assets/{vennDiagram-L72KCM5P-DYkiLe-P.js → vennDiagram-L72KCM5P-CZsJy139.js} +1 -1
- package/dist/web-client/assets/{wardley-OPB4EBWU-Daaqr1Vp.js → wardley-OPB4EBWU-DJ7MS6XZ.js} +1 -1
- package/dist/web-client/assets/{wardleyDiagram-EHGQE667-Bofbsg3J.js → wardleyDiagram-EHGQE667-rqhcmsbM.js} +1 -1
- package/dist/web-client/assets/{xychartDiagram-FW5EYKEG-KNF4VTfL.js → xychartDiagram-FW5EYKEG-HuK4Seps.js} +1 -1
- package/dist/web-client/index.html +2 -2
- package/dist/web-server/app.d.ts +4 -1
- package/dist/web-server/app.d.ts.map +1 -1
- package/dist/web-server/app.js +19 -3
- package/dist/web-server/app.js.map +1 -1
- package/dist/web-server/claude-runner.d.ts +109 -7
- package/dist/web-server/claude-runner.d.ts.map +1 -1
- package/dist/web-server/claude-runner.js +329 -50
- package/dist/web-server/claude-runner.js.map +1 -1
- package/dist/web-server/index.d.ts +2 -2
- package/dist/web-server/index.d.ts.map +1 -1
- package/dist/web-server/index.js +3 -2
- package/dist/web-server/index.js.map +1 -1
- package/dist/web-server/permission-policy.d.ts +46 -0
- package/dist/web-server/permission-policy.d.ts.map +1 -0
- package/dist/web-server/permission-policy.js +96 -0
- package/dist/web-server/permission-policy.js.map +1 -0
- package/dist/web-server/prompt-assembly.d.ts +454 -0
- package/dist/web-server/prompt-assembly.d.ts.map +1 -0
- package/dist/web-server/prompt-assembly.js +1122 -0
- package/dist/web-server/prompt-assembly.js.map +1 -0
- package/dist/web-server/routes/hook-events.d.ts +28 -4
- package/dist/web-server/routes/hook-events.d.ts.map +1 -1
- package/dist/web-server/routes/hook-events.js +218 -37
- package/dist/web-server/routes/hook-events.js.map +1 -1
- package/dist/web-server/routes/sessions.d.ts +8 -8
- package/dist/web-server/routes/sessions.d.ts.map +1 -1
- package/dist/web-server/routes/sessions.js +1007 -261
- package/dist/web-server/routes/sessions.js.map +1 -1
- package/dist/web-server/routes/workspace.d.ts +27 -0
- package/dist/web-server/routes/workspace.d.ts.map +1 -0
- package/dist/web-server/routes/workspace.js +280 -0
- package/dist/web-server/routes/workspace.js.map +1 -0
- package/dist/web-server/run-event-log.d.ts +167 -0
- package/dist/web-server/run-event-log.d.ts.map +1 -0
- package/dist/web-server/run-event-log.js +468 -0
- package/dist/web-server/run-event-log.js.map +1 -0
- package/dist/web-server/session-reconciler.d.ts +162 -0
- package/dist/web-server/session-reconciler.d.ts.map +1 -0
- package/dist/web-server/session-reconciler.js +363 -0
- package/dist/web-server/session-reconciler.js.map +1 -0
- package/dist/web-server/static.d.ts +7 -0
- package/dist/web-server/static.d.ts.map +1 -1
- package/dist/web-server/static.js +46 -3
- package/dist/web-server/static.js.map +1 -1
- package/dist/web-server/web-auth.d.ts +17 -0
- package/dist/web-server/web-auth.d.ts.map +1 -0
- package/dist/web-server/web-auth.js +33 -0
- package/dist/web-server/web-auth.js.map +1 -0
- package/dist/web-server/web-token.d.ts +39 -0
- package/dist/web-server/web-token.d.ts.map +1 -0
- package/dist/web-server/web-token.js +71 -0
- package/dist/web-server/web-token.js.map +1 -0
- package/opencode/arcs/manifest.json +8 -8
- package/package.json +1 -1
- package/scripts/claude-code-session-hook.mjs +37 -16
- package/scripts/deploy-claudecode-bundle.mjs +24 -3
- package/dist/web-client/assets/architecture-TIHT7OUA-CJqI5wNI.js +0 -1
- package/dist/web-client/assets/channel-C8DlmyVe.js +0 -1
- package/dist/web-client/assets/classDiagram-OUVF2IWQ-p32N1P_G.js +0 -1
- package/dist/web-client/assets/classDiagram-v2-EOCWNBFH-p32N1P_G.js +0 -1
- package/dist/web-client/assets/eventmodeling-45OFAUF4-Bj5P8mZJ.js +0 -1
- package/dist/web-client/assets/flowDiagram-23GEKE2U-37BztFri.js +0 -1
- package/dist/web-client/assets/index-3mNPVkix.css +0 -2
- package/dist/web-client/assets/railroad-abnf-AHOZXSZD-2Dg9wu0J.js +0 -1
- package/dist/web-client/assets/railroad-ebnf-EBAXGLYW-C_E2ot0R.js +0 -1
- package/dist/web-client/assets/railroad-peg-LSFZ7HO6-Bs9UQR1b.js +0 -1
- package/dist/web-client/assets/stateDiagram-v2-6OUMAXLB-BCMWGnsJ.js +0 -1
- package/dist/web-client/assets/swimlanesDiagram-G3AALYLV-DmudmLcK.js +0 -8
- package/dist/web-server/opencode-client.d.ts +0 -123
- package/dist/web-server/opencode-client.d.ts.map +0 -1
- package/dist/web-server/opencode-client.js +0 -514
- package/dist/web-server/opencode-client.js.map +0 -1
|
@@ -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
|
|
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
|
|
33
|
-
|
|
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
|
|
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"}
|