pi-central 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +204 -2
- package/bin/pi-central.js +2836 -0
- package/bin/pi-central.js.map +7 -0
- package/dist/ui/assets/1c-CM5UDbeN.js +1 -0
- package/dist/ui/assets/1c-query-DVSXgUoE.js +1 -0
- package/dist/ui/assets/abap-BdImnpbu.js +1 -0
- package/dist/ui/assets/ada-BIOnG6TC.js +1 -0
- package/dist/ui/assets/adoc-DYxrpSh4.js +1 -0
- package/dist/ui/assets/ahk1-D2f7Ijez.js +1 -0
- package/dist/ui/assets/ahk2-8Zs4aa1G.js +1 -0
- package/dist/ui/assets/angular-expression-C9JlwiXB.js +1 -0
- package/dist/ui/assets/angular-html-D2F1cIM9.js +1 -0
- package/dist/ui/assets/angular-inline-style-aI-YlMgr.js +1 -0
- package/dist/ui/assets/angular-inline-template-BhVWr-GB.js +1 -0
- package/dist/ui/assets/angular-let-declaration-tXF0G55Y.js +1 -0
- package/dist/ui/assets/angular-template-PU1Sa1kY.js +1 -0
- package/dist/ui/assets/angular-template-blocks-D4Ew2WDq.js +1 -0
- package/dist/ui/assets/angular-ts-B5vx3S6V.js +1 -0
- package/dist/ui/assets/apache-Pmp26Uib.js +1 -0
- package/dist/ui/assets/apex-DhZFqWV2.js +1 -0
- package/dist/ui/assets/apl-BD0Fkm--.js +1 -0
- package/dist/ui/assets/applescript-Co6uUVPk.js +1 -0
- package/dist/ui/assets/ara-BRHolxvo.js +1 -0
- package/dist/ui/assets/as3-CDRFp3-Q.js +1 -0
- package/dist/ui/assets/asm-D_Q5rh1f.js +1 -0
- package/dist/ui/assets/astro-CeWFm1CR.js +1 -0
- package/dist/ui/assets/awk-DMzUqQB5.js +1 -0
- package/dist/ui/assets/ballerina-BFfxhgS-.js +1 -0
- package/dist/ui/assets/be-DZEKi0sx.js +1 -0
- package/dist/ui/assets/beancount-BgSHGh8x.js +1 -0
- package/dist/ui/assets/bibtex-CHM0blh-.js +1 -0
- package/dist/ui/assets/bicep-Bmn6On1c.js +1 -0
- package/dist/ui/assets/bird-CeWWThpZ.js +1 -0
- package/dist/ui/assets/blade-CQs288pm.js +1 -0
- package/dist/ui/assets/c-BIGW1oBm.js +1 -0
- package/dist/ui/assets/c3-Dp5svz6Z.js +1 -0
- package/dist/ui/assets/cairo-Ba9TFftT.js +1 -0
- package/dist/ui/assets/cdc-Dyf7ylpP.js +1 -0
- package/dist/ui/assets/chpl-QDWX03Hx.js +1 -0
- package/dist/ui/assets/clarity-Dn5IMItf.js +1 -0
- package/dist/ui/assets/clj-CZPsrr31.js +1 -0
- package/dist/ui/assets/closure-templates-Cv0TS44c.js +1 -0
- package/dist/ui/assets/cmake-D1j8_8rp.js +1 -0
- package/dist/ui/assets/cmd-T6t9F4r8.js +1 -0
- package/dist/ui/assets/cobol-D6oRQjbg.js +1 -0
- package/dist/ui/assets/codeowners-Bp6g37R7.js +1 -0
- package/dist/ui/assets/coffeescript-l2w2Enha.js +1 -0
- package/dist/ui/assets/console-JZr4tX8D.js +1 -0
- package/dist/ui/assets/coq-BqXF5kFF.js +1 -0
- package/dist/ui/assets/core-CcaQSRDK.js +12 -0
- package/dist/ui/assets/cpp-2YYZ9QPt.js +1 -0
- package/dist/ui/assets/cpp-macro-BD_u8FP2.js +1 -0
- package/dist/ui/assets/cql-BEVj694s.js +1 -0
- package/dist/ui/assets/crystal-HlmihOZ-.js +1 -0
- package/dist/ui/assets/cs-CYZm7ub4.js +1 -0
- package/dist/ui/assets/css-Z72CF80r.js +1 -0
- package/dist/ui/assets/csv-fuZLfV_i.js +1 -0
- package/dist/ui/assets/cue-D82EKSYY.js +1 -0
- package/dist/ui/assets/d-85-TOEBH.js +1 -0
- package/dist/ui/assets/dart-CtKP3W-j.js +1 -0
- package/dist/ui/assets/dax-CEL-wOlO.js +1 -0
- package/dist/ui/assets/desktop-BmXAJ9_W.js +1 -0
- package/dist/ui/assets/diff-D97Zzqfu.js +1 -0
- package/dist/ui/assets/dockerfile-D5eKlNGy.js +1 -0
- package/dist/ui/assets/dotenv-De0Btta0.js +1 -0
- package/dist/ui/assets/dream-maker-BtqSS_iP.js +1 -0
- package/dist/ui/assets/edge-BA25uoI8.js +1 -0
- package/dist/ui/assets/elisp-BQtZl9iK.js +1 -0
- package/dist/ui/assets/elixir-BzU7DNz_.js +1 -0
- package/dist/ui/assets/elm-DbKCFpqz.js +1 -0
- package/dist/ui/assets/engine-javascript-CuxHA8oH.js +141 -0
- package/dist/ui/assets/erb-B-_FUE16.js +1 -0
- package/dist/ui/assets/erl-btUkaAv0.js +1 -0
- package/dist/ui/assets/es-tag-css-Di8q07hx.js +1 -0
- package/dist/ui/assets/es-tag-glsl-DuY_gmju.js +1 -0
- package/dist/ui/assets/es-tag-html-CFifETgQ.js +1 -0
- package/dist/ui/assets/es-tag-sql-B4Gn1aEo.js +1 -0
- package/dist/ui/assets/es-tag-xml-BsowVsUr.js +1 -0
- package/dist/ui/assets/f95-OKqbxTCS.js +1 -0
- package/dist/ui/assets/fennel-BYunw83y.js +1 -0
- package/dist/ui/assets/fish-BvzEVeQv.js +1 -0
- package/dist/ui/assets/for-TD-2-mvr.js +1 -0
- package/dist/ui/assets/fs-2PIK3WrC.js +1 -0
- package/dist/ui/assets/fsl-DKWnMB5p.js +1 -0
- package/dist/ui/assets/ftl-anfqXHis.js +1 -0
- package/dist/ui/assets/gd-DmgsuTnj.js +1 -0
- package/dist/ui/assets/gdshader-DkwncUOv.js +1 -0
- package/dist/ui/assets/genie-D0YGMca9.js +1 -0
- package/dist/ui/assets/gherkin-DyxjwDmM.js +1 -0
- package/dist/ui/assets/git-commit-F4YmCXRG.js +1 -0
- package/dist/ui/assets/git-rebase-Bqs5jo_V.js +1 -0
- package/dist/ui/assets/github-dark-default-Cuk6v7N8.js +1 -0
- package/dist/ui/assets/gjs-gYW9DBuS.js +1 -0
- package/dist/ui/assets/gleam-BspZqrRM.js +1 -0
- package/dist/ui/assets/glsl-DplSGwfg.js +1 -0
- package/dist/ui/assets/gn-8iBmfbXw.js +1 -0
- package/dist/ui/assets/gnuplot-DdkO51Og.js +1 -0
- package/dist/ui/assets/go-C27-OAKa.js +1 -0
- package/dist/ui/assets/gql-MGiqm7Pm.js +1 -0
- package/dist/ui/assets/grammar-map-VuoEIDon.js +2 -0
- package/dist/ui/assets/groovy-gcz8RCvz.js +1 -0
- package/dist/ui/assets/gts-DqU1Me4G.js +1 -0
- package/dist/ui/assets/hack-V6hhs-tI.js +1 -0
- package/dist/ui/assets/haml-jtrXxllQ.js +1 -0
- package/dist/ui/assets/haxe-CfZj7gIn.js +1 -0
- package/dist/ui/assets/hbs-CsgKbzmm.js +1 -0
- package/dist/ui/assets/hcl-BWvSN4gD.js +1 -0
- package/dist/ui/assets/hjson-D5-asLiD.js +1 -0
- package/dist/ui/assets/hlsl-D3lLCCz7.js +1 -0
- package/dist/ui/assets/hs-cv9E3dfg.js +1 -0
- package/dist/ui/assets/html-derivative-zZehN5i_.js +1 -0
- package/dist/ui/assets/html-tMaWOgtj.js +1 -0
- package/dist/ui/assets/http-BS9aPmI9.js +1 -0
- package/dist/ui/assets/hurl-BxI2TCqe.js +1 -0
- package/dist/ui/assets/hxml-2-FPmUDs.js +1 -0
- package/dist/ui/assets/hy-DFXneXwc.js +1 -0
- package/dist/ui/assets/imba-DGztddWO.js +1 -0
- package/dist/ui/assets/index-BGiGoTsW.css +1 -0
- package/dist/ui/assets/index-BiOcGwMm.js +1 -0
- package/dist/ui/assets/index-qgPxqDeZ.js +40 -0
- package/dist/ui/assets/jade-CZYBTpjq.js +1 -0
- package/dist/ui/assets/java-CylS5w8V.js +1 -0
- package/dist/ui/assets/jinja-BV377aI7.js +1 -0
- package/dist/ui/assets/jinja-html-J_niBkwy.js +1 -0
- package/dist/ui/assets/jison-CI3IlJ66.js +1 -0
- package/dist/ui/assets/jl-DnT-B-wk.js +1 -0
- package/dist/ui/assets/json-Cp-IABpG.js +1 -0
- package/dist/ui/assets/json5-C9tS-k6U.js +1 -0
- package/dist/ui/assets/jsonc-Des-eS-w.js +1 -0
- package/dist/ui/assets/jsonl-DcaNXYhu.js +1 -0
- package/dist/ui/assets/jsonnet-DFQXde-d.js +1 -0
- package/dist/ui/assets/jsx-g9-lgVsj.js +1 -0
- package/dist/ui/assets/justfile-DM2KgpEt.js +1 -0
- package/dist/ui/assets/kdl-DV7GczEv.js +1 -0
- package/dist/ui/assets/kql-CAZwdKJH.js +1 -0
- package/dist/ui/assets/kts-DPNV9Nys.js +1 -0
- package/dist/ui/assets/latex-DJVikpzR.js +1 -0
- package/dist/ui/assets/lean4-CwwCO9Zr.js +1 -0
- package/dist/ui/assets/less-B1dDrJ26.js +1 -0
- package/dist/ui/assets/liquid-Bzms4plq.js +1 -0
- package/dist/ui/assets/lisp-d1OqKJ0v.js +1 -0
- package/dist/ui/assets/lit-C5SbI-X-.js +1 -0
- package/dist/ui/assets/llvm-BZoOZj88.js +1 -0
- package/dist/ui/assets/log-2UxHyX5q.js +1 -0
- package/dist/ui/assets/logo-BtOb2qkB.js +1 -0
- package/dist/ui/assets/lua-BaeVxFsk.js +1 -0
- package/dist/ui/assets/luau-nOZpfvQi.js +1 -0
- package/dist/ui/assets/makefile-BSLFislC.js +1 -0
- package/dist/ui/assets/markdown-nix-CTic1qqq.js +1 -0
- package/dist/ui/assets/markdown-vue-Cbnvtjg5.js +1 -0
- package/dist/ui/assets/marko-CPupNq7w.js +1 -0
- package/dist/ui/assets/matlab-D7o27uSR.js +1 -0
- package/dist/ui/assets/mbti-Myp4B69h.js +1 -0
- package/dist/ui/assets/md-Dt3v3XWf.js +1 -0
- package/dist/ui/assets/mdc-BlkeRyZp.js +1 -0
- package/dist/ui/assets/mdx-Cmh6b_Ma.js +1 -0
- package/dist/ui/assets/mips-D0hiF3sa.js +1 -0
- package/dist/ui/assets/mjs-BOef1GtE.js +1 -0
- package/dist/ui/assets/mmd-CtyOPoVJ.js +1 -0
- package/dist/ui/assets/mojo-DJz3ZmWd.js +1 -0
- package/dist/ui/assets/move-el3G9tDJ.js +1 -0
- package/dist/ui/assets/nar-CgJlRCyo.js +1 -0
- package/dist/ui/assets/nextflow-groovy-CedDknKX.js +1 -0
- package/dist/ui/assets/nf-BAJNoAAx.js +1 -0
- package/dist/ui/assets/nginx-BpAMiNFr.js +1 -0
- package/dist/ui/assets/nim-BCvQ9LSl.js +1 -0
- package/dist/ui/assets/nix-CNulEsOp.js +1 -0
- package/dist/ui/assets/nsis-BlV79W_Q.js +1 -0
- package/dist/ui/assets/nu-AJ1T3e-j.js +1 -0
- package/dist/ui/assets/objc-B7QIVcT6.js +1 -0
- package/dist/ui/assets/objective-cpp-CLxacb5B.js +1 -0
- package/dist/ui/assets/ocaml-C0hk2d4L.js +1 -0
- package/dist/ui/assets/odin-BBf5iR-q.js +1 -0
- package/dist/ui/assets/org-D8hinxRC.js +1 -0
- package/dist/ui/assets/pascal-D93ZcfNL.js +1 -0
- package/dist/ui/assets/perl-9_yHOIUr.js +1 -0
- package/dist/ui/assets/perl6-Czm8BUOa.js +1 -0
- package/dist/ui/assets/php-x4cDdYNa.js +1 -0
- package/dist/ui/assets/pkl-u5AG7uiY.js +1 -0
- package/dist/ui/assets/plsql-ChMvpjG-.js +1 -0
- package/dist/ui/assets/polar-C0HS_06l.js +1 -0
- package/dist/ui/assets/postcss-CXtECtnM.js +1 -0
- package/dist/ui/assets/potx-Zubr0-kU.js +1 -0
- package/dist/ui/assets/powerquery-CEu0bR-o.js +1 -0
- package/dist/ui/assets/prisma-Vru482bI.js +1 -0
- package/dist/ui/assets/prolog-CbFg5uaA.js +1 -0
- package/dist/ui/assets/properties-h29QGfEk.js +1 -0
- package/dist/ui/assets/protobuf-S0pP4B9O.js +1 -0
- package/dist/ui/assets/puppet-BMWR74SV.js +1 -0
- package/dist/ui/assets/purescript-CklMAg4u.js +1 -0
- package/dist/ui/assets/pwsh-BpVOxWsY.js +1 -0
- package/dist/ui/assets/py-DWrqmXJp.js +1 -0
- package/dist/ui/assets/ql-CuETlrBG.js +1 -0
- package/dist/ui/assets/qml-CAmqjwmT.js +1 -0
- package/dist/ui/assets/qmldir-C8lEn-DE.js +1 -0
- package/dist/ui/assets/qss-IeuSbFQv.js +1 -0
- package/dist/ui/assets/r-Cf5RLm7j.js +1 -0
- package/dist/ui/assets/racket-BqYA7rlc.js +1 -0
- package/dist/ui/assets/razor-CMtLDytp.js +1 -0
- package/dist/ui/assets/rb-COoUipX6.js +1 -0
- package/dist/ui/assets/reg-C-SQnVFl.js +1 -0
- package/dist/ui/assets/regex-4rjvoqip.js +1 -0
- package/dist/ui/assets/rel-C3B-1QV4.js +1 -0
- package/dist/ui/assets/riscv-BM1_JUlF.js +1 -0
- package/dist/ui/assets/ron-D8l8udqQ.js +1 -0
- package/dist/ui/assets/rosmsg-BJDFO7_C.js +1 -0
- package/dist/ui/assets/rs-BhsT8Mbu.js +1 -0
- package/dist/ui/assets/rst-C2w80Rnv.js +1 -0
- package/dist/ui/assets/ruby-signature-Dr53Dmnh.js +1 -0
- package/dist/ui/assets/sas-BGn39htG.js +1 -0
- package/dist/ui/assets/sass-Cj5Yp3dK.js +1 -0
- package/dist/ui/assets/scad-C1CbN4fu.js +1 -0
- package/dist/ui/assets/scala-CqE71os6.js +1 -0
- package/dist/ui/assets/scheme-C98Dy4si.js +1 -0
- package/dist/ui/assets/scss-Cw6XMfVs.js +1 -0
- package/dist/ui/assets/shader-Ca6eN1Do.js +1 -0
- package/dist/ui/assets/smalltalk-BOQMe2GC.js +1 -0
- package/dist/ui/assets/smithy-cds9vsN8.js +1 -0
- package/dist/ui/assets/solidity-DijEV5ha.js +1 -0
- package/dist/ui/assets/sparql-rVzFXLq3.js +1 -0
- package/dist/ui/assets/spl-CYnMqPaz.js +1 -0
- package/dist/ui/assets/sql-DkvoVjww.js +1 -0
- package/dist/ui/assets/ssh-config-_ykCGR6B.js +1 -0
- package/dist/ui/assets/stata-DACIj5bg.js +1 -0
- package/dist/ui/assets/styl-BRLZixNf.js +1 -0
- package/dist/ui/assets/surql-DuXNjW2b.js +1 -0
- package/dist/ui/assets/svelte-Co4ixzwc.js +1 -0
- package/dist/ui/assets/swift-C2oV4EkX.js +1 -0
- package/dist/ui/assets/system-verilog-0hqHdDBg.js +1 -0
- package/dist/ui/assets/systemd-4A_iFExJ.js +1 -0
- package/dist/ui/assets/talon-Byugm8Eo.js +1 -0
- package/dist/ui/assets/tasl-QIJgUcNo.js +1 -0
- package/dist/ui/assets/tcl-dwOrl1Do.js +1 -0
- package/dist/ui/assets/templ-8ecetEyQ.js +1 -0
- package/dist/ui/assets/tex-D96PA37w.js +1 -0
- package/dist/ui/assets/tfvars-C4cyYA4z.js +1 -0
- package/dist/ui/assets/toml-vGWfd6FD.js +1 -0
- package/dist/ui/assets/ts-muzv-B4Z.js +1 -0
- package/dist/ui/assets/tscn-DU9HihWa.js +1 -0
- package/dist/ui/assets/tsp-Ds05qIuC.js +1 -0
- package/dist/ui/assets/tsv-B_m7g4N7.js +1 -0
- package/dist/ui/assets/tsx-COt5Ahok.js +1 -0
- package/dist/ui/assets/turtle-BsS91CYL.js +1 -0
- package/dist/ui/assets/twig-C3N1ALdA.js +1 -0
- package/dist/ui/assets/typ-BREikwzP.js +1 -0
- package/dist/ui/assets/v-BGw2Nkan.js +1 -0
- package/dist/ui/assets/vala-CsfeWuGM.js +1 -0
- package/dist/ui/assets/vb-Cu-pLBUe.js +1 -0
- package/dist/ui/assets/verilog-nZwndyjY.js +1 -0
- package/dist/ui/assets/vhdl-CeAyd5Ju.js +1 -0
- package/dist/ui/assets/vimscript-Ct4t_lzx.js +1 -0
- package/dist/ui/assets/vue-Du5E_mGo.js +1 -0
- package/dist/ui/assets/vue-directives-OHNAz0dh.js +1 -0
- package/dist/ui/assets/vue-html-D5SNZKGg.js +1 -0
- package/dist/ui/assets/vue-interpolations-CiJO6OZ_.js +1 -0
- package/dist/ui/assets/vue-sfc-style-variable-injection-Bt3_SK3a.js +1 -0
- package/dist/ui/assets/vue-vine-CXe2Z88O.js +1 -0
- package/dist/ui/assets/vy-tAjnZflE.js +1 -0
- package/dist/ui/assets/wasm-MzD3tlZU.js +1 -0
- package/dist/ui/assets/wenyan-BV7otONQ.js +1 -0
- package/dist/ui/assets/wgsl-Dx-B1_4e.js +1 -0
- package/dist/ui/assets/wiki-CEMRVyHf.js +1 -0
- package/dist/ui/assets/wit-5i3qLPDT.js +1 -0
- package/dist/ui/assets/wl-CprSW-RW.js +1 -0
- package/dist/ui/assets/xml-sdJ4AIDG.js +1 -0
- package/dist/ui/assets/xsl-CtQFsRM5.js +1 -0
- package/dist/ui/assets/yml-DbVfSbck.js +1 -0
- package/dist/ui/assets/zenscript-DVFEvuxE.js +1 -0
- package/dist/ui/assets/zig-VOosw3JB.js +1 -0
- package/dist/ui/assets/zsh-A78Q77OD.js +1 -0
- package/dist/ui/index.html +17 -0
- package/package.json +49 -4
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../packages/orchestrator/src/config.ts", "../packages/orchestrator/src/installation-root.ts", "../packages/protocol/src/pi.ts", "../packages/protocol/src/envelopes.ts", "../packages/protocol/src/own-installation.ts", "../packages/protocol/src/bytes.ts", "../packages/orchestrator/src/agent-store.ts", "../packages/orchestrator/src/supervisor.ts", "../packages/orchestrator/src/policy.ts", "../packages/orchestrator/src/env.ts", "../packages/orchestrator/src/rpc.ts", "../packages/orchestrator/src/framing.ts", "../packages/orchestrator/src/resume.ts", "../packages/orchestrator/src/capabilities.ts", "../packages/orchestrator/src/signing.ts", "../packages/orchestrator/src/fragment.ts", "../packages/orchestrator/src/files.ts", "../packages/orchestrator/src/state.ts", "../packages/orchestrator/src/transport.ts", "../packages/orchestrator/src/package-meta.ts", "../packages/orchestrator/src/registry.ts", "../packages/orchestrator/src/static-ui.ts", "../packages/orchestrator/src/sandbox-guard.ts", "../packages/orchestrator/src/server.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Configuration and the app-owned default workspace (US-3, D-5).\n *\n * The whole point of US-3: \"start without a path\" must not silently default to `$HOME` or\n * the orchestrator's cwd, because the sandbox grants read/write to the directory it names.\n *\n * D-5 therefore constrains the location. The default workspace must NOT be inside:\n *\n * - `$HOME` proper safehouse's base profile grants parts of the home tree\n * - `/tmp`, `/var/folders` safehouse grants these GLOBALLY (any session can read them)\n * - `~/.pi` granted read/write to every session (C6)\n *\n * A scratch directory under any of those would make its \"isolation\" a fiction: a second\n * session's policy would reach it regardless of the per-session workdir grant.\n *\n * `~/.local/share/pi-central/workspaces/` satisfies D-5: it is under `$HOME` as a path but\n * is not granted by any base profile, and it is not a tmp or agent dir.\n */\nimport { homedir } from \"node:os\";\nimport { resolveInstallationRoot } from \"./installation-root.ts\";\nimport { join, resolve, sep } from \"node:path\";\nimport { looksLikeAgentTree } from \"./agent-store.ts\";\n\nexport interface Config {\n /** Root under which one directory per scratch session is created. */\n workspaceRoot: string;\n /** Where Pi Central keeps its own state (Q7). Never inside ~/.pi. */\n stateRoot: string;\n /** Pi's session store; overridable for tests. */\n sessionsRoot: string;\n /** Safehouse binary. */\n safehouse: string;\n /** Pi binary. */\n pi: string;\n /**\n * The agent identity's credential root (ADR-0010).\n *\n * Holds `gh/` (GH_CONFIG_DIR), `gitconfig` (GIT_CONFIG_GLOBAL), and `ssh/`. Either owned by\n * Pi Central (`managed`) or by an external tree the user maintains (`external`), in which\n * case Pi Central reads and verifies but never writes.\n */\n agentRoot: string;\n /**\n * Where rendered policy fragments are written (ADR-0010 \u00A72).\n *\n * Under the state root, so it is outside `~/.pi` (Q7) and outside every session's workdir.\n */\n fragmentDir: string;\n /**\n * Whether Pi Central owns `agentRoot`. When false, credentials are only ever read, and\n * re-import is an explicit action (ADR-0010 \u00A75).\n */\n agentRootManaged: boolean;\n /**\n * The user's home directory.\n *\n * Carried in the config rather than re-derived per use, and sent to the UI so it can render\n * `~/git/x` instead of `/Users/x/git/x`. Every path in the rail and the header is abbreviated\n * with it, which is what makes a list of 20 rows scannable.\n */\n home: string;\n /**\n * Where this Pi Central installation lives, if it could be identified (issue #25).\n *\n * Travels in the snapshot so the UI can warn about it LIVE, as the user types a directory into\n * the sheet \u2014 before any session exists to carry the warning. Without it the browser would have to\n * guess, and a guess about \"am I pointing at my own checkout?\" is exactly the kind of claim the UI\n * is not allowed to invent (C8's family).\n *\n * `undefined` when the walk could not identify it, which is reported as unknown rather than as\n * safe.\n */\n installationRoot: string | undefined;\n /**\n * The agent directory holding Pi's provider configuration (`auth.json`).\n *\n * Usually the same as `agentRoot`, but not necessarily: a session inherits the default\n * `~/.pi/agent` unless `PI_CODING_AGENT_DIR` is set, and Pi's provider `!command`\n * indirections live there. Kept separate so the policy is derived from where the provider\n * configuration **actually** is, rather than from an assumption (C13).\n */\n providerAgentDir: string;\n}\n\n/**\n * Choose the agent credential root.\n *\n * Precedence, and why:\n *\n * 1. `PI_CENTRAL_AGENT_ROOT` \u2014 explicit always wins, and implies `external` ownership.\n * 2. An existing tree at `~/.agent` \u2014 **adopted**, so a machine already set up by\n * `pi-sandbox-hardening` is imported rather than duplicated. This is the path that makes\n * an interactive setup and Pi Central share one credential tree instead of drifting\n * (ADR-0010 \u00A75: one writer per file).\n * 3. A Pi-Central-owned tree under the state root \u2014 the fresh-install default.\n *\n * `~/.agent` is checked for a *marker* rather than mere existence: a stray empty directory\n * must not silently become the credential root.\n */\nexport function resolveAgentRoot(\n stateRoot: string,\n env: NodeJS.ProcessEnv = process.env,\n home: string = homedir(),\n): { agentRoot: string; managed: boolean } {\n const explicit = env[\"PI_CENTRAL_AGENT_ROOT\"];\n if (explicit !== undefined && explicit !== \"\") return { agentRoot: explicit, managed: false };\n\n const conventional = join(home, \".agent\");\n if (looksLikeAgentTree(conventional)) return { agentRoot: conventional, managed: false };\n\n return { agentRoot: join(stateRoot, \"agents\", \"default\"), managed: true };\n}\n\nexport function defaultConfig(): Config {\n const home = homedir();\n const stateRoot = process.env.PI_CENTRAL_STATE ?? join(home, \".config\", \"pi-central\");\n const { agentRoot, managed } = resolveAgentRoot(stateRoot);\n return {\n workspaceRoot:\n process.env.PI_CENTRAL_WORKSPACE ?? join(home, \".local\", \"share\", \"pi-central\", \"workspaces\"),\n stateRoot,\n sessionsRoot: process.env.PI_SESSIONS_DIR ?? join(home, \".pi\", \"agent\", \"sessions\"),\n safehouse: process.env.PI_CENTRAL_SAFEHOUSE ?? \"safehouse\",\n pi: process.env.PI_CENTRAL_PI ?? \"pi\",\n agentRoot,\n fragmentDir: join(stateRoot, \"policy\"),\n agentRootManaged: managed,\n home,\n // Where a session actually reads provider configuration. Defaults to Pi's own agent dir,\n // because Pi Central does not currently pass `PI_CODING_AGENT_DIR`; if that changes, this\n // must follow, or the policy would grant keychain access for the wrong location.\n providerAgentDir: process.env.PI_CODING_AGENT_DIR ?? join(home, \".pi\", \"agent\"),\n // Derived from the ORCHESTRATOR's module URL, never from configuration (issue #25): see\n // `installation-root.ts`. `defaultConfig` is the orchestrator's entry point, so resolving here\n // means the answer is the running installation and not whatever a config file claims.\n installationRoot: resolveInstallationRoot(),\n };\n}\n\n/**\n * Paths the default workspace must not live under (D-5).\n *\n * Two kinds, and conflating them is a bug:\n *\n * - `exact` \u2014 banned only when the root IS this path. `$HOME` is the case: defaulting\n * to `$HOME` itself is the mistake US-3 exists to prevent, but\n * `$HOME/.local/share/pi-central` is perfectly fine and is exactly the\n * recommended location.\n * - `subpath` \u2014 banned when the root is this path or inside it, because the path is\n * recursively readable/writable.\n *\n * Note what is NOT a blanket subpath ban: `$HOME/.local/share`. Safehouse grants it only\n * as a *literal* (directory listing, for traversal) and says so itself:\n *\n * ;; Intentionally avoid broad RW grants like ~/.cache, ~/Library/Caches,\n * ;; ~/.local/share, ~/.local/state.\n *\n * The recursive grants that DO exist under `.local/share` are for specific tools (bun, go,\n * uv, gh, \u2026) and are listed individually, because a workspace inside one of those would be\n * reachable by every other session.\n */\nconst LOCAL_SHARE_TOOLS = [\n \"bun\", \"go\", \"uv\", \"gh\", \"glab-cli\", \"composer\", \"pypoetry\", \"pdm\", \"hatch\",\n \"mise\", \"asdf\", \"pkgx\", \"fnm\", \"pnpm\", \"aider\", \"amp\", \"claude\", \"cline\",\n \"cursor-agent\", \"goose\", \"kilo\", \"opencode\", \"opentui\", \"containers\",\n];\nconst LOCAL_STATE_TOOLS = [\"bun\", \"gh\", \"uv\", \"mise\", \"asdf\", \"amp\", \"cline\", \"kilo\", \"opencode\"];\n\nexport function forbiddenWorkspaceRoots(home = homedir()): { exact: string[]; subpath: string[] } {\n return {\n exact: [home],\n subpath: [\n \"/tmp\",\n \"/var/folders\",\n \"/private/tmp\",\n \"/private/var/folders\",\n join(home, \".pi\"), // C6: readable/writable from every session\n join(home, \".cache\"),\n join(home, \"Library\", \"Caches\"),\n ...LOCAL_SHARE_TOOLS.map((name) => join(home, \".local\", \"share\", name)),\n ...LOCAL_STATE_TOOLS.map((name) => join(home, \".local\", \"state\", name)),\n ],\n };\n}\n\n/** True when `child` is `parent` or inside it, compared on resolved paths (not prefixes). */\nfunction contains(parent: string, child: string): boolean {\n const p = resolve(parent);\n const c = resolve(child);\n if (c === p) return true;\n return c.startsWith(p.endsWith(sep) ? p : p + sep);\n}\n\nexport interface WorkspaceViolation {\n root: string;\n reason: string;\n}\n\n/**\n * Validate the workspace root against D-5. Returns violations rather than throwing, so the\n * caller decides whether to refuse or warn.\n *\n * `/tmp` and `/var/folders` are checked as *roots*, but on macOS those are symlinks to\n * `/private/...`, so both spellings are checked.\n */\nexport function checkWorkspaceRoot(root: string, home = homedir()): WorkspaceViolation[] {\n const resolved = resolve(root);\n const { exact, subpath } = forbiddenWorkspaceRoots(home);\n const violations: WorkspaceViolation[] = [];\n\n // `$HOME` itself: equality only. `$HOME/.local/share/pi-central` is the recommended\n // location, so a containment test here would reject the very path we suggest.\n for (const path of exact) {\n if (resolve(path) === resolved) {\n violations.push({\n root: path,\n reason: \"the default must not be $HOME itself, or a scratch session would hold a policy over the home tree (D-5)\",\n });\n }\n }\n\n for (const path of subpath) {\n if (!contains(path, resolved)) continue;\n const reason =\n path === join(home, \".pi\")\n ? \"inside ~/.pi, which every sandboxed session can read and write (C6)\"\n : contains(\"/tmp\", resolved) || contains(\"/var/folders\", resolved)\n ? `${path} is granted globally by the base safehouse profile (D-5)`\n : `${path} is a recursive read/write grant in the base safehouse profile (D-5)`;\n violations.push({ root: path, reason });\n }\n return violations;\n}\n\n/** The directory a scratch session gets. One per session, so two never share a policy. */\nexport function scratchDirFor(config: Config, sessionId: string): string {\n if (!/^[A-Za-z0-9._-]+$/.test(sessionId)) {\n throw new Error(`refusing to build a workspace path from an unsafe id: ${JSON.stringify(sessionId)}`);\n }\n return join(config.workspaceRoot, sessionId);\n}\n\n/** A session is \"scratch\" when its cwd is inside the app-owned workspace root. */\nexport function isScratch(config: Config, cwd: string): boolean {\n return contains(config.workspaceRoot, cwd);\n}\n", "/**\n * Where this Pi Central installation lives, derived at runtime (issue #25).\n *\n * **Not configured, and that is the point** \u2014 the issue's constraint. A path in a config file would\n * be right for exactly one checkout (the author's) and silently wrong for everyone else, which is\n * worse than no detection because it would look like it was working.\n *\n * In its own module rather than beside the static file server, because the two answer unrelated\n * questions: `static-ui.ts` resolves where the *UI bundle* is, and this resolves where *the\n * installation* is. The supervisor needs this one and has no business importing an HTTP file\n * server to get it.\n *\n * Derived from THIS module's URL and walked up to the repository root. The walk is bounded and\n * checks for the pipeline's own package directories rather than assuming a fixed depth, so moving a\n * file within the package cannot quietly point the warning at the wrong directory. When the walk\n * finds nothing, `undefined` is returned and callers report the path as unidentifiable rather than\n * as safe \u2014 \"I could not tell where I am\" and \"you are not pointing at me\" are different statements.\n */\nimport { existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { looksLikePiCentral } from \"@pi-central/protocol\";\n\nexport function resolveInstallationRoot(moduleUrl: string = import.meta.url): string | undefined {\n let current = fileURLToPath(new URL(\".\", moduleUrl));\n // Bounded so a surprising layout cannot walk to `/` and claim the filesystem is Pi Central.\n for (let depth = 0; depth < 8; depth += 1) {\n const parent = resolve(current, \"..\");\n if (parent === current) return undefined;\n if (looksLikeInstallationRoot(parent)) return parent;\n current = parent;\n }\n return undefined;\n}\n\n/**\n * Is this directory the root of a Pi Central checkout?\n *\n * Recognised by its WORKSPACE PACKAGES rather than by a directory name, because a checkout may be\n * called anything (`pi-central-dev`, `pi-central`, a temporary clone). All three are required: two\n * would mistake an unrelated repository that happens to have a `packages/ui`.\n *\n * `existsSync` is checked here and the list is then handed to the shared `looksLikePiCentral`, so\n * the naming rule lives in one place and this cannot disagree with the UI about what a checkout\n * looks like.\n *\n * The existence check is load-bearing, and its absence was a real bug: a list built by\n * concatenation trivially ENDS WITH the marker it was built from, so\n * `looksLikePiCentral([\"/\u2026/packages/orchestrator/packages/ui\", \u2026])` answered \"yes\" for every\n * directory. The walk therefore stopped at its first step and reported `packages/orchestrator` as\n * the installation root \u2014 a warning that would have pointed at the wrong tree for every session.\n */\nfunction looksLikeInstallationRoot(directory: string): boolean {\n const markers = [\n join(directory, \"packages\", \"orchestrator\"),\n join(directory, \"packages\", \"ui\"),\n join(directory, \"packages\", \"protocol\"),\n ];\n if (!markers.every((marker) => existsSync(marker))) return false;\n return looksLikePiCentral(markers);\n}\n", "/**\n * The Pi RPC surface Pi Central depends on, as types only.\n *\n * The load-bearing decision (docs/STACK.md, ADR-0005): these are `import type`, so Pi is a\n * devDependency of this package and never enters the runtime bundle. If Pi changes a\n * command or event name, `tsc` fails here instead of the supervisor silently\n * misunderstanding the protocol.\n *\n * ADR-0009: the orchestrator does NOT import Pi's agent core at runtime. Discovery is our\n * own parser. The only runtime contact with Pi is the `pi --mode rpc` child process.\n */\nimport type {\n RpcCommand,\n RpcResponse,\n RpcSessionState,\n RpcExtensionUIRequest,\n RpcExtensionUIResponse,\n} from \"@earendil-works/pi-coding-agent\";\n\nexport type { RpcCommand, RpcResponse, RpcSessionState, RpcExtensionUIRequest, RpcExtensionUIResponse };\n\n/** JSON records Pi emits on stdout. Pi's own union is broad; we narrow where we act. */\nexport interface PiRecord {\n type: string;\n [key: string]: unknown;\n}\n\n/** A response Pi has correlated to a command `id`. */\nexport function isRpcResponse(record: PiRecord): record is PiRecord & RpcResponse {\n return record.type === \"response\" && typeof record.id === \"string\";\n}\n\n/** The dialog methods that block the agent until answered (docs/PROTOCOL.md). */\nexport const BLOCKING_DIALOG_METHODS = [\"select\", \"confirm\", \"input\", \"editor\"] as const;\nexport type BlockingDialogMethod = (typeof BLOCKING_DIALOG_METHODS)[number];\n\nconst BLOCKING = new Set<string>(BLOCKING_DIALOG_METHODS);\n\n/**\n * Extension UI methods that expect **no response** (docs/rpc-extension-ui.md).\n *\n * This list is the load-bearing one, and it is deliberately a list of the *non-dialog* family\n * rather than of the dialogs. Both families are documented and closed for Pi 1.4.0 \u2014 five\n * fire-and-forget plus four dialogs, which is exactly the nine requests observed in the startup\n * burst \u2014 so either could be enumerated. Which one is enumerated decides how to fail on a\n * method nobody has seen before:\n *\n * - Enumerate the DIALOGS: an unrecognized method is ignored. If a future Pi adds a blocking\n * method, the agent waits forever for a response nobody knows to send. Silent, permanent,\n * unrecoverable without restarting the session.\n * - Enumerate the NON-DIALOGS (this): an unrecognized method is treated as a dialog. The user\n * sees it and can cancel. A spurious dialog costs a click; it cannot cost the session.\n *\n * The recoverable direction wins. This is the same lesson as C12/C13 in a different place: a\n * silent, plausible-looking non-failure is worse than a visible annoyance.\n */\nexport const FIRE_AND_FORGET_UI_METHODS = [\n \"notify\",\n \"setStatus\",\n \"setWidget\",\n \"setTitle\",\n \"set_editor_text\",\n] as const;\n\nexport type FireAndForgetUIMethod = (typeof FIRE_AND_FORGET_UI_METHODS)[number];\n\nconst FIRE_AND_FORGET = new Set<string>(FIRE_AND_FORGET_UI_METHODS);\n\n/**\n * Does this extension UI request need an answer?\n *\n * The default for an unknown method is **yes**, because that is the recoverable direction: an\n * unanswerable request that Pi is waiting on hangs the session with no visible cause, whereas a\n * request that needed no answer merely shows a dialog the user dismisses.\n */\nexport function isBlockingDialog(record: PiRecord): boolean {\n if (record.type !== \"extension_ui_request\") return false;\n return !FIRE_AND_FORGET.has(String(record.method ?? \"\"));\n}\n\n/** Is this a method Pi Central recognizes as expecting no response? */\nexport function isFireAndForget(record: PiRecord): boolean {\n return record.type === \"extension_ui_request\" && FIRE_AND_FORGET.has(String(record.method ?? \"\"));\n}\n\n/** Whether the method is one this build knows how to render. */\nexport function isKnownDialogMethod(method: string): boolean {\n return BLOCKING.has(method);\n}\n\n/**\n * The commands Pi Central is allowed to send.\n *\n * A closed union, on purpose: the supervisor must not be able to send an arbitrary object\n * derived from untrusted input (docs/ARCHITECTURE.md, trust boundary rules).\n */\nexport type Command =\n | { type: \"get_state\" }\n | { type: \"get_session_stats\" }\n /**\n * The existing conversation.\n *\n * Needed because Pi does NOT re-emit a resumed session's history as events: without this\n * command the UI renders \"No messages yet\" for a session with hundreds of them. It was\n * missing from this union while `rpc.send` accepted any object, so the omission could not be\n * caught by the compiler \u2014 see the note on `send`.\n */\n | { type: \"get_messages\" }\n /**\n * The models Pi can currently offer.\n *\n * Also absent from this union while being called from `verify-three.ts`, which is how the\n * C13 check (can the session reach a model?) was written without the compiler noticing. Making\n * `send` take this union found both omissions in the same minute.\n */\n | { type: \"get_available_models\" }\n | { type: \"set_model\"; provider: string; modelId: string }\n | { type: \"get_available_thinking_levels\" }\n | { type: \"set_thinking_level\"; level: string }\n | { type: \"prompt\"; message: string; streamingBehavior?: \"steer\" | \"followUp\" }\n | { type: \"steer\"; message: string }\n | { type: \"follow_up\"; message: string }\n /**\n * Empty the queue and return what was in it.\n *\n * Pi's `clear_queue` is handled as `session.clearQueue()` and answered with\n * `{ steering, followUp }` (rpc-mode.js), which is what makes Esc able to give the text back\n * instead of discarding it (issue #3).\n */\n | { type: \"clear_queue\" }\n | { type: \"abort\" }\n | { type: \"set_session_name\"; name: string }\n | { type: \"bash\"; command: string; excludeFromContext?: boolean };\n\n/** Extension UI responses are fire-and-forget: they carry no `id` of our own. */\nexport type DialogAnswer =\n | { type: \"extension_ui_response\"; id: string; value: string }\n | { type: \"extension_ui_response\"; id: string; confirmed: boolean }\n | { type: \"extension_ui_response\"; id: string; cancelled: true };\n", "/**\n * The orchestrator \u2194 UI contract.\n *\n * Control and events travel on the same socket but have different trust levels\n * (docs/ARCHITECTURE.md): control is user intent (trusted), events carry model output\n * (untrusted). They are therefore distinguished **structurally**, by envelope type, not by\n * convention. A client cannot smuggle control by shaping an event payload.\n *\n * This package defines the shapes. The transport that carries them is Phase 2.\n */\nimport type { PiRecord } from \"./pi.ts\";\n\nexport type LiveState =\n | \"registered\"\n | \"starting\"\n | \"idle\"\n | \"streaming\"\n | \"awaiting-approval\"\n | \"stopped\"\n | \"failed\";\n\n/**\n * A session Pi Central has discovered on disk. Untrusted in content, trusted in shape.\n *\n * Optional fields admit `undefined` explicitly. These values come from a JSON record, where\n * \"the key is absent\" and \"the key is null/undefined\" are the same thing for display\n * purposes; narrowing them would force conditional construction everywhere for no gain.\n */\nexport interface HistoricalSession {\n path: string;\n id: string;\n cwd: string;\n name?: string | undefined;\n created: string;\n modifiedMs: number;\n bytes: number;\n messageCount: number;\n firstMessage: string;\n /**\n * Whether the recorded cwd still exists. Validated at discovery time for display, and\n * again before every resume (D-4), because a directory can vanish between the two.\n */\n cwdExists: boolean;\n /** True when this session lives in the app-owned default workspace (US-3). */\n scratch: boolean;\n}\n\n/** A session Pi Central is supervising. This is the only source of truth for \"running\" (C3). */\nexport interface LiveSession {\n id: string;\n name: string;\n cwd: string;\n state: LiveState;\n mode: \"sandboxed\" | \"direct\";\n /** Human-readable description of the isolation actually in force. */\n isolation: string;\n /** The exact command line, for display. Never executed verbatim from here. */\n command: string;\n scratch: boolean;\n model?: string | undefined;\n provider?: string | undefined;\n thinkingLevel?: string | undefined;\n sessionFile?: string | undefined;\n /**\n * Pi's OWN session id, a uuid, from `get_state`.\n *\n * Distinct from `id`, and the distinction is not cosmetic: `id` is Pi Central's short handle\n * (`s1`) that addresses the process, while this identifies the transcript. Showing `s1` under\n * the label \"Session id\" states something false \u2014 Pi's id is `01a171ff-\u2026` \u2014 and the uuid is\n * what a person needs in order to find the session file or correlate it with `pi --session`.\n */\n sessionId?: string | undefined;\n /**\n * Whether the existing conversation has been fetched from Pi.\n *\n * Guards a single load: the UI reducer APPENDS every message it receives, so loading twice\n * would duplicate the whole transcript. Exposed so that property is directly testable.\n */\n transcriptLoaded?: boolean | undefined;\n messageCount: number;\n pending: number;\n contextPercent: number | null;\n tokensTotal: number | null;\n cost: number | null;\n dialogs: PendingDialog[];\n error?: string | undefined;\n warnings: string[];\n startedAt: number;\n}\n\nexport interface PendingDialog {\n id: string;\n method: string;\n title?: string | undefined;\n message?: string | undefined;\n options?: string[] | undefined;\n placeholder?: string | undefined;\n prefill?: string | undefined;\n receivedAt: number;\n}\n\n/** A model Pi can offer for a session. A subset of Pi's `Model`, chosen for display. */\nexport interface ModelOption {\n provider: string;\n id: string;\n /** The human name, when Pi reports one. Falls back to `id` for display. */\n name?: string | undefined;\n /**\n * Whether the model supports extended thinking.\n *\n * Displayed rather than acted on here: the authoritative constraint is\n * `get_available_thinking_levels` for the model (issue #2), because `reasoning: false` does not\n * by itself say WHICH levels are available.\n */\n reasoning?: boolean | undefined;\n}\n\n/** Commands the UI may send. A closed union \u2014 the UI cannot name an arbitrary operation. */\nexport type Control =\n | { kind: \"list\" }\n | { kind: \"start\"; cwd?: string; name?: string; mode?: \"sandboxed\" | \"direct\" }\n | { kind: \"prompt\"; sessionId: string; message: string; behavior?: \"steer\" | \"followUp\" }\n /**\n * Empty Pi's message queue, optionally stopping the run.\n *\n * `andStop` is a flag rather than two controls because the two are always one gesture: Esc\n * stops the run *and* returns the queue to the composer. Splitting them would let a caller\n * abort while silently discarding what the user typed, which is the bug this exists to fix.\n *\n * The cleared texts come back in a `queue` event, so \"what was removed\" is reported rather\n * than guessed \u2014 the composer restores exactly that text, not a reconstruction of it.\n */\n | { kind: \"clearQueue\"; sessionId: string; andStop: boolean }\n | { kind: \"abort\"; sessionId: string }\n | { kind: \"stop\"; sessionId: string }\n | { kind: \"rename\"; sessionId: string; name: string }\n /** Change the model. Carries provider AND id because `set_model` requires both (PROTOCOL.md). */\n | { kind: \"setModel\"; sessionId: string; provider: string; modelId: string }\n /** Ask for the session's available models. Answered with `models` or `modelsError`. */\n | { kind: \"listModels\"; sessionId: string }\n /** Change the thinking level. Only levels `listThinkingLevels` reported should be offered. */\n | { kind: \"setThinkingLevel\"; sessionId: string; level: string }\n /** Ask which thinking levels the CURRENT model supports. */\n | { kind: \"listThinkingLevels\"; sessionId: string }\n | { kind: \"answerDialog\"; sessionId: string; dialogId: string; answer: { value: string } | { confirmed: boolean } | { cancelled: true } }\n /**\n * List a session's working directory (issue #8).\n *\n * Paths are **relative to the session's cwd** and never absolute. That is not a convenience: the\n * orchestrator resolves each relative path and verifies the result is inside the cwd, so an\n * absolute path would mean the client and the boundary disagreed about what was being asked for.\n * The orchestrator refuses one outright rather than interpreting it (`files.ts`).\n *\n * `sessionId` rather than a bare path, because the confinement is per session: the root is that\n * session's cwd, so \"which directory\" is answered by \"which session\".\n */\n | { kind: \"listFiles\"; sessionId: string; path: string; includeHidden?: boolean }\n /** Read one text file, confined the same way. Answered with `file` or `fileError`. */\n | { kind: \"readFile\"; sessionId: string; path: string }\n | { kind: \"resume\"; path: string; mode?: \"sandboxed\" | \"direct\" };\n\n/**\n * Facts about the running installation, for the Settings surface (issue #58).\n *\n * These are things the browser cannot discover for itself \u2014 the orchestrator is a local process that\n * already knows them, and three of the four were previously printed ONLY to the terminal at startup,\n * so a user who closed that terminal could not answer \"where are my sessions on disk?\".\n */\nexport interface InstallFacts {\n /**\n * The orchestrator's own version, read from the `package.json` it was shipped in (ADR-0011).\n *\n * Never a constant in either codebase: it must report the artifact that is actually running, or\n * \"is this the build with the fix?\" cannot be answered from the process being asked.\n */\n version: string;\n /** Where Pi's session files are discovered from. */\n sessionsRoot: string;\n /** The app-owned workspace used when a session is started without a directory (US-1). */\n workspaceRoot: string;\n /** The agent's credential/identity root. */\n agentRoot: string;\n /** True when Pi Central manages that root itself, rather than using an external one. */\n agentRootManaged: boolean;\n /**\n * The wire version the orchestrator speaks.\n *\n * Also carried on `hello`, and duplicated here deliberately: `hello` is consumed once by the\n * connection and never stored, while this surface must render the pair (orchestrator vs UI) from\n * the authoritative snapshot \u2014 including after a reconnect. Recovering it from `hello` would mean a\n * second piece of connection state kept in parallel with `store.ts`, which is exactly the split\n * this codebase avoids: everything the UI shows comes from an event (`store.ts`).\n */\n protocol: number;\n}\n\n/**\n * A page of history plus the true total it was drawn from.\n *\n * Kept as a pair so a caller cannot accidentally present the page size as the total. The two\n * travel together for the same reason `{ sessions, total }` does in `registry.ts`.\n */\nexport interface HistoryPage {\n sessions: HistoricalSession[];\n /** Total session files on disk, which may exceed `sessions.length`. */\n total: number;\n}\n\n/** Events the UI may receive. `record` is untrusted and must be sanitized before render. */\nexport type Event =\n | {\n kind: \"snapshot\";\n live: LiveSession[];\n history: HistoricalSession[];\n /**\n * Facts about the running installation (issue #58).\n *\n * Optional, and **a new optional field is deliberately NOT a protocol-version change**: a\n * receiver ignores a field it does not know, so an older UI meeting a newer orchestrator is\n * unaffected, and a newer UI meeting an older orchestrator renders \"not reported\" (the UI\n * handles the absent case rather than assuming it). `wire-surface.test.ts` fingerprints KINDS,\n * and this is the gap it documents \u2014 which is why the absent case is a tested state here\n * rather than an afterthought. The alternative, sending three new events, would have been a\n * breaking change to fix a cosmetic one.\n */\n install?: InstallFacts | undefined;\n /** True history total, as distinct from the length of `history`. */\n historyTotal: number;\n /**\n * The orchestrator's `$HOME`, for abbreviating paths in the UI.\n *\n * The browser cannot discover this: it is a local single-user tool talking to a process\n * that already knows the answer. Without it every path renders in full, so the rail loses\n * the `~/\u2026` form that makes rows scannable.\n */\n home: string;\n /**\n * Where the orchestrator's own installation lives, if it could identify it (issue #25).\n *\n * Sent so the UI can warn LIVE, while a directory is being typed into the sheet \u2014 before any\n * session exists to carry the warning. `undefined` means the orchestrator could not identify\n * its own root, which is reported as unknown rather than as safe.\n */\n installationRoot?: string | undefined;\n }\n | { kind: \"session\"; session: LiveSession }\n | { kind: \"record\"; sessionId: string; record: PiRecord }\n /**\n * The models Pi can offer for a session, in response to `listModels`.\n *\n * A REQUEST-shaped event rather than part of `LiveSession`, and that is a deliberate choice:\n * the catalogue is hundreds of entries (426 measured across three providers), so folding it\n * into every session update would resend the whole list on each state refresh. It changes only\n * when the provider configuration does, so it is fetched on demand.\n *\n * `error` is present instead of `models` when Pi could not answer \u2014 never an empty list, which\n * would render as \"this session has no models\" when the truth is \"the question failed\".\n */\n | { kind: \"models\"; sessionId: string; models: ModelOption[] }\n | { kind: \"modelsError\"; sessionId: string; message: string }\n /**\n * The thinking levels the CURRENT model supports, in response to `listThinkingLevels`.\n *\n * Per-model rather than global: a model without extended thinking has fewer levels, so a list\n * fetched for one model can offer a level the next model cannot use.\n */\n | { kind: \"thinkingLevels\"; sessionId: string; levels: string[] }\n | { kind: \"thinkingLevelsError\"; sessionId: string; message: string }\n /**\n * A session's EXISTING conversation, fetched when it starts or resumes.\n *\n * Separate from `record` on purpose. `record` is the live stream and is appended to; this is\n * the authoritative past and REPLACES what the client holds. Encoding both in one event would\n * make \"append or replace?\" undecidable from the payload, and a reconnect would then either\n * duplicate the conversation or lose the newest turn.\n */\n | { kind: \"transcript\"; sessionId: string; messages: unknown[] }\n /**\n * The conversation could not be read.\n *\n * A distinct event rather than an empty `transcript`, because the two mean opposite things: an\n * empty conversation is a fact about the session, and a failed read is a fact about the attempt.\n * Collapsing them made the UI show \"Loading this session's history\u2026\" forever, which is a claim\n * that something is still in progress when nothing is.\n */\n | { kind: \"transcriptError\"; sessionId: string; message: string }\n | { kind: \"history\"; history: HistoricalSession[]; total: number }\n /**\n * The session's CURRENT message queue, in full.\n *\n * A whole-list event rather than a delta, for the same reason `transcript` replaces rather than\n * appends: Pi already reports the entire queue on every `queue_update`, so a delta encoding\n * would mean reconstructing state the source hands over complete. It is also what makes\n * `clearQueue` reportable \u2014 the emptied queue arrives as `{ steering: [], followUp: [] }`\n * *and* the cleared texts are read from the same structure, so \"nothing is queued\" and \"this is\n * what was queued\" cannot disagree.\n *\n * `LiveSession.pending` remains the count (it is what the rail shows); this is the texts. The\n * two can legitimately differ after a reconnect, which is why the composer falls back to the\n * count rather than inventing text it never received.\n */\n | { kind: \"queue\"; sessionId: string; steering: string[]; followUp: string[] }\n /**\n * What `clearQueue` removed, so the composer can give it back (issue #3).\n *\n * Deliberately NOT the `queue` event with the cleared contents: Pi empties its queue as part of\n * clearing it, so the emptied `queue` arrives first and these texts would overwrite it \u2014 the\n * queue would then appear to still hold the messages just removed. The cleared texts belong to\n * the composer's draft, which is a different place, so they are a different event.\n */\n | { kind: \"queueCleared\"; sessionId: string; steering: string[]; followUp: string[] }\n /**\n * A directory listing, in response to `listFiles` (issue #8).\n *\n * `path` is echoed so the client can tell WHICH request this answers after a rapid series of\n * expansions \u2014 without it, a slow listing for one directory could be rendered under another's\n * heading, which is the class of bug that shows stale content as current.\n */\n | { kind: \"files\"; sessionId: string; path: string; entries: FileEntry[]; truncated: boolean }\n /**\n * A file's contents, in response to `readFile`.\n *\n * Text, and **untrusted**: it is displayed inert, never rendered as markup (ADR-0006).\n */\n | { kind: \"file\"; sessionId: string; path: string; text: string; bytes: number }\n /**\n * A file request was refused.\n *\n * A distinct event from an empty listing, for the same reason `transcriptError` is distinct from\n * an empty transcript: \"this directory is empty\" and \"this directory could not be read\" are\n * opposite facts, and collapsing them made the UI assert something false. `reason` is a closed set\n * so the client must handle each case rather than print prose the orchestrator chose.\n *\n * `request` says WHICH question failed. Without it the client would have to guess from whichever\n * cache happened to be pending, and two requests for the same path (a listing and a read) would\n * make that guess a coin flip \u2014 with the failure then recorded against the wrong one.\n */\n | { kind: \"fileError\"; sessionId: string; path: string; request: \"list\" | \"read\"; reason: FileErrorReason; code?: string | undefined; message: string }\n | { kind: \"error\"; message: string; sessionId?: string | undefined };\n\n/** One entry in a directory listing. Metadata only: a listing never reads file contents. */\nexport interface FileEntry {\n name: string;\n kind: \"file\" | \"directory\" | \"symlink\" | \"other\";\n /**\n * For a symlink, what it points at (`stat`, following the link), or `missing` when it dangles.\n *\n * `kind` stays `symlink` \u2014 the entry *is* a link, and calling it a directory would hide that\n * following it may leave the root. This only tells the UI whether the link can be entered.\n */\n linkTarget?: \"file\" | \"directory\" | \"other\" | \"missing\" | undefined;\n /** Size in bytes, for regular files only. */\n size?: number | undefined;\n hidden: boolean;\n /** `lstat` failed: the entry exists but its type could not be established. */\n unreadable?: boolean | undefined;\n}\n\n/**\n * Why a file request was refused. Mirrors `FileFailure` in the orchestrator's `files.ts`.\n *\n * A closed union rather than a string, so the client cannot silently ignore a new reason: adding one\n * here is a compile error at every handler that switches on it.\n */\nexport type FileErrorReason =\n | \"absolute-path\"\n | \"escapes-cwd\"\n /** Not a usable path: a NUL byte, or a non-string. Malformed input, not a boundary violation. */\n | \"invalid-path\"\n | \"not-a-directory\"\n | \"is-directory\"\n | \"too-large\"\n | \"binary\"\n | \"not-found\"\n | \"permission\"\n | \"unreadable\";\n\n/**\n * The wire version, checked by BOTH sides on `hello`.\n *\n * **Bump this whenever a Control or Event kind is added, removed, or renamed.** That is the whole\n * point of the handshake: adding a kind is a protocol change, and it is exactly the case where\n * nobody remembers to bump, because nothing local fails \u2014 the author's UI and orchestrator are the\n * same commit. The failure appears later, on someone else's machine, as an unrelated-looking\n * `unknown kind` from a connection that reported itself compatible.\n *\n * That is not hypothetical: issue #8 added five kinds without a bump, and the symptom was\n * `bad control: unknown kind 'listFiles'` against a v1 orchestrator that had said `hello`.\n *\n * Enforced by `test/wire-surface.test.ts`, which pins the kind set against this number in both\n * directions \u2014 so the surface and the version cannot move independently.\n *\n * 1 \u2192 2: `listFiles`, `readFile`, `files`, `file`, `fileError` (issue #8).\n */\nexport const PROTOCOL_VERSION = 2 as const;\n\nexport interface Hello {\n kind: \"hello\";\n protocol: typeof PROTOCOL_VERSION;\n}\n\n/**\n * What an `orchestrator\u2192client` envelope may carry.\n *\n * `hello` is a member even though it is not an `Event`: it is a preamble rather than state,\n * but it must travel in the same envelope so a client parses exactly one shape. Sending it\n * unwrapped made the client's reader handle two shapes, which is how the transport's first\n * tests failed.\n */\nexport type ServerPayload = Event | Hello;\n\n/**\n * Envelope for anything crossing the socket. `dir` makes the trust direction explicit and\n * machine-checkable rather than implied by message shape.\n */\nexport type Envelope =\n | { dir: \"client\u2192orchestrator\"; protocol: typeof PROTOCOL_VERSION; payload: Control }\n | { dir: \"orchestrator\u2192client\"; protocol: typeof PROTOCOL_VERSION; payload: ServerPayload };\n", "/**\n * The hazard of pointing a session at the checkout Pi Central is running from (issue #25).\n *\n * ## Why this is worth naming\n *\n * Three failures follow from it, and none of them looks like the actual cause \u2014 see\n * `docs/DOGFOODING.md` for the full account:\n *\n * 1. Vite hot-reloads the UI you are using into a mid-edit error overlay.\n * 2. A branch switch desynchronises the UI and the orchestrator.\n * 3. If the agent breaks the orchestrator's source, the next restart loads it, and Pi Central\n * becomes unavailable exactly when it is needed to supervise the repair.\n *\n * Without a warning, a user sees a dashboard that mysteriously stops working and has no reason to\n * connect that to the directory they chose. The warning names the consequences, not the condition.\n *\n * ## Why the predicate lives in the PROTOCOL package\n *\n * It is needed in **two** places and must give the same answer in both: the directory sheet warns\n * *before* spawning (in the browser) and the supervisor attaches the warning to the session so it\n * persists on the rail afterwards (in the orchestrator). Two implementations would be two chances to\n * disagree, and the disagreement would be invisible \u2014 one surface warning, the other not.\n *\n * So it lives in the package both sides already share, rather than in the orchestrator with the UI\n * importing across packages. It is dependency-free and pure, which is what keeps it safe to put in\n * the protocol: nothing here reads a file, a clock, or the environment, so it adds no runtime\n * behaviour to the client bundle beyond a few hundred bytes of string comparison.\n *\n * It is also **pure**: two paths in, a boolean out. No filesystem, no configuration, no module\n * state. That is what makes the cases testable, including the one that must be FALSE (a separate\n * clone) and would otherwise be a bug nobody notices until the recommended setup nags them.\n *\n * ## Ancestor, descendant, and the same directory\n *\n * All three are hazards, and for different reasons:\n *\n * - **The same directory**: the obvious case.\n * - **An ancestor** (e.g. `~/git` containing the checkout): a session there is sandboxed to that\n * broader tree, so it can write the checkout's files anyway. The sandbox grants the working\n * directory, so a parent *is* a grant on the child.\n * - **A descendant** (e.g. `packages/ui`): editing one package is enough to hot-reload the UI or\n * break the orchestrator. It is less obviously a hazard than the parent, which is exactly why it\n * would be missed.\n *\n * ## What must NOT warn\n *\n * A sibling clone \u2014 `~/git/pi-central-dev` while running from `~/git/pi-central` \u2014 is the\n * recommended setup, and it is protected by the sandbox (a session cannot write a sibling at all).\n * Warning there would train the user to dismiss the warning, which is worse than not having one.\n */\n\n/** The names a Pi Central installation is recognisable by. */\nconst PIPELINE_MARKERS = [\"packages/orchestrator\", \"packages/ui\", \"packages/protocol\"] as const;\n\n/**\n * Normalise a path for comparison.\n *\n * Trailing slashes are removed and `.`/`..` segments are resolved **textually**, with no filesystem\n * access. `realpath` is deliberately not used: the predicate must be sync and pure for the sheet to\n * call it on every keystroke, and a symlink that resolves differently from its spelling (a common\n * macOS `/tmp` \u2192 `/private/tmp`) is a case where warning is the safer error \u2014 the consequence of a\n * false positive is a sentence on screen, and the consequence of a false negative is a broken\n * dashboard.\n *\n * A root path (`/`) survives as `/` rather than becoming empty, so \"is the parent of everything\"\n * does not silently become \"the empty string\".\n */\nexport function normalizePath(path: string): string {\n const parts: string[] = [];\n for (const segment of path.split(\"/\")) {\n if (segment === \"\" || segment === \".\") continue;\n if (segment === \"..\") {\n parts.pop();\n continue;\n }\n parts.push(segment);\n }\n const joined = parts.join(\"/\");\n return path.startsWith(\"/\") ? `/${joined}` : joined;\n}\n\n/**\n * Is `candidate` the same directory as `installation`, inside it, or a parent of it?\n *\n * Comparison is on whole path SEGMENTS, which is the detail that makes the sibling case correct:\n * a bare `startsWith` would call `\u2026/pi-central-dev` a descendant of `\u2026/pi-central`, and that is\n * precisely the configuration that must not warn.\n *\n * A session is sandboxed to its working directory, so an ancestor is a hazard for the same reason\n * the directory itself is: the policy grants the working directory, and a parent grant includes the\n * child.\n */\nexport function isOwnInstallation(candidate: string, installation: string): boolean {\n const a = normalizePath(candidate);\n const b = normalizePath(installation);\n if (a.length === 0 || b.length === 0) return false;\n if (a === b) return true;\n // `/a/b` is inside `/a`; `/a/bc` is not. The slash is what enforces the segment boundary \u2014 a\n // bare `startsWith` would call the sibling clone `pi-central-dev` a descendant of `pi-central`,\n // which is the one case that must NOT warn.\n return isInside(a, b) || isInside(b, a);\n}\n\n/**\n * Is `child` inside `parent` (strictly)?\n *\n * The root is special-cased, and not for tidiness: `parent + \"/\"` is `\"//\"` when the parent IS\n * `/`, which matches nothing \u2014 so the one directory that contains everything would have been the\n * only one that warns about nothing. Found by a test asserting that a session in `/` is an ancestor\n * of every checkout.\n */\nfunction isInside(child: string, parent: string): boolean {\n if (parent === \"/\") return child !== \"/\";\n return child.startsWith(`${parent}/`);\n}\n\n/**\n * Does this path look like a Pi Central checkout at all?\n *\n * Distinguished from \"is the running installation\" because the two have different consequences and\n * different copy. A `__dirname` fallback is reported as *unidentifiable* rather than as *safe* \u2014\n * \"I could not tell where I am running from\" and \"you are not pointing at me\" are different\n * statements, and collapsing them would make the warning silently stop working.\n */\nexport function looksLikePiCentral(paths: readonly string[]): boolean {\n const normalized = paths.map(normalizePath);\n return PIPELINE_MARKERS.every((marker) =>\n normalized.some((path) => path === marker || path.endsWith(`/${marker}`)),\n );\n}\n\n/**\n * The warning to show, or `undefined` when there is nothing to say.\n *\n * **The copy names the consequences**, because \"this is the installation Pi Central is running\n * from\" describes a condition the reader cannot evaluate. What they need to know is which three\n * things break, so that the sentence alone is enough to decide.\n *\n * Returns a single string (not a paragraph or an array) so the same value can go into\n * `LiveSession.warnings` \u2014 which the rail renders as one line with a tooltip \u2014 and into the sheet.\n */\nexport const OWN_INSTALLATION_WARNING =\n \"This is the checkout Pi Central is running from. Editing it can hot-reload this UI, desynchronise \" +\n \"it from the orchestrator on a branch switch, and leave Pi Central unable to restart if the \" +\n \"orchestrator's source is broken \u2014 exactly when you need it. Use a separate clone (see \" +\n \"docs/DOGFOODING.md).\";\n\n/**\n * The warning for a candidate directory, given where Pi Central is running from.\n *\n * `installation === undefined` means the path could not be determined, and the result is\n * deliberately `undefined` rather than a warning: a warning that fires on every session would be\n * noise, and the honest report for \"unknown\" is the absence of a claim rather than a guess. The\n * caller is expected to say the path was unidentifiable where that matters (the startup banner),\n * not to attach a per-session warning to it.\n */\nexport function ownInstallationWarning(\n candidate: string,\n installation: string | undefined,\n): string | undefined {\n if (installation === undefined) return undefined;\n return isOwnInstallation(candidate, installation) ? OWN_INSTALLATION_WARNING : undefined;\n}\n", "/**\n * Byte formatting, shared by both sides of the wire (issue #8).\n *\n * ## Why this is in `protocol` rather than duplicated\n *\n * Both the orchestrator and the UI must render a file size, and they must agree. The orchestrator\n * phrases a refusal \u2014 *\"big.log is 4.2 MB, above the 256.0 KB limit\"* \u2014 while the tree shows the same\n * file's size beside its name. Two implementations would let those two strings disagree, and this\n * project has already shipped that bug once: the inspector rendered `127.2M` while the metric strip\n * beside it rendered `12723914`, one value in two formats on one screen.\n *\n * It is not a wire TYPE, so it does not belong in `envelopes.ts` \u2014 but it is part of the shared\n * contract between the two processes, which is what `protocol` is for. `own-installation.ts` is here\n * for the same reason: a pure function both sides must implement identically.\n *\n * Binary units (1024), matching `formatTokens`, so a size in the tree and the size quoted in a\n * refusal cannot differ by a factor of 1000 either.\n *\n * The CAP itself is deliberately NOT here. `MAX_FILE_BYTES` lives in the orchestrator's `files.ts`,\n * next to the check that enforces it: the client must never decide what is readable, and a second\n * copy of the number would be a second thing to keep in step \u2014 the drift this module exists to\n * prevent. Adding one here that nothing used was a mistake, and it is recorded rather than quietly\n * deleted.\n */\n\n/** A file size a reader can judge: `512 B`, `2.0 KB`, `5.0 MB`. */\nexport function formatBytes(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes < 0) return \"\u2014\";\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n", "/**\n * The agent credential store: inspection, ownership, explicit re-import, and drift\n * (ADR-0010 \u00A75).\n *\n * The agent root is a single path holding everything the agent *is*: `gh/` (GH_CONFIG_DIR),\n * `gitconfig` (GIT_CONFIG_GLOBAL), `ssh/`, and optionally a keychain. It has exactly one\n * writer, chosen by ownership mode:\n *\n * - `managed` \u2014 Pi Central owns it and may write (create, rotate, export).\n * - `external` \u2014 the user owns it (e.g. the hardening project's `~/.agent`). Pi Central\n * **reads and verifies only, and never writes.**\n *\n * Two writers on one credential tree is drift, not concurrency \u2014 the hardening project hit\n * exactly that (\"the repo copy and the live copy had already drifted once\"). So ownership is\n * exclusive and explicit, never shared.\n *\n * ## Inspection never reads a secret\n *\n * Everything here is metadata: which files exist, their modes, the agent's *non-secret*\n * identity, and a fingerprint of those facts. Reading a token to \"check\" it would copy the\n * user's secret into Pi Central's process for no benefit, so `readStore` deliberately does\n * not open `gh/hosts.yml`, an SSH private key, or a keychain item. The fingerprint is built\n * from stat metadata and `gitconfig`'s non-secret keys, so drift is detectable without the\n * secret ever being read.\n */\nimport { createHash } from \"node:crypto\";\nimport { existsSync, readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nexport type Ownership = \"managed\" | \"external\";\n\nexport interface StoreFile {\n /** Path relative to the agent root, so a report is portable between machines. */\n relPath: string;\n mode: number;\n size: number;\n}\n\n/** The non-secret identity an agent acts as. */\nexport interface AgentIdentity {\n /** `user.name` from the agent's gitconfig, if set. */\n name?: string;\n /** `user.email` from the agent's gitconfig \u2014 GitHub's noreply form links commits. */\n email?: string;\n}\n\nexport interface StoreSnapshot {\n root: string;\n exists: boolean;\n ownership: Ownership;\n identity: AgentIdentity;\n /** Non-secret facts about what is present. Booleans only \u2014 never contents. */\n has: {\n gitconfig: boolean;\n ghConfig: boolean;\n sshKey: boolean;\n sshSigningKey: boolean;\n knownHosts: boolean;\n sshCommandWrapper: boolean;\n keychain: boolean;\n };\n /** Metadata for files under the root, for drift comparison. */\n files: StoreFile[];\n /**\n * A hash of the non-secret facts above.\n *\n * Drift is \"the fingerprint changed\", not \"the secret changed\" \u2014 so re-import can compare\n * states without reading a credential.\n */\n fingerprint: string;\n}\n\n/** The agent keychain's filename within an agent root (the hardening project's convention). */\nexport const KEYCHAIN_FILE = \"agent.keychain-db\";\n\n/** Pi's provider credential file, inside an agent directory. */\nexport const PROVIDER_AUTH_FILE = \"auth.json\";\n\n/**\n * Does this directory look like an agent credential tree?\n *\n * Used to adopt an existing external tree (the hardening project's `~/.agent`) instead of\n * creating a second one, so an existing setup is imported rather than duplicated\n * (ADR-0010 \u00A75). Deliberately requires a marker file, not merely an existing directory: a\n * stray empty `~/.agent` must not silently become the credential root.\n */\nexport function looksLikeAgentTree(root: string): boolean {\n if (!existsSync(root)) return false;\n return (\n existsSync(join(root, KEYCHAIN_FILE)) ||\n existsSync(join(root, \"gitconfig\")) ||\n existsSync(join(root, \"gh\")) ||\n existsSync(join(root, \"ssh\"))\n );\n}\n\n/**\n * What Pi's provider configuration implies about keychain access (C13).\n *\n * `auth.json` maps a provider to either a literal key or a **command** that resolves one:\n *\n * \"litellm\": { \"key\": \"!security find-generic-password -ws 'litellm'\" }\n *\n * A command-based key is looked up lazily on the first call that needs it, so Pi cannot know\n * at startup whether it will succeed. That is why the *policy* has to reason about it up\n * front: without keychain access the session starts, reports idle, and then cannot think.\n */\nexport interface ProviderAuth {\n /** The file inspected. */\n path: string;\n readable: boolean;\n /** Providers named in the file. Names are configuration, not secrets. */\n providers: string[];\n /** Providers whose key is resolved by running a command rather than stored inline. */\n commandProviders: string[];\n /** True when some command reaches the macOS keychain and therefore needs a policy grant. */\n needsKeychain: boolean;\n}\n\n/**\n * Inspect a provider credential file for its *shape* only.\n *\n * Never returns a key. A file may hold literal keys for some users, so the value is examined\n * only to classify it (literal vs command) and is then discarded. Nothing here logs, returns,\n * or fingerprints a secret.\n */\nexport function inspectProviderAuth(agentDir: string): ProviderAuth {\n const path = join(agentDir, PROVIDER_AUTH_FILE);\n const result: ProviderAuth = {\n path,\n readable: false,\n providers: [],\n commandProviders: [],\n needsKeychain: false,\n };\n\n let text: string;\n try {\n text = readFileSync(path, \"utf8\");\n } catch {\n // Absent (ENOENT) is normal and Pi tolerates it. A denied read (EPERM) is NOT tolerated:\n // Pi exits 1 (C12), which is why this file must never be denied.\n return result;\n }\n result.readable = true;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return result;\n }\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) return result;\n\n for (const [provider, value] of Object.entries(parsed as Record<string, unknown>)) {\n result.providers.push(provider);\n if (typeof value !== \"object\" || value === null) continue;\n const key = (value as { key?: unknown }).key;\n if (typeof key !== \"string\" || !key.startsWith(\"!\")) continue;\n result.commandProviders.push(provider);\n // `security` is the macOS keychain CLI. Other commands (a vault CLI, say) do not need a\n // keychain grant, so the check is specific rather than \"any command\".\n if (/\\bsecurity\\b/.test(key)) result.needsKeychain = true;\n }\n\n return result;\n}\n\n/** Files whose *contents* are never read, only their presence and metadata. */\nconst SECRET_BEARING = new Set([\"gh/hosts.yml\", \"ssh/id_ed25519\", \"ssh/id_rsa\", KEYCHAIN_FILE]);\n\nexport interface ReadStoreOptions {\n ownership?: Ownership;\n}\n\n/**\n * Inspect an agent root without reading any secret.\n *\n * Returns a snapshot even when the root does not exist, so a caller can render \"not\n * configured yet\" rather than handling a special case.\n */\nexport function readStore(root: string, options: ReadStoreOptions = {}): StoreSnapshot {\n const ownership = options.ownership ?? \"managed\";\n const exists = existsSync(root);\n\n const gitconfigPath = join(root, \"gitconfig\");\n const identity = exists && existsSync(gitconfigPath) ? readIdentity(gitconfigPath) : {};\n\n const has = {\n gitconfig: existsSync(gitconfigPath),\n ghConfig: existsSync(join(root, \"gh\")),\n sshKey: existsSync(join(root, \"ssh\", \"github_login_ed25519\")) || existsSync(join(root, \"ssh\", \"id_ed25519\")),\n sshSigningKey: existsSync(join(root, \"ssh\", \"allowed_signers\")),\n knownHosts: existsSync(join(root, \"ssh\", \"known_hosts\")),\n sshCommandWrapper: existsSync(join(root, \"ssh\", \"ssh-github.sh\")),\n keychain: existsSync(join(root, KEYCHAIN_FILE)),\n };\n\n const files = exists ? listFiles(root) : [];\n const snapshot: StoreSnapshot = { root, exists, ownership, identity, has, files, fingerprint: \"\" };\n snapshot.fingerprint = fingerprintOf(snapshot);\n return snapshot;\n}\n\n/**\n * A stable hash over the *non-secret* state of a store.\n *\n * `size` is included but contents are not: it catches a rotated token (which changes the file\n * length) while still never reading the value. A same-length rotation is not detected \u2014 that\n * is a deliberate limit, and the reason re-import is explicit rather than trusted to be exact.\n */\nfunction fingerprintOf(snapshot: StoreSnapshot): string {\n const facts = {\n exists: snapshot.exists,\n identity: snapshot.identity,\n has: snapshot.has,\n files: snapshot.files.map((f) => [f.relPath, f.mode, f.size]),\n };\n return createHash(\"sha256\").update(JSON.stringify(facts)).digest(\"hex\").slice(0, 16);\n}\n\n/** List files under the root as metadata only. One level of nesting is enough here. */\nfunction listFiles(root: string): StoreFile[] {\n const found: StoreFile[] = [];\n const walk = (dir: string, rel: string, depth: number): void => {\n if (depth > 2) return;\n let entries: import(\"node:fs\").Dirent[];\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n const relPath = rel === \"\" ? entry.name : `${rel}/${entry.name}`;\n const full = join(dir, entry.name);\n try {\n const stats = statSync(full);\n if (stats.isDirectory()) {\n walk(full, relPath, depth + 1);\n } else {\n found.push({ relPath, mode: stats.mode & 0o777, size: stats.size });\n }\n } catch {\n // Unreadable entries are skipped: the report is advisory, and refusing here would\n // make an unreadable file indistinguishable from a missing one.\n }\n }\n };\n walk(root, \"\", 0);\n return found.sort((a, b) => a.relPath.localeCompare(b.relPath));\n}\n\n/**\n * Read the agent's non-secret identity from its gitconfig.\n *\n * Only `user.name` and `user.email`. A credential helper line is deliberately ignored: it may\n * embed a token, and the point of this module is not to read credentials.\n */\nfunction readIdentity(gitconfigPath: string): AgentIdentity {\n const identity: AgentIdentity = {};\n let text: string;\n try {\n text = readFileSync(gitconfigPath, \"utf8\");\n } catch {\n return identity;\n }\n // Read the *user* section only, so no other section's values are picked up.\n let inUser = false;\n for (const raw of text.split(\"\\n\")) {\n const line = raw.trim();\n if (line.startsWith(\"[\")) {\n inUser = /^\\[user\\]/i.test(line);\n continue;\n }\n if (!inUser) continue;\n const match = /^(name|email)\\s*=\\s*(.+)$/i.exec(line);\n if (!match) continue;\n const value = match[2]!.trim();\n if (value === \"\") continue;\n if (match[1]!.toLowerCase() === \"name\") identity.name = value;\n else identity.email = value;\n }\n return identity;\n}\n\nexport interface DriftEntry {\n relPath: string;\n kind: \"added\" | \"removed\" | \"changed\";\n detail: string;\n}\n\nexport interface DriftReport {\n drifted: boolean;\n entries: DriftEntry[];\n /** Fingerprint comparison, for the badge's tooltip. */\n from: string;\n to: string;\n}\n\n/**\n * Compare an imported snapshot against the current state of the same tree.\n *\n * This is what powers the drift badge: an `external` tree can change under Pi Central at any\n * time (the user owns it), so the difference is surfaced rather than silently picked up. It is\n * computed on demand \u2014 never on the launch path \u2014 because re-import is explicit (ADR-0010 \u00A75).\n */\nexport function detectDrift(imported: StoreSnapshot, current: StoreSnapshot): DriftReport {\n const entries: DriftEntry[] = [];\n const before = new Map(imported.files.map((f) => [f.relPath, f]));\n const after = new Map(current.files.map((f) => [f.relPath, f]));\n\n for (const [relPath, file] of after) {\n const previous = before.get(relPath);\n if (!previous) {\n entries.push({ relPath, kind: \"added\", detail: `new file (${file.size} bytes)` });\n } else if (previous.size !== file.size || previous.mode !== file.mode) {\n // Described without values: a size change is enough to flag a rotation, and naming the\n // content would defeat the purpose of never reading it.\n entries.push({\n relPath,\n kind: \"changed\",\n detail:\n previous.mode !== file.mode\n ? `mode ${octal(previous.mode)} -> ${octal(file.mode)}`\n : `size ${previous.size} -> ${file.size}`,\n });\n }\n }\n for (const [relPath] of before) {\n if (!after.has(relPath)) entries.push({ relPath, kind: \"removed\", detail: \"no longer present\" });\n }\n\n if (imported.identity.name !== current.identity.name || imported.identity.email !== current.identity.email) {\n entries.push({\n relPath: \"gitconfig\",\n kind: \"changed\",\n detail: `identity ${describeIdentity(imported.identity)} -> ${describeIdentity(current.identity)}`,\n });\n }\n\n entries.sort((a, b) => a.relPath.localeCompare(b.relPath));\n return {\n drifted: imported.fingerprint !== current.fingerprint,\n entries,\n from: imported.fingerprint,\n to: current.fingerprint,\n };\n}\n\nfunction describeIdentity(identity: AgentIdentity): string {\n if (identity.name === undefined && identity.email === undefined) return \"(none)\";\n return `${identity.name ?? \"?\"} <${identity.email ?? \"?\"}>`;\n}\n\nfunction octal(mode: number): string {\n return `0${mode.toString(8)}`;\n}\n\nexport interface ImportOptions {\n /** The tree to adopt. */\n root: string;\n ownership: Ownership;\n}\n\nexport interface ImportPlan {\n ok: boolean;\n ownership: Ownership;\n snapshot: StoreSnapshot;\n /** What is missing for the agent to be able to act at all. */\n missing: string[];\n /** Human-readable notes, e.g. what `external` ownership means for writes. */\n notes: string[];\n}\n\n/**\n * Plan an import: inspect the tree and report what would be adopted.\n *\n * Writing is **not** part of this. For `external` ownership Pi Central never writes at all;\n * for `managed` the caller creates missing pieces. Separating the plan from the write is what\n * makes re-import safe to run repeatedly.\n */\nexport function planImport(options: ImportOptions): ImportPlan {\n const snapshot = readStore(options.root, { ownership: options.ownership });\n const missing: string[] = [];\n const notes: string[] = [];\n\n if (!snapshot.exists) {\n missing.push(`agent root does not exist: ${options.root}`);\n } else {\n if (!snapshot.has.gitconfig) missing.push(\"gitconfig (needed for GIT_CONFIG_GLOBAL and the agent's commit identity)\");\n if (!snapshot.has.ghConfig) missing.push(\"gh/ (needed for GH_CONFIG_DIR; supplies the agent's GitHub token)\");\n if (!snapshot.has.sshKey) {\n notes.push(\n \"no SSH key found: the agent can still push over HTTPS with its gh token, but not \" +\n \"over SSH, and commit signing needs a key\",\n );\n }\n if (snapshot.identity.email === undefined) {\n notes.push(\"no user.email in gitconfig: commits would not be attributed to the agent account\");\n }\n }\n\n if (options.ownership === \"external\") {\n notes.push(\n \"external ownership: Pi Central reads and verifies this tree but never writes to it, \" +\n \"so the tree can be shared with an interactive setup. Re-import is explicit.\",\n );\n } else {\n notes.push(\"managed ownership: Pi Central owns this tree and may create or rotate files in it.\");\n }\n\n return { ok: missing.length === 0, ownership: options.ownership, snapshot, missing, notes };\n}\n\n/** True when a path holds secret contents and must never be read by inspection code. */\nexport function isSecretBearing(relPath: string): boolean {\n return SECRET_BEARING.has(relPath);\n}\n", "/**\n * Session supervisor: the state machine and lifecycle.\n *\n * Two rules that the rest of the product depends on:\n *\n * 1. **Completion is gated on `agent_settled`, never `agent_end`** (C8). Retries,\n * compaction, steering and follow-ups can all follow `agent_end`; treating it as \"done\"\n * would let a session be marked idle while it is still working, and would tear down or\n * re-prompt a process mid-run.\n *\n * 2. **At most one live process per `(sessionFile, cwd)`** (ADR-0004) \u2014 among the sessions\n * THIS instance spawned. Two processes appending to one session file add a branch point\n * to its history, and Pi takes no file lock, so a `pi` started by hand in a terminal is\n * undetectable (C9). That case is a stated limitation, not a solved problem. A second\n * resume for a session this instance already supervises returns the existing one rather\n * than spawning.\n *\n * The supervisor is also the only source of truth for \"running\" (C3): Pi has no daemon and\n * no attach protocol, so a session is live iff this process spawned it.\n */\nimport { EventEmitter } from \"node:events\";\nimport { existsSync, mkdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Command, HistoricalSession, LiveSession, ModelOption, PiRecord } from \"@pi-central/protocol\";\nimport { composePolicy, describe, type LaunchMode, type SessionSpec } from \"./policy.ts\";\nimport { RpcProcess, type LaunchSpec } from \"./rpc.ts\";\nimport { checkResumable } from \"./resume.ts\";\nimport { isScratch, scratchDirFor, type Config } from \"./config.ts\";\nimport { agentRoutingEnv } from \"./env.ts\";\nimport { KEYCHAIN_FILE, inspectProviderAuth, readStore } from \"./agent-store.ts\";\nimport { assessAgentCapabilities } from \"./capabilities.ts\";\nimport { renderPolicyFragment } from \"./fragment.ts\";\nimport {\n listDirectory,\n readFileText,\n type DirListing,\n type FileContent,\n type Result,\n} from \"./files.ts\";\nimport { resolveInstallationRoot } from \"./installation-root.ts\";\nimport { ownInstallationWarning } from \"@pi-central/protocol\";\nimport { interpretRecord } from \"./state.ts\";\n\nexport interface StartRequest {\n /** Directory to work in. Omitted means \"the app-owned default workspace\" (US-1, US-3). */\n cwd?: string;\n name?: string;\n mode?: LaunchMode;\n enable?: string[];\n /**\n * Extra environment values for this session \u2014 the trusted channel for the agent's own\n * credentials (ADR-0010). Never sourced from `process.env` by the caller.\n */\n env?: Record<string, string>;\n}\n\nexport interface SupervisorEvents {\n update: (session: LiveSession) => void;\n record: (sessionId: string, record: PiRecord) => void;\n error: (sessionId: string, message: string) => void;\n}\n\nexport interface StartResult {\n ok: true;\n session: LiveSession;\n}\nexport interface StartFailure {\n ok: false;\n message: string;\n}\n\nexport class Supervisor extends EventEmitter {\n #config: Config;\n #sessions = new Map<string, LiveSession>();\n #processes = new Map<string, RpcProcess>();\n #created = 0;\n\n constructor(config: Config) {\n super();\n this.#config = config;\n // Resolved from THIS module's URL rather than configured: a configured path would be right for\n // exactly one checkout \u2014 the author's \u2014 and silently wrong everywhere else (issue #25).\n this.#installationRoot = resolveInstallationRoot();\n }\n\n list(): LiveSession[] {\n return [...this.#sessions.values()];\n }\n\n get(id: string): LiveSession | undefined {\n return this.#sessions.get(id);\n }\n\n /**\n * The underlying RPC process, for verification and diagnostics.\n *\n * Exposed so a test can issue a probe through the *model-facing* `bash` tool inside a\n * session (verify-three.ts). Normal product code goes through the typed lifecycle methods\n * above; this is the escape hatch that keeps the harness honest about what it exercises.\n */\n rpcProcess(id: string): RpcProcess | undefined {\n return this.#processes.get(id);\n }\n\n /**\n * Start a new session. When `cwd` is omitted this creates and uses the app-owned default\n * workspace for the new session's id (US-1 with US-3), rather than defaulting to `$HOME`\n * or the orchestrator's own directory.\n */\n start(request: StartRequest = {}): StartResult | StartFailure {\n const mode = request.mode ?? \"sandboxed\";\n\n // The id is allocated before the cwd, because a scratch workdir is derived from it.\n const id = `s${++this.#created}`;\n\n let cwd: string;\n if (request.cwd) {\n cwd = request.cwd;\n } else {\n try {\n cwd = scratchDirFor(this.#config, id);\n // Create it eagerly: composePolicy validates the directory exists, and a scratch\n // session should find an empty workspace waiting rather than a missing path.\n mkdirSync(cwd, { recursive: true });\n } catch (error) {\n return { ok: false, message: (error as Error).message };\n }\n }\n\n return this.#spawn({ id, cwd, mode, name: request.name, enable: request.enable, env: request.env });\n }\n\n /**\n * Resume a historical session in its own recorded directory (US-5).\n *\n * The cwd is validated here, before spawning, because RPC-mode Pi exits `1` on a missing\n * directory rather than prompting (C2). The session's directory is never changed\n * (ADR-0008); an unusable directory is refused, not retargeted.\n */\n resume(\n session: HistoricalSession,\n options: { mode?: LaunchMode; env?: Record<string, string> } = {},\n ): StartResult | StartFailure {\n const readiness = checkResumable(session);\n if (!readiness.ok) return { ok: false, message: readiness.message };\n\n const live = this.#findLiveByPath(session.path);\n if (live) return { ok: true, session: live }; // one process per session file (ADR-0004)\n\n const id = `s${++this.#created}`;\n return this.#spawn({\n id,\n cwd: readiness.cwd,\n mode: options.mode ?? \"sandboxed\",\n // No name: the session already carries its own, and re-applying one would append a\n // redundant session_info entry to a file we did not create.\n ...(options.env ? { env: options.env } : {}),\n sessionPath: session.path,\n });\n }\n\n #findLiveByPath(sessionPath: string): LiveSession | undefined {\n for (const session of this.#sessions.values()) {\n if (session.sessionFile === sessionPath && session.state !== \"stopped\" && session.state !== \"failed\") {\n return session;\n }\n }\n return undefined;\n }\n\n #spawn(input: {\n id: string;\n cwd: string;\n mode: LaunchMode;\n name?: string | undefined;\n enable?: string[] | undefined;\n env?: Record<string, string> | undefined;\n sessionPath?: string | undefined;\n }): StartResult | StartFailure {\n // A resumed session must reopen its file; a new one gets a fresh name.\n //\n // The environment carries the agent-scoped routing (ADR-0010 \u00A73): GH_CONFIG_DIR and\n // GIT_CONFIG_GLOBAL point into the agent's own tree, and GIT_CONFIG_SYSTEM=/dev/null\n // removes the host's system git config as an independent route to the login keychain.\n const env = { ...agentRoutingEnv(this.#config.agentRoot), ...(input.env ?? {}) };\n\n // Provider-key resolution needs keychain access (C13), and the grant is derived from the\n // provider configuration that is ACTUALLY in force rather than from an assumption.\n //\n // `auth.json` resolves each key with a command such as\n // !security find-generic-password -ws 'litellm'\n // which needs `SecurityServer` to read any keychain, and the user-wide search list to find\n // the agent keychain. A command-based key is resolved lazily, so Pi cannot report the\n // failure at startup: without this grant the session starts, reports idle, and then cannot\n // think. That is the exact C13 failure mode.\n const providerAuth = inspectProviderAuth(this.#config.providerAgentDir);\n const keychainPath = join(this.#config.agentRoot, KEYCHAIN_FILE);\n const keychainNeeded = providerAuth.needsKeychain;\n const keychainPresent = existsSync(keychainPath);\n\n // Only sandboxed sessions get a fragment: direct mode applies no Safehouse policy, so\n // there is nothing to append to.\n //\n // The agent root is passed unconditionally (not just when managed): the session is\n // configured with GH_CONFIG_DIR/GIT_CONFIG_GLOBAL inside it, so it must be able to read\n // its own credentials whether Pi Central created the tree or adopted an external one.\n const fragment =\n input.mode === \"sandboxed\"\n ? renderPolicyFragment({\n agentRoot: this.#config.agentRoot,\n ...(keychainNeeded ? { keychain: true, keychainPath } : {}),\n })\n : undefined;\n\n const spec: SessionSpec = {\n id: input.id,\n cwd: input.cwd,\n mode: input.mode,\n safehouse: this.#config.safehouse,\n pi: this.#config.pi,\n env,\n ...(fragment !== undefined\n ? { policyFragment: fragment, fragmentDir: this.#config.fragmentDir }\n : {}),\n ...(input.enable ? { enable: input.enable } : {}),\n ...(input.name ? { name: input.name } : {}),\n };\n\n let policy;\n try {\n policy = composePolicy(spec);\n } catch (error) {\n return { ok: false, message: (error as Error).message };\n }\n\n // Can the session USE the credentials it is configured with? (issue #28)\n //\n // Checked here rather than earlier because the honest answer depends on the RENDERED fragment:\n // the C15 defect was a fragment that did not grant what GH_CONFIG_DIR pointed at, and the\n // fragment is the artifact that actually gets applied. Assessment never reads a credential --\n // `readStore` is metadata-only -- and never blocks the session: a session with no identity is\n // still useful for non-credential work.\n const store = readStore(this.#config.agentRoot, {\n ownership: this.#config.agentRootManaged ? \"managed\" : \"external\",\n });\n const capabilities = assessAgentCapabilities({\n store,\n sandboxed: input.mode === \"sandboxed\",\n ...(fragment !== undefined ? { fragment } : {}),\n providerAuth,\n keychainPath,\n });\n\n const session: LiveSession = {\n id: input.id,\n name: input.name?.trim() || defaultName(input.cwd, input.id),\n cwd: input.cwd,\n state: \"starting\",\n mode: input.mode,\n isolation: policy.isolation,\n command: describe(policy),\n scratch: isScratch(this.#config, input.cwd),\n messageCount: 0,\n pending: 0,\n contextPercent: null,\n tokensTotal: null,\n cost: null,\n dialogs: [],\n // Policy warnings, credential warnings, and the self-hosting hazard (issues #28, #25).\n // A session that starts and cannot think, or cannot use its own identity, is one failure this\n // surfaces (C13, C15); a session pointed at the checkout Pi Central is running from is\n // another, and it is warned about rather than refused \u2014 the user may have a good reason, and\n // the consequences are what they need in order to judge (issue #25).\n warnings: [\n ...policy.warnings,\n ...capabilities.warnings,\n ...(ownInstallationWarning(input.cwd, this.#installationRoot) !== undefined\n ? [ownInstallationWarning(input.cwd, this.#installationRoot)!]\n : []),\n ],\n startedAt: Date.now(),\n ...(input.sessionPath ? { sessionFile: input.sessionPath } : {}),\n };\n this.#sessions.set(session.id, session);\n\n const rpc = new RpcProcess(this.#buildLaunch(policy.launch, input.sessionPath));\n this.#processes.set(session.id, rpc);\n\n rpc.on(\"record\", (record: PiRecord) => this.#onRecord(session.id, record));\n rpc.on(\"failed\", (error: Error) => {\n session.state = \"failed\";\n session.error = error.message;\n this.#patch(session.id);\n this.emit(\"error\", session.id, error.message);\n });\n rpc.on(\"exit\", ({ code, signal, stderr }: { code: number | null; signal: string | null; stderr: string }) => {\n if (session.state === \"stopped\") return;\n session.state = \"failed\";\n session.error = describeExitFailure(code, signal, stderr);\n session.dialogs = [];\n this.#patch(session.id);\n this.emit(\"error\", session.id, session.error);\n });\n\n rpc.start();\n this.#patch(session.id);\n void this.#refreshState(session.id);\n return { ok: true, session };\n }\n\n /**\n * A resumed session reopens its file (`--session <path>`); a new session is left to Pi's\n * default naming in its workspace.\n *\n * `--session` is appended after the `--` separator, so it is a *pi* argument, not a\n * safehouse one. The only value interpolated is a path validated to exist by\n * `checkResumable`; the argument shape is fixed.\n */\n #buildLaunch(launch: LaunchSpec, sessionPath?: string): LaunchSpec {\n if (!sessionPath) return launch;\n return { ...launch, args: [...launch.args, \"--session\", sessionPath] };\n }\n\n async #refreshState(id: string): Promise<void> {\n const session = this.#sessions.get(id);\n const rpc = this.#processes.get(id);\n if (!session || !rpc) return;\n try {\n const state = (await rpc.send({ type: \"get_state\" }, 20_000)) as Record<string, unknown>;\n const model = state.model as { id?: string; provider?: string } | undefined;\n session.model = model?.id;\n session.provider = model?.provider;\n session.thinkingLevel = state.thinkingLevel as string | undefined;\n if (typeof state.sessionFile === \"string\") session.sessionFile = state.sessionFile;\n // Pi's OWN session id, which is a uuid. Distinct from `session.id`, which is Pi Central's\n // short handle. Both are needed: the handle addresses the process, the uuid identifies the\n // transcript, and showing one under the other's label is a lie about which is which.\n if (typeof state.sessionId === \"string\" && state.sessionId) session.sessionId = state.sessionId;\n if (typeof state.sessionName === \"string\" && state.sessionName) session.name = state.sessionName;\n session.messageCount = Number(state.messageCount ?? session.messageCount);\n session.pending = Number(state.pendingMessageCount ?? session.pending);\n // Only leave `starting` if the process is genuinely up and idle or streaming.\n if (session.state === \"starting\") session.state = state.isStreaming ? \"streaming\" : \"idle\";\n session.error = undefined;\n this.#patch(id);\n void this.#refreshStats(id);\n // Load the existing conversation BEFORE anything new arrives.\n void this.#loadTranscript(id);\n } catch (error) {\n session.state = \"failed\";\n session.error = `${(error as Error).message}${rpc.stderrTail ? `\\n${rpc.stderrTail}` : \"\"}`;\n this.#patch(id);\n }\n }\n\n /**\n * Fetch a session's conversation from Pi.\n *\n * **Authoritative and current, and nothing is cached.** That is the whole design, and it is a\n * correction: an earlier version fetched once at session start, cached the result, and replayed\n * the cache to every client that connected later. For a session started fresh that cache held\n * the empty conversation from its first instant, so a reconnect REPLACED a correct transcript\n * with an empty one \u2014 and a one-shot guard meant it could never recover. The UI then showed\n * \"Loading this session's history\u2026\" indefinitely, because Pi reported messages that the client\n * would never receive.\n *\n * A cache of a conversation is stale the moment it is taken. Re-fetching costs one sub-\n * millisecond round trip per session per connection, which is a price worth paying for being\n * right.\n *\n * Returns a result rather than throwing, because callers must distinguish \"the conversation is\n * empty\" from \"the conversation could not be read\" \u2014 the first is fine and the second must not\n * be presented as an eternal spinner.\n */\n async fetchTranscript(\n id: string,\n ): Promise<{ ok: true; messages: unknown[] } | { ok: false; message: string }> {\n const rpc = this.#processes.get(id);\n if (!rpc) return { ok: false, message: \"this session is not running\" };\n try {\n const data = (await rpc.send({ type: \"get_messages\" }, 30_000)) as { messages?: unknown[] };\n return {\n ok: true,\n messages: (data?.messages ?? []).filter(\n (message) => typeof message === \"object\" && message !== null,\n ),\n };\n } catch (error) {\n return { ok: false, message: (error as Error).message };\n }\n }\n\n /**\n * Load a session's conversation once, when it starts or resumes.\n *\n * **This is the fix for a resumed session showing an empty transcript.** Pi loads a resumed\n * session's history into the model's context but does NOT re-emit it as events, so a UI built\n * only from `record` events shows \"No messages yet\" for a session with hundreds of messages \u2014\n * which is precisely the case US-5 exists to serve.\n *\n * Re-emitted as `message_end` records rather than through a new event type, so history and a\n * live stream take the SAME code path in the UI reducer. There is one renderer for one\n * conversation; two is how the past and the present end up looking different.\n *\n * The once-only guard covers this automatic path to avoid duplicating the conversation on every\n * state refresh. Clients that connect later call `fetchTranscript` directly instead, so they\n * always get the CURRENT conversation rather than this session's opening state.\n */\n async #loadTranscript(id: string): Promise<void> {\n const session = this.#sessions.get(id);\n if (!session || session.transcriptLoaded) return;\n\n const result = await this.fetchTranscript(id);\n if (!result.ok) {\n // Deliberately NOT marking the load as done: a transient failure (Pi not ready yet) should\n // be retryable rather than permanently disabling this session's history.\n session.warnings = [\n ...session.warnings,\n `Could not load this session's earlier messages: ${result.message}`,\n ];\n this.emit(\"transcriptError\", id, result.message);\n this.#patch(id);\n return;\n }\n\n session.transcriptLoaded = true;\n this.emit(\"transcript\", id, result.messages);\n }\n\n async #refreshStats(id: string): Promise<void> {\n const session = this.#sessions.get(id);\n const rpc = this.#processes.get(id);\n if (!session || !rpc || !rpc.running) return;\n try {\n const stats = (await rpc.send({ type: \"get_session_stats\" }, 20_000)) as Record<string, unknown>;\n const ctx = stats.contextUsage as { percent?: number | null } | undefined;\n const tokens = stats.tokens as { total?: number } | undefined;\n session.contextPercent = ctx?.percent ?? null;\n session.tokensTotal = tokens?.total ?? null;\n session.cost = typeof stats.cost === \"number\" ? stats.cost : null;\n this.#patch(id);\n } catch {\n // Stats are cosmetic. Never fail a session over them.\n }\n }\n\n #onRecord(id: string, record: PiRecord): void {\n const session = this.#sessions.get(id);\n if (!session) return;\n\n // The decision is pure (state.ts) so it is testable without a spawned process; the\n // supervisor only applies it.\n const effect = interpretRecord(session, record);\n if (effect.dialog) session.dialogs.push(effect.dialog);\n if (effect.patch) Object.assign(session, effect.patch);\n if (effect.refreshStats) void this.#refreshStats(id);\n\n this.emit(\"record\", id, record);\n // The queued TEXTS come from the record, not from `LiveSession.pending` (which is only a\n // count). Pi sends the whole queue on every `queue_update`, so this is a full replacement\n // rather than a reconstruction \u2014 see the `queue` event's comment.\n if (record.type === \"queue_update\") {\n this.emit(\n \"queue\",\n id,\n asTextArray(record.steering),\n asTextArray(record.followUp),\n );\n }\n this.#patch(id);\n }\n\n answerDialog(\n id: string,\n dialogId: string,\n answer: { value: string } | { confirmed: boolean } | { cancelled: true },\n ): void {\n const session = this.#sessions.get(id);\n const rpc = this.#processes.get(id);\n if (!session || !rpc) return;\n\n const payload: Record<string, unknown> = { type: \"extension_ui_response\", id: dialogId };\n if (\"cancelled\" in answer) payload.cancelled = true;\n else if (\"confirmed\" in answer) payload.confirmed = answer.confirmed;\n else payload.value = answer.value;\n\n rpc.write(payload);\n session.dialogs = session.dialogs.filter((dialog) => dialog.id !== dialogId);\n if (session.dialogs.length === 0 && session.state === \"awaiting-approval\") session.state = \"idle\";\n this.#patch(id);\n }\n\n /**\n * Send a prompt. The command shape is closed; the message is the only free value.\n *\n * `behavior` decides what a message means mid-run, and it is the caller's choice rather than\n * this method's: Pi REJECTS a streaming `prompt` with no `streamingBehavior` (`\"Agent is\n * already processing. Specify streamingBehavior ('steer' or 'followUp')\"`), so defaulting it\n * silently would decide for the user something the composer is there to ask (issue #3). When\n * idle there is nothing to steer into, so the flag is dropped rather than sent.\n */\n async prompt(id: string, message: string, behavior?: \"steer\" | \"followUp\"): Promise<void> {\n const rpc = this.#processes.get(id);\n const session = this.#sessions.get(id);\n if (!rpc || !session) throw new Error(`no such session: ${id}`);\n if (session.state === \"streaming\") {\n await rpc.send({ type: \"prompt\", message, streamingBehavior: behavior ?? \"followUp\" }, 60_000);\n } else {\n await rpc.send({ type: \"prompt\", message }, 60_000);\n }\n }\n\n /**\n * Empty Pi's queue, and optionally stop the run, in that order.\n *\n * **The order is the whole point.** Pi's `clear_queue` returns the queued texts, so clearing\n * first is what makes them recoverable: abort-then-clear would let the run settle and, in the\n * window before the queue was emptied, Pi could begin delivering a queued message as a real\n * turn. Clearing first removes the queue before anything else can act on it, and the returned\n * texts are broadcast so the composer restores exactly what was removed instead of guessing.\n *\n * `abort` is sent directly rather than via `this.abort` so the pair cannot be reordered by a\n * later refactor.\n *\n * Returns the cleared texts, so a caller that wants them does not have to wait for the event.\n */\n async clearQueue(id: string, andStop: boolean): Promise<{ steering: string[]; followUp: string[] }> {\n const rpc = this.#processes.get(id);\n if (!rpc) throw new Error(`no such session: ${id}`);\n const result = await clearQueueVia((command, timeout) => rpc.send(command, timeout), andStop);\n // A SEPARATE event from `queue`, and deliberately so. Pi emits an emptied `queue_update`\n // during `clearQueue` (agent-session.js: `_emitQueueUpdate()`), which already tells the\n // client the queue is now empty. Reporting these texts as the queue would therefore\n // overwrite that empty list with the very messages that were just removed \u2014 the queue would\n // appear to still hold them. These texts belong to the composer, not to the queue.\n //\n // Emitted even when both lists are empty, so \"nothing was queued\" is reported rather than\n // left ambiguous with \"the clear failed\".\n this.emit(\"queueCleared\", id, result.steering, result.followUp);\n return result;\n }\n\n async abort(id: string): Promise<void> {\n const rpc = this.#processes.get(id);\n if (rpc) await rpc.send({ type: \"abort\" }, 30_000);\n }\n\n async rename(id: string, name: string): Promise<void> {\n const rpc = this.#processes.get(id);\n const session = this.#sessions.get(id);\n if (!rpc || !session) throw new Error(`no such session: ${id}`);\n await rpc.send({ type: \"set_session_name\", name }, 20_000);\n session.name = name;\n this.#patch(id);\n }\n\n /**\n * Ask Pi which models this session can use.\n *\n * Returns a result rather than throwing, for the same reason `fetchTranscript` does: the caller\n * needs \"there are none\" and \"the question failed\" to be different events. A list rendered as\n * empty when the command errored would say the session has no models, which is a different and\n * false claim (C14's family).\n */\n async listModels(\n id: string,\n ): Promise<{ ok: true; models: ModelOption[] } | { ok: false; message: string }> {\n const rpc = this.#processes.get(id);\n if (!rpc) return { ok: false, message: \"this session is not running\" };\n try {\n const data = (await rpc.send({ type: \"get_available_models\" }, 30_000)) as {\n models?: Record<string, unknown>[];\n };\n // Narrowed here rather than passed through: the picker renders these, and `model.name` may\n // be absent, so the shape is normalized once instead of at every render site.\n const models: ModelOption[] = (data?.models ?? [])\n .filter((model) => typeof model === \"object\" && model !== null)\n .map((model) => ({\n provider: String(model[\"provider\"] ?? \"\"),\n id: String(model[\"id\"] ?? \"\"),\n ...(typeof model[\"name\"] === \"string\" ? { name: model[\"name\"] } : {}),\n ...(typeof model[\"reasoning\"] === \"boolean\" ? { reasoning: model[\"reasoning\"] } : {}),\n }))\n // A model without a provider or an id cannot be selected by `set_model`, which requires\n // both, so offering it would be a control that cannot work.\n .filter((model) => model.provider !== \"\" && model.id !== \"\");\n return { ok: true, models };\n } catch (error) {\n return { ok: false, message: (error as Error).message };\n }\n }\n\n /**\n * Change a session's model, then re-read the state Pi reports.\n *\n * **The session's fields are not set optimistically.** Pi validates the choice (an unconfigured\n * provider fails with `No API key for <provider>/<model>`), persists it to the transcript, and\n * re-applies the thinking level for the new model. Setting `session.model` from the request\n * would therefore display a change that may not have happened, and would miss the thinking-level\n * adjustment \u2014 the two ways this can disagree with Pi.\n *\n * A refusal is thrown, not swallowed: the caller broadcasts it, so it appears as an error naming\n * the reason rather than as a control that silently did nothing.\n */\n async setModel(id: string, provider: string, modelId: string): Promise<void> {\n const rpc = this.#processes.get(id);\n const session = this.#sessions.get(id);\n if (!rpc || !session) throw new Error(`no such session: ${id}`);\n await rpc.send({ type: \"set_model\", provider, modelId }, 30_000);\n // Re-read rather than assume. `refreshState` also pulls the thinking level the switch applied.\n await this.#refreshState(id);\n }\n\n /** Which thinking levels the CURRENT model supports. Per-model, so it must be re-read on a switch. */\n async listThinkingLevels(\n id: string,\n ): Promise<{ ok: true; levels: string[] } | { ok: false; message: string }> {\n const rpc = this.#processes.get(id);\n if (!rpc) return { ok: false, message: \"this session is not running\" };\n try {\n const data = (await rpc.send({ type: \"get_available_thinking_levels\" }, 30_000)) as {\n levels?: unknown[];\n };\n return { ok: true, levels: (data?.levels ?? []).map((level) => String(level)) };\n } catch (error) {\n return { ok: false, message: (error as Error).message };\n }\n }\n\n /**\n * Change the thinking level, then re-read the state Pi reports.\n *\n * The same reasoning as `setModel`: Pi may reject a level the model does not support, so the\n * value shown must be the one Pi confirms rather than the one that was asked for.\n */\n async setThinkingLevel(id: string, level: string): Promise<void> {\n const rpc = this.#processes.get(id);\n const session = this.#sessions.get(id);\n if (!rpc || !session) throw new Error(`no such session: ${id}`);\n await rpc.send({ type: \"set_thinking_level\", level }, 20_000);\n await this.#refreshState(id);\n }\n\n async stop(id: string): Promise<void> {\n const session = this.#sessions.get(id);\n const rpc = this.#processes.get(id);\n if (!session || !rpc) return;\n session.state = \"stopped\";\n session.dialogs = [];\n this.#patch(id);\n await rpc.stop();\n this.#processes.delete(id);\n }\n\n async stopAll(): Promise<void> {\n await Promise.all([...this.#processes.keys()].map((id) => this.stop(id)));\n }\n\n /**\n * List a session's working directory (issue #8, ADR-0007).\n *\n * Synchronous and plain `fs`, at the orchestrator's UID \u2014 no sandbox, no RPC round trip. The\n * confinement is the session's own `cwd`, which is why this goes through the supervisor: it is the\n * component that knows what that cwd is, so there is exactly one answer to \"what is this session\n * confined to\".\n *\n * An unknown session is REFUSED rather than resolved against some other root. Falling back would\n * mean a stale session id browsed a directory that had nothing to do with it.\n */\n listFiles(\n id: string,\n path: string,\n options: { includeHidden?: boolean } = {},\n ): Result<DirListing> {\n const session = this.#sessions.get(id);\n if (!session) {\n return {\n ok: false,\n error: { reason: \"not-found\", message: `no session ${id}, so there is no working directory to list` },\n };\n }\n return listDirectory(session.cwd, path, options);\n }\n\n /** Read one text file from a session's working directory, confined the same way. */\n readFile(id: string, path: string): Result<FileContent> {\n const session = this.#sessions.get(id);\n if (!session) {\n return {\n ok: false,\n error: { reason: \"not-found\", message: `no session ${id}, so there is no working directory to read from` },\n };\n }\n return readFileText(session.cwd, path);\n }\n\n /**\n * Where this Pi Central installation lives, resolved once.\n *\n * Overridable so a test can supply a path rather than depending on where the test file happens to\n * be \u2014 the same seam `#config` provides for everything else. `undefined` means the walk could not\n * identify it, in which case no session is warned: an unidentifiable path is not evidence of\n * safety, but a warning on every session would be noise, so the honest report is the absence of a\n * claim. The startup banner says so instead, once.\n */\n #installationRoot: string | undefined;\n\n setInstallationRoot(path: string | undefined): void {\n this.#installationRoot = path;\n }\n\n #patch(id: string): void {\n const session = this.#sessions.get(id);\n if (session) this.emit(\"update\", session);\n }\n}\n\n/**\n * Explain a session's death in a way a reader can act on.\n *\n * The MOST LIKELY first-run failure is also the least self-explanatory. Seatbelt does not nest\n * (C1), so running the orchestrator inside another sandbox makes `sandbox-exec` fail with EPERM\n * and every session dies the moment it starts. The raw output \u2014 `pi exited (code=71)` plus\n * `sandbox_apply: Operation not permitted` \u2014 reads like a launcher bug and sends the reader\n * looking in the wrong place, so the diagnosis is attach\u00ADed here, where the evidence is.\n *\n * Exported so the test exercises this function rather than a paraphrase of it, which is the\n * same reason `interpretRecord` is pure.\n */\nexport function describeExitFailure(\n code: number | null,\n signal: string | null,\n stderr: string,\n): string {\n const base = `pi exited (code=${code ?? \"-\"}, signal=${signal ?? \"-\"})${stderr ? `\\n${stderr}` : \"\"}`;\n if (!/sandbox_apply|Operation not permitted/i.test(stderr)) return base;\n return (\n `${base}\\n\\n` +\n \"This looks like Seatbelt refusing to nest (C1). The orchestrator must run from a plain \" +\n \"terminal, not inside another sandbox, editor task, or agent harness -- because `safehouse` \" +\n \"applies its policy with `sandbox-exec`, and that cannot run inside an existing sandbox.\"\n );\n}\n\n/** Only used so a display name is never a bare id. */\nfunction defaultName(cwd: string, id: string): string {\n const base = cwd.split(\"/\").filter(Boolean).pop();\n return base && base.length > 0 ? base : id;\n}\n\n/**\n * A record's list field as strings.\n *\n * Records are untrusted, so the elements are coerced rather than asserted: a `queue_update` whose\n * `steering` held objects would otherwise reach the composer as `[object Object]` or, worse, be\n * rendered as markup. Nothing is filtered out, because dropping a queued message would make the\n * count and the list disagree in a way the user cannot see.\n */\nfunction asTextArray(value: unknown): string[] {\n return Array.isArray(value) ? value.map((item) => String(item)) : [];\n}\n\n/** The `send` surface `clearQueueVia` needs, so the ordering can be tested without a live Pi. */\nexport type RpcSend = (command: Command, timeoutMs: number) => Promise<unknown>;\n\n/**\n * Empty Pi's queue, and optionally abort, in the order that makes the texts recoverable.\n *\n * **The order is the whole point, and it is why this is a separate exported function rather than\n * three lines inside `Supervisor#clearQueue`.** Two things go wrong if the abort is sent first:\n *\n * - Pi's `clear_queue` returns what it removed, so aborting first lets the run settle in the\n * window before the clear, and Pi may begin delivering one of the queued messages as a real\n * turn \u2014 the user asked for the opposite.\n * - Any later refactor that reorders the pair would be invisible. Extracted, the order is an\n * assertion in a test (C13's lesson: a thing that must happen in one order should be\n * testable in that order).\n *\n * The returned texts are normalised from untrusted record data, so a caller can render them\n * without a second narrowing step.\n */\nexport async function clearQueueVia(\n send: RpcSend,\n andStop: boolean,\n): Promise<{ steering: string[]; followUp: string[] }> {\n const cleared = (await send({ type: \"clear_queue\" }, 20_000)) as {\n steering?: unknown;\n followUp?: unknown;\n };\n const result = {\n steering: asTextArray(cleared?.steering),\n followUp: asTextArray(cleared?.followUp),\n };\n // Only now, once nothing is left to be delivered.\n if (andStop) await send({ type: \"abort\" }, 30_000);\n return result;\n}\n", "/**\n * Safehouse launch policy \u2014 the SINGLE place a session command line is assembled.\n *\n * If a second call site ever appears, the guarantee that every session's grant set is\n * derived from exactly one function is gone (packages/orchestrator/README.md, ADR-0002).\n *\n * Isolation is per session, and the working directory is its only input (ADR-0004). A\n * session's cwd is fixed at spawn and never changes (ADR-0008), so this function has no\n * \"update\" path by design.\n *\n * Three controls are composed here, and they are independent \u2014 each covers a gap the others\n * do not (ADR-0010):\n *\n * 1. the Safehouse argv \u2014 filesystem grants, derived from the workdir\n * 2. the appended fragment \u2014 denies for the host's *credential* paths\n * 3. the spawn environment \u2014 what the session can see at all (C11)\n *\n * On `--env-pass`: it **is** used, and the reason is worth recording because an earlier reading of\n * the same fact caused a defect.\n *\n * The fact: Safehouse's environment handling is add-only. `--env-pass` can introduce a name, and\n * cannot remove one \u2014 `SSH_AUTH_SOCK` is on its default list (C4a), so no flag will take it away.\n * That is why *sanitisation* happens in `env.ts`, and that part was always right.\n *\n * What an earlier comment here generalised from it \u2014 \"so `--env-pass` is deliberately not used\" \u2014\n * was wrong, and cost a working feature. Safehouse runs the session as `env -i` plus its own list\n * of names, so a variable that is not requested **by name** is discarded no matter how carefully\n * the spawn environment sets it. Without `--env-pass`, every variable we inject is silently\n * dropped: the agent's `gh` fell back to the host config (denied, so it refused to start) and its\n * git identity was never applied.\n *\n * Removal and delivery are different problems. `env.ts` solves removal; this flag solves delivery.\n */\nimport { accessSync, constants, mkdirSync, statSync, writeFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { buildSessionEnv } from \"./env.ts\";\nimport type { LaunchSpec } from \"./rpc.ts\";\n\nexport type LaunchMode = \"sandboxed\" | \"direct\";\n\nexport interface SessionSpec {\n id: string;\n cwd: string;\n mode: LaunchMode;\n /** Extra safehouse `--enable` features. Validated against a denylist. */\n enable?: string[];\n /**\n * Values injected into the session environment \u2014 the trusted channel for the agent's own\n * credentials (ADR-0010). Never sourced from `process.env` by the caller.\n */\n env?: Record<string, string>;\n /**\n * A rendered policy fragment appended after Safehouse's generated rules (ADR-0010 \u00A72).\n * Write it with `renderPolicyFragment` in `fragment.ts`.\n */\n policyFragment?: string;\n /** Directory to write the rendered fragment into. Required when `policyFragment` is set. */\n fragmentDir?: string;\n safehouse: string;\n pi: string;\n /** Optional session display name (`--name`). */\n name?: string;\n}\n\nexport interface ComposedPolicy {\n launch: LaunchSpec;\n /** Human-readable description of the isolation actually in force. */\n isolation: string;\n warnings: string[];\n /** The rendered fragment's path, when one was applied. Surfaced for verification. */\n fragmentFile?: string;\n}\n\n/**\n * `--enable` features Pi Central must never pass for a supervised session.\n *\n * `wide-read` grants read across the filesystem, which is exactly the union ADR-0002 exists\n * to prevent: one prompt injection would reach every project the user owns.\n *\n * `all-agents` / `all-apps` are refused for the same reason at a coarser grain: they enable\n * broad feature sets rather than one scoped capability, so the resulting grant is not derived\n * from the workdir at all. Verify against `safehouse --help` on upgrade; the supported list is\n * finite and printed there.\n *\n * `ssh` and `keychain` re-open the credential boundary this policy exists to close. Both were\n * verified by diffing generated policies (`safehouse --stdout \u2026 --enable=ssh`): `ssh` appends\n * an `Integration: SSH` block (1,076 \u2192 1,136 lines) granting agent-socket network access, and\n * `keychain` grants keychain read/write plus `SecurityServer` mach-lookups.\n *\n * Note that refusing `ssh` does **not** by itself close the ssh-agent path: `SSH_AUTH_SOCK` is\n * already in Safehouse's sanitized environment allowlist (C4a). That is closed in the spawn\n * environment (C11) and again in the appended fragment.\n *\n * `cloud-credentials` and `cloud-storage` are refused by the same reasoning: they hand the\n * session the *user's* cloud identity rather than the agent's own (ADR-0010).\n */\nconst FORBIDDEN_ENABLE = new Set([\n \"wide-read\",\n \"all-agents\",\n \"all-apps\",\n \"ssh\",\n \"keychain\",\n \"cloud-credentials\",\n \"cloud-storage\",\n]);\n\n/**\n * Flags that weaken the profile's own protections. Refused outright rather than warned\n * about, because a warning implies the session still starts.\n *\n * Both are real safehouse flags (verified against `safehouse --help`, 0.12.0).\n */\nconst FORBIDDEN_FLAGS = [\"--allow-workdir-config-writes\", \"--allow-profile-writes\"];\n\nexport function composePolicy(spec: SessionSpec): ComposedPolicy {\n const warnings: string[] = [];\n\n assertLaunchableDirectory(spec.cwd);\n\n if (spec.mode === \"direct\") {\n return {\n launch: {\n command: spec.pi,\n args: [\"--mode\", \"rpc\", ...(spec.name ? [\"--name\", spec.name] : [])],\n cwd: spec.cwd,\n env: buildSessionEnv(spec.env ? { set: spec.env } : {}),\n },\n isolation: \"NONE \u2014 pi runs unsandboxed, inheriting this process's sandbox (if any)\",\n warnings: [\"Direct mode applies no Safehouse policy. Feasibility testing only.\"],\n };\n }\n\n const enable = (spec.enable ?? []).filter((feature) => {\n if (FORBIDDEN_ENABLE.has(feature)) {\n warnings.push(`Refused --enable=${feature}: it grants access beyond the workdir.`);\n return false;\n }\n return true;\n });\n\n const fragmentFile = materializeFragment(spec);\n\n const args: string[] = [\"--workdir\", spec.cwd];\n if (enable.length > 0) args.push(`--enable=${enable.join(\",\")}`);\n // Emitted after generated rules; SBPL last-match-wins makes these denies authoritative.\n if (fragmentFile !== undefined) args.push(`--append-profile=${fragmentFile}`);\n\n // Handed to `spawn` directly, replacing the inherited environment entirely (C11).\n const env = buildSessionEnv(spec.env ? { set: spec.env } : {});\n\n /**\n * Tell Safehouse to forward the variables we injected.\n *\n * **Without this, the values never reach the session.** Safehouse runs the wrapped command as\n * `env -i` followed by its own list of names (verified in the installed script), so a variable\n * that is not on that list is discarded \u2014 however carefully we set it in the spawn environment.\n *\n * The names are DERIVED from the environment we just built rather than written out, because a\n * literal list drifts: someone adds a variable to `agentRoutingEnv` and the new one is silently\n * dropped again. Deriving it means the delivery set is exactly the injected set, by\n * construction.\n *\n * Forwarding everything we inject cannot leak anything, because `env.ts` built that object \u2014 it\n * is the trusted channel of ADR-0010 \u00A73, never `process.env` (C11).\n *\n * This was a real defect, not a precaution. `GH_CONFIG_DIR` was absent from Safehouse's\n * defaults, so the agent's `gh` fell back to the host's `~/.config/gh/config.yml`, found it\n * denied by our own fragment, and refused to start \u2014 while `GIT_CONFIG_GLOBAL` and\n * `GIT_CONFIG_SYSTEM` were dropped in the same way, so the agent's git identity was never in\n * effect either. The deny was working perfectly; the capability next to it was dead.\n */\n // Forward ONLY what we inject, not everything in `env`.\n //\n // Safehouse builds its own `HOME`, `PATH`, `TMPDIR`, `SHELL`, `USER`, `LOGNAME` and forwards a\n // terminal/editor/locale list of its own (verified in the installed script). Naming those here\n // would override values it chose deliberately \u2014 `--env-pass` upserts \u2014 for no gain. The names\n // that matter are the ones Safehouse cannot know about: the agent's routing variables.\n //\n // Derived from `spec.env` rather than written out, so adding a variable to `agentRoutingEnv`\n // cannot silently reintroduce this defect.\n const injected = Object.keys(spec.env ?? {});\n if (injected.length > 0) args.push(`--env-pass=${injected.join(\",\")}`);\n // The names must exist in the safehouse process's own environment, and they do: `env`, built\n // from the same values, is handed to `spawn` for exactly that process.\n\n for (const bad of FORBIDDEN_FLAGS) {\n // Not merely absent: asserted absent, so a future caller cannot smuggle one in.\n if (args.includes(bad)) throw new Error(`refusing to pass ${bad}`);\n }\n\n args.push(\"--\", spec.pi, \"--mode\", \"rpc\");\n if (spec.name) args.push(\"--name\", spec.name);\n\n const result: ComposedPolicy = {\n launch: { command: spec.safehouse, args, cwd: spec.cwd, env },\n isolation:\n `Safehouse workdir=${spec.cwd}` +\n `${enable.length ? ` enable=${enable.join(\",\")}` : \"\"}` +\n `${fragmentFile !== undefined ? \" +bespoke-fragment\" : \"\"}`,\n warnings,\n };\n if (fragmentFile !== undefined) result.fragmentFile = fragmentFile;\n return result;\n}\n\n/**\n * Write the fragment and return its path, or `undefined` when none was requested.\n *\n * Mode 0600, and in a 0700 directory: Safehouse reads this file as the orchestrator, and a\n * writable policy file would be a policy-injection route. Safehouse additionally emits a\n * terminal `deny file-write*` for every `--append-profile` path, so a session cannot rewrite\n * the rules that confine it \u2014 that emitted protection is why appending is safe at all, and\n * `--allow-profile-writes` (refused above) is what would defeat it.\n */\nfunction materializeFragment(spec: SessionSpec): string | undefined {\n if (spec.policyFragment === undefined) {\n if (spec.fragmentDir !== undefined) {\n throw new Error(\"fragmentDir was given without a policyFragment to write\");\n }\n return undefined;\n }\n if (spec.fragmentDir === undefined) {\n throw new Error(\"a policyFragment needs a fragmentDir to be written into\");\n }\n const dir = resolve(spec.fragmentDir);\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n const file = join(dir, `${spec.id}.sb`);\n writeFileSync(file, spec.policyFragment, { mode: 0o600 });\n return file;\n}\n\n/**\n * A directory is launchable when it exists, is a directory, and is readable. `statSync`\n * would follow a symlink, so this also rejects a path that resolves to a non-directory.\n *\n * Validated here because Pi exits `1` in RPC mode on a missing cwd rather than prompting\n * (C2, US-5), and because safehouse would otherwise be handed a grant for nothing.\n */\nfunction assertLaunchableDirectory(dir: string): void {\n let stats;\n try {\n stats = statSync(dir);\n } catch {\n throw new Error(`working directory does not exist: ${dir}`);\n }\n if (!stats.isDirectory()) throw new Error(`working directory is not a directory: ${dir}`);\n try {\n accessSync(dir, constants.R_OK | constants.X_OK);\n } catch {\n throw new Error(`working directory is not readable: ${dir}`);\n }\n}\n\n/**\n * Render the equivalent shell command, for display. Displayed, never executed.\n *\n * A note on why there is no metacharacter scan on `cwd`: the supervisor spawns with\n * `child_process.spawn` and no shell, so a path containing `$`, `(`, or a space is passed\n * as a single argv element and cannot be interpreted. Adding a scan would refuse legitimate\n * directories such as `/Users/x/My Project (copy)` while protecting nothing. The real\n * control is that the command is a fixed binary and the argument *shapes* are fixed; the\n * only free values are a validated path and identifier-shaped env names.\n */\nexport function describe(policy: ComposedPolicy): string {\n return [policy.launch.command, ...policy.launch.args].join(\" \");\n}\n", "/**\n * The spawn environment for a session \u2014 the sanitization boundary (C11, C4a).\n *\n * Why this is not Safehouse's job: **environment variables are not a sandbox concept.** No\n * SBPL grant or deny constrains them, so no policy can keep a variable out of a session.\n * Two verified facts (Safehouse `0.12.0`, `bin/safehouse`) make relying on Safehouse\n * actively wrong:\n *\n * 1. Its sanitized allowlist **already contains `SSH_AUTH_SOCK`** (line 7055), so a session\n * launched through Safehouse still reaches the host's ssh-agent by default.\n * 2. `--env-pass` / `--env=FILE` merge **append-only** \u2014 the merge helper is\n * `runtime_append_requested_host_env_entries`. They can *add* a variable but never\n * *remove* one from the sanitized set.\n *\n * So the orchestrator builds the environment and hands it to `spawn`. `LaunchSpec.env` is\n * required, not optional, because omitting it in Node means \"inherit `process.env`\" \u2014 the\n * exact failure this module exists to prevent.\n */\nimport { homedir } from \"node:os\";\n\n/**\n * Variables forwarded **only if set** in the source environment.\n *\n * Deliberately an allowlist, not a denylist of secret-shaped names: that set is unbounded and\n * user-defined, so a denylist silently fails open on the next tool that invents one.\n *\n * Notably **absent**, with the reason:\n * - `SSH_AUTH_SOCK`, `SSH_AGENT_PID` \u2014 would let the session sign as the user with any key\n * in their agent, without reading any key file (the hardening project's F4). Git-over-SSH\n * uses a dedicated key with `IdentitiesOnly=yes` and needs no agent (ADR-0010 \u00A74).\n * - every `*_TOKEN`, `*_KEY`, `*_SECRET` \u2014 the agent's own credentials are injected per\n * session from its own store, never inherited (ADR-0010).\n */\nexport const PASSTHROUGH = [\n // Process basics a CLI needs to behave normally.\n \"HOME\",\n \"USER\",\n \"LOGNAME\",\n \"SHELL\",\n \"PATH\",\n \"TMPDIR\",\n \"LANG\",\n \"LC_ALL\",\n \"TERM\",\n \"COLORTERM\",\n \"EDITOR\",\n \"VISUAL\",\n // Pi's own switches (environment-variables.md). These select behaviour, not identity, so\n // passing them preserves user intent without carrying a credential.\n \"PI_OFFLINE\",\n \"PI_SKIP_VERSION_CHECK\",\n \"PI_TIMING\",\n \"PI_CODING_AGENT_DIR\",\n \"PI_CODING_AGENT_SESSION_DIR\",\n \"PI_PACKAGE_DIR\",\n \"PI_SUBAGENT_PI_BINARY\",\n] as const;\n\n/**\n * Variables that must never be *injected*, whoever asks, because they name the **host user's**\n * ssh-agent. Checked case-insensitively.\n *\n * `GH_TOKEN` is deliberately NOT here: injecting the agent's own token under that name is the\n * intended mechanism (ADR-0010 \u00A73). The distinction that matters is **provenance, not name**.\n */\nconst FORBIDDEN_NAMES = new Set([\"ssh_auth_sock\", \"ssh_agent_pid\"]);\n\nexport interface SessionEnvOptions {\n /** Source to read {@link PASSTHROUGH} from. Defaults to `process.env`. */\n source?: NodeJS.ProcessEnv;\n /** `PATH` to use when the source has none (e.g. a launchd-started orchestrator). */\n fallbackPath?: string;\n /**\n * Values set explicitly for this session \u2014 the **trusted channel** (ADR-0010).\n *\n * This is where the agent's *own* credentials go: a token from the agent store, an\n * agent-scoped `GH_CONFIG_DIR`, `GIT_CONFIG_GLOBAL`, `GIT_CONFIG_SYSTEM=/dev/null`.\n *\n * The caller must not source these from `process.env`. `buildSessionEnv` cannot verify\n * provenance \u2014 a value read out of the host environment is the user's secret whatever it is\n * called \u2014 so it enforces only the two structurally host-only names above, and the store is\n * responsible for the rest.\n */\n set?: Record<string, string>;\n}\n\n/**\n * Build the complete environment for one session.\n *\n * @throws if an injected name is malformed, or names the host ssh-agent. Failing loudly is\n * the point: a silently dropped credential surfaces much later as a confusing auth error.\n */\nexport function buildSessionEnv(options: SessionEnvOptions = {}): Record<string, string> {\n const source = options.source ?? process.env;\n const env: Record<string, string> = {};\n\n for (const name of PASSTHROUGH) {\n const value = source[name];\n if (value !== undefined) env[name] = value;\n }\n\n env[\"PATH\"] ??= options.fallbackPath ?? DEFAULT_PATH;\n\n for (const [name, value] of Object.entries(options.set ?? {})) {\n assertInjectableName(name);\n env[name] = value;\n }\n\n return env;\n}\n\n/** A minimal PATH, so a session is usable even if the orchestrator was launched without one. */\nexport const DEFAULT_PATH = \"/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin\";\n\n/**\n * Whether an environment lets its session reach the host's ssh-agent.\n *\n * Exposed so the isolation claim is *checkable* rather than asserted in a comment, and so a\n * test can prove the boundary holds.\n */\nexport function leaksSshAgent(env: Record<string, string>): boolean {\n return env[\"SSH_AUTH_SOCK\"] !== undefined || env[\"SSH_AGENT_PID\"] !== undefined;\n}\n\n/**\n * The agent-scoped routing variables (ADR-0010 \u00A73).\n *\n * These close the hardening project's F12 route, where the host's global gitconfig invoked\n * `gh auth git-credential` and resolved a token from the login keychain \u2014 a path that no file\n * deny on `~/.ssh` would stop. `GIT_CONFIG_SYSTEM=/dev/null` removes the host's system config\n * as an independent route to the same place.\n *\n * `HOME` is intentionally left alone: Pi needs the real home for `~/.pi` (C6), and the\n * credential paths under it are closed by the appended policy fragment instead.\n */\nexport function agentRoutingEnv(agentRoot: string): Record<string, string> {\n if (!agentRoot.startsWith(\"/\")) {\n throw new Error(`agent root must be an absolute path: ${JSON.stringify(agentRoot)}`);\n }\n return {\n GH_CONFIG_DIR: `${agentRoot}/gh`,\n GIT_CONFIG_GLOBAL: `${agentRoot}/gitconfig`,\n GIT_CONFIG_SYSTEM: \"/dev/null\",\n };\n}\n\n/** The agent root for an id, under a state root. One directory per agent identity. */\nexport function agentRootFor(stateRoot: string, agentId: string): string {\n if (!/^[A-Za-z0-9._-]+$/.test(agentId)) {\n throw new Error(`refusing to build an agent path from an unsafe id: ${JSON.stringify(agentId)}`);\n }\n return `${stateRoot}/agents/${agentId}`;\n}\n\n/** Where a fresh agent's store lives when Pi Central owns it (the `managed` mode). */\nexport function defaultAgentRoot(agentId: string): string {\n return agentRootFor(`${homedir()}/.config/pi-central`, agentId);\n}\n\nfunction assertInjectableName(name: string): void {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {\n throw new Error(`refusing to inject a malformed env var name: ${JSON.stringify(name)}`);\n }\n if (FORBIDDEN_NAMES.has(name.toLowerCase())) {\n throw new Error(\n `refusing to inject ${name}: it would expose the host user's ssh-agent to the session. ` +\n `An agent that needs SSH uses its own key with IdentitiesOnly=yes (ADR-0010 \u00A74).`,\n );\n }\n}\n", "/**\n * One supervised `pi --mode rpc` process (ADR-0003).\n *\n * A custom supervisor, not Pi's `RpcClient`: that class hardcodes `spawn(\"node\", [cliPath,\n * ...])`, which cannot express spawning through `safehouse`. Its correlation patterns are\n * reused conceptually; see packages/protocol/src/pi.ts for the types.\n *\n * Responsibilities, and nothing else:\n * - spawn the fixed command with its own process group, so teardown signals the whole\n * sandboxed tree (safehouse \u2192 pi \u2192 tools), not just pi\n * - frame stdout through the LF-only decoder (C7)\n * - correlate responses by `id`\n * - report exit, including stderr, so the supervisor can surface a failure verbatim\n *\n * It holds no policy logic and performs no state machine. Those are policy.ts and\n * supervisor.ts.\n */\nimport { spawn, type ChildProcessWithoutNullStreams } from \"node:child_process\";\nimport { EventEmitter } from \"node:events\";\nimport type { Command, PiRecord } from \"@pi-central/protocol\";\nimport { JsonlDecoder, encodeLine } from \"./framing.ts\";\n\nexport interface LaunchSpec {\n command: string;\n args: string[];\n cwd: string;\n /**\n * The session's complete environment. **Required, deliberately.**\n *\n * Node's `spawn` inherits `process.env` when `env` is omitted, which would hand the session\n * the orchestrator's ambient secrets \u2014 including `SSH_AUTH_SOCK`, which Safehouse's own\n * sanitized allowlist passes through (C4a). Making this non-optional means a caller that\n * forgets it fails to compile instead of leaking silently (C11).\n */\n env: Record<string, string>;\n}\n\ninterface Pending {\n resolve: (value: unknown) => void;\n reject: (error: Error) => void;\n timer: NodeJS.Timeout;\n}\n\nexport interface ExitInfo {\n code: number | null;\n signal: string | null;\n stderr: string;\n}\n\n/** Events: `record` (PiRecord), `stderr` (string), `exit` (ExitInfo), `failed` (Error). */\nexport class RpcProcess extends EventEmitter {\n readonly launch: LaunchSpec;\n\n #child: ChildProcessWithoutNullStreams | null = null;\n #decoder = new JsonlDecoder();\n #pending = new Map<string, Pending>();\n #seq = 0;\n #stderr = \"\";\n #stopping = false;\n #exited = false;\n\n constructor(launch: LaunchSpec) {\n super();\n this.launch = launch;\n }\n\n start(): this {\n if (this.#child) throw new Error(\"session already started\");\n const child = spawn(this.launch.command, this.launch.args, {\n cwd: this.launch.cwd,\n // Replaces the inherited environment outright. There is no merge: an omitted `env`\n // would mean \"inherit everything\", which is the failure C11 exists to prevent.\n env: this.launch.env,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n // Own process group, so a wedged session cannot outlive the dashboard.\n detached: true,\n });\n this.#child = child;\n\n child.stdout.on(\"data\", (chunk: Buffer) => {\n for (const record of this.#decoder.push(chunk)) this.#dispatch(record);\n });\n child.stderr.on(\"data\", (chunk: Buffer) => {\n const text = chunk.toString(\"utf8\");\n this.#stderr += text;\n this.emit(\"stderr\", text);\n });\n child.on(\"error\", (error) => this.emit(\"failed\", error));\n child.on(\"exit\", (code, signal) => {\n this.#exited = true;\n // Any record still buffered at exit is real; flush before reporting.\n for (const record of this.#decoder.flush()) this.#dispatch(record);\n for (const [, pending] of this.#pending) {\n clearTimeout(pending.timer);\n pending.reject(new Error(`pi exited before responding (code=${code ?? \"-\"}, signal=${signal ?? \"-\"})`));\n }\n this.#pending.clear();\n this.emit(\"exit\", { code, signal, stderr: this.stderrTail } satisfies ExitInfo);\n });\n\n return this;\n }\n\n #dispatch(record: PiRecord): void {\n if (record.type === \"response\" && typeof record.id === \"string\") {\n const pending = this.#pending.get(record.id);\n if (pending) {\n this.#pending.delete(record.id);\n clearTimeout(pending.timer);\n if (record.success === false) {\n pending.reject(new Error(String(record.error ?? \"command failed\")));\n } else {\n pending.resolve(record.data);\n }\n return;\n }\n }\n this.emit(\"record\", record);\n }\n\n /** Send a command and resolve with its response `data`, or reject on error/timeout. */\n /**\n * Send a command and await its correlated response.\n *\n * Takes the CLOSED `Command` union, not `{ type: string } & Record<string, unknown>`.\n *\n * This is not a style preference. The union was documented as \"closed, on purpose: the\n * supervisor must not be able to send an arbitrary object derived from untrusted input\", but\n * `send` accepted any object with a `type` string \u2014 so the union was decorative and a typo\n * (`get_message` for `get_messages`) compiled cleanly and failed only at runtime, as a\n * `success: false` response the caller might not check. `get_messages` was in fact absent from\n * the union while being called from the supervisor, which is exactly how that goes unnoticed.\n */\n send(command: Command, timeoutMs = 60_000): Promise<unknown> {\n const child = this.#child;\n if (!child || !child.stdin.writable) return Promise.reject(new Error(\"session is not running\"));\n\n const id = `c${++this.#seq}`;\n return new Promise<unknown>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.#pending.delete(id);\n reject(new Error(`timeout after ${timeoutMs}ms waiting for ${command.type}`));\n }, timeoutMs);\n timer.unref?.();\n this.#pending.set(id, { resolve, reject, timer });\n child.stdin.write(encodeLine({ ...command, id }));\n });\n }\n\n /** Fire-and-forget write, for records that produce no `response` (extension UI). */\n write(record: Record<string, unknown>): void {\n const child = this.#child;\n if (child?.stdin.writable) child.stdin.write(encodeLine(record));\n }\n\n /**\n * Bounded, graceful teardown. Close stdin first (Pi's preferred shutdown), then escalate\n * to the process group.\n *\n * Waits at every step, including after SIGKILL, so that when this resolves the process has\n * actually been reaped. Firing the final signal and returning immediately would leave\n * `running` true for a process that is already dying, and a caller doing `stopAll()` before\n * exit could not tell a clean shutdown from a wedged one.\n */\n async stop(graceMs = 1500): Promise<void> {\n if (this.#stopping) return;\n this.#stopping = true;\n const child = this.#child;\n if (!child || this.#exited) return;\n\n try {\n child.stdin.end();\n } catch {\n /* already closed */\n }\n\n const pid = child.pid;\n if (await this.#waitForExit(graceMs)) return;\n\n try {\n if (pid) process.kill(-pid, \"SIGTERM\"); // the group, not just the child\n } catch {\n /* gone */\n }\n if (await this.#waitForExit(2000)) return;\n\n try {\n if (pid) process.kill(-pid, \"SIGKILL\");\n } catch {\n /* gone */\n }\n // SIGKILL cannot be blocked or ignored, so the exit is prompt; still, wait for it rather\n // than assuming. Bounded, so a pathological child cannot wedge the caller forever.\n await this.#waitForExit(2000);\n }\n\n #waitForExit(ms: number): Promise<boolean> {\n const child = this.#child;\n if (!child || this.#exited) return Promise.resolve(true);\n return new Promise((resolve) => {\n const timer = setTimeout(() => resolve(this.#exited), ms);\n timer.unref?.();\n child.once(\"exit\", () => {\n clearTimeout(timer);\n resolve(true);\n });\n });\n }\n\n get stderrTail(): string {\n return this.#stderr.split(\"\\n\").slice(-12).join(\"\\n\").trim();\n }\n\n get running(): boolean {\n const child = this.#child;\n return child !== null && !this.#exited;\n }\n}\n", "/**\n * JSONL framing for Pi's RPC stream (C7).\n *\n * The rule: **one JSON object per record, terminated by LF (0x0a).** Never `readline`.\n *\n * Why this is its own module rather than a few lines in the supervisor: Node's `readline`\n * also splits on U+2028 (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR), both of which are\n * *legal inside a JSON string*. A model that emits one \u2014 in a code block, a quoted passage,\n * an error message \u2014 would have a single record torn in half and the supervisor would parse\n * two invalid fragments. The bug would be rare, output-dependent, and look like corruption.\n *\n * So: split on the byte 0x0a only, and tolerate a CR immediately before it (a CRLF sender).\n */\nimport type { PiRecord } from \"@pi-central/protocol\";\n\n/** Raised when a record has no `type`, so the caller can decide rather than guess. */\nexport class MalformedRecordError extends Error {\n // A plain field, not a parameter property: Node's strip-only TypeScript loader rejects\n // constructor parameter properties, and we run .ts directly (no build step).\n readonly line: string;\n constructor(line: string) {\n super(`record has no \"type\": ${line.slice(0, 120)}`);\n this.name = \"MalformedRecordError\";\n this.line = line;\n }\n}\n\n/**\n * A streaming splitter over a byte buffer. Feed it chunks; it yields complete records.\n *\n * Stateful, because a record can straddle any chunk boundary: the JSON, and even the\n * terminating newline, may arrive split.\n */\nexport class JsonlDecoder {\n #buffer: Buffer = Buffer.alloc(0);\n\n /** Feed a chunk; returns any complete records it completed. */\n push(chunk: Buffer): PiRecord[] {\n this.#buffer = this.#buffer.length === 0 ? chunk : Buffer.concat([this.#buffer, chunk]);\n return this.#drain();\n }\n\n /** Records left when the stream ends. A trailing record without LF is still valid. */\n flush(): PiRecord[] {\n const out = this.#drain();\n if (this.#buffer.length > 0) {\n const record = this.#parse(this.#buffer);\n this.#buffer = Buffer.alloc(0);\n if (record) out.push(record);\n }\n return out;\n }\n\n #drain(): PiRecord[] {\n const records: PiRecord[] = [];\n for (;;) {\n const nl = this.#buffer.indexOf(0x0a);\n if (nl === -1) break;\n const line = this.#buffer.subarray(0, nl);\n this.#buffer = this.#buffer.subarray(nl + 1);\n const record = this.#parse(line);\n if (record) records.push(record);\n }\n return records;\n }\n\n #parse(raw: Buffer): PiRecord | null {\n let line = raw;\n // Tolerate a CR before the LF.\n if (line.length > 0 && line[line.length - 1] === 0x0d) line = line.subarray(0, line.length - 1);\n if (line.length === 0) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line.toString(\"utf8\"));\n } catch {\n // A malformed record must not kill the reader (docs/PROTOCOL.md).\n return null;\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n const record = parsed as PiRecord;\n if (typeof record.type !== \"string\" || record.type.length === 0) return null;\n return record;\n }\n}\n\n/** Encode a command as a single LF-terminated line. */\nexport function encodeLine(value: unknown): string {\n return `${JSON.stringify(value)}\\n`;\n}\n", "/**\n * Resume validation (US-5, D-4, C2).\n *\n * The trap, verified in `dist/main.js:558`: if a session's recorded `cwd` no longer exists,\n * Pi's modes differ \u2014\n *\n * interactive Pi prompts \"continue in current cwd\" or cancel\n * print / JSON / RPC Pi writes MissingSessionCwdError to stderr and **exits 1**\n *\n * Pi Central drives RPC mode, so it gets the exit, not the prompt. It must therefore check\n * the recorded cwd ITSELF before spawning, and present the outcome, rather than spawn and\n * interpret a crash.\n *\n * This is not theoretical: 82 of 833 sessions on the development machine point at\n * directories that no longer exist (finished worktrees, unmounted volumes, moved checkouts).\n *\n * Pi Central does NOT retarget a session to another directory (ADR-0008). The only remedies\n * are: restore the directory, or start a new session where you actually mean to work.\n */\nimport { accessSync, constants, statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport type { HistoricalSession } from \"@pi-central/protocol\";\n\nexport type ResumeReadiness =\n | { ok: true; cwd: string }\n | { ok: false; reason: ResumeBlocker; message: string; cwd: string };\n\nexport type ResumeBlocker = \"missing-directory\" | \"not-a-directory\" | \"unreadable\" | \"unknown-cwd\";\n\n/**\n * Decide whether a discovered session can be resumed, before anything is spawned.\n *\n * Takes the session rather than a bare path so the blocker carries the reason a user needs:\n * \"this project's directory is gone\" is actionable, \"resume failed\" is not.\n */\nexport function checkResumable(session: Pick<HistoricalSession, \"cwd\" | \"path\">): ResumeReadiness {\n if (!session.cwd) {\n return {\n ok: false,\n reason: \"unknown-cwd\",\n cwd: session.cwd,\n message:\n \"This session does not record the directory it ran in, so a sandbox cannot be composed for it. Start a new session in the directory you want.\",\n };\n }\n\n let stats;\n try {\n stats = statSync(session.cwd);\n } catch (error) {\n // The error CODE decides the message, and this was originally a bare `catch` that always\n // claimed \"no longer exists\". Observed in practice: an EACCES on an ancestor directory \u2014 the\n // path was present and fine \u2014 reported as a missing directory, which sends the reader off to\n // restore something that is already there. `unreadable` existed as a blocker for exactly this\n // case and could never be produced by this branch.\n const code = (error as NodeJS.ErrnoException).code;\n const unreadable = code === \"EACCES\" || code === \"EPERM\";\n return {\n ok: false,\n reason: unreadable ? \"unreadable\" : \"missing-directory\",\n cwd: session.cwd,\n message: unreadable\n ? `The directory this session worked in cannot be read (${code}):\\n ${session.cwd}\\n\\nThis is a permissions problem, not a missing directory. Check that you can enter it: it may be owned by another user, or a parent directory may deny access.`\n : `The directory this session worked in no longer exists:\\n ${session.cwd}\\n\\nRestore it, or start a new session in the directory you want to work in. Pi Central does not move a session between directories.`,\n };\n }\n\n if (!stats.isDirectory()) {\n return {\n ok: false,\n reason: \"not-a-directory\",\n cwd: session.cwd,\n message: `The directory this session recorded is no longer a directory:\\n ${session.cwd}`,\n };\n }\n\n try {\n accessSync(session.cwd, constants.R_OK | constants.X_OK);\n } catch {\n return {\n ok: false,\n reason: \"unreadable\",\n cwd: session.cwd,\n message: `The directory this session worked in is not readable:\\n ${session.cwd}`,\n };\n }\n\n return { ok: true, cwd: resolve(session.cwd) };\n}\n", "/**\n * Can a session actually USE the agent credential store? (issue #28)\n *\n * The failure this exists for, reported from real use inside a sandboxed session:\n *\n * warning: failed to load config: open /Users/sven1103/.config/gh/config.yml: operation not permitted\n * failed to create root command: failed to read configuration: operation not permitted\n *\n * That message names a **host** path, so it reads as \"the sandbox is blocking my config\". The truth\n * was the opposite: the agent's own config was never pointed at, so `gh` fell back to the host's \u2014\n * which the fragment correctly denies \u2014 and then refused to start. The deny was working perfectly;\n * the *capability* was dead.\n *\n * The same shape had already happened once, for Pi's `auth.json` (C12): a denied read is\n * indistinguishable from an absent file from inside the session. Both are cases where verifying a\n * deny proved nothing about whether the intended thing worked, and where the diagnosis required\n * reading Safehouse's source.\n *\n * ## The distinction this module is built around\n *\n * \"The capability is missing\" has two causes with opposite fixes, and conflating them is what makes\n * the original message mislead:\n *\n * - **not provisioned** \u2014 there is nothing there. A setup step: create the file, add the key.\n * - **provisioned but unreachable** \u2014 the thing exists and the session still cannot use it. That\n * is a **bug in Pi Central** (a policy that fails to grant what the environment points at, or\n * permissions), not something the user did wrong.\n *\n * Telling a user to \"provision\" a credential that is already sitting on disk sends them to fix the\n * wrong thing, which is the cost this module is meant to remove.\n *\n * ## What this CAN and CANNOT see\n *\n * It runs in the orchestrator, before the session spawns, so it reasons from facts available there:\n * the store's contents, whether those files are readable by their owner, and **the rendered policy\n * fragment the session will actually run under**. That last one is what makes the reachability\n * check real rather than a guess \u2014 the C15 defect was exactly a fragment that did not grant what\n * `GH_CONFIG_DIR` pointed at, and the fragment text is checkable.\n *\n * It does **not** observe the session from the inside. Whether a keychain item resolves, or whether\n * `gh` finds its token, needs a live session \u2014 those are asserted by `verify-three.ts` probes, which\n * run a real sandboxed process. This module deliberately claims less than that: it reports what\n * would make the capability unusable, not that the capability works.\n *\n * ## What it never says\n *\n * It does not claim anything about the **host's** credentials being blocked. That is already proven\n * by the fragment and its tests, and conflating the two is precisely how the original bug hid for so\n * long. Every message here names a path inside the agent root, and a test asserts that no message\n * names a host credential path.\n */\nimport { accessSync, constants } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { KEYCHAIN_FILE, type ProviderAuth, type StoreSnapshot } from \"./agent-store.ts\";\nimport {\n allowedSignersPath,\n auditSigning,\n readSigning,\n signingKeyPath,\n signingMaterial,\n signingFixCommands,\n} from \"./signing.ts\";\nimport { readFileSync } from \"node:fs\";\n\n/**\n * The two causes, plus \"fine\".\n *\n * `unreachable` means \"it is there and the session still could not use it\", which is a defect in\n * Pi Central rather than a setup step.\n */\nexport type CapabilityState = \"ok\" | \"not-provisioned\" | \"unreachable\";\n\nexport type CapabilityId = \"gitconfig\" | \"gh-config\" | \"ssh-key\" | \"keychain\" | \"signing\";\n\nexport interface CapabilityFinding {\n id: CapabilityId;\n state: CapabilityState;\n /**\n * One sentence naming (a) which capability, (b) what is wrong, and (c) what to do or that this is\n * Pi Central's bug. Empty when `state` is `ok`.\n */\n message: string;\n}\n\nexport interface CapabilityAssessment {\n findings: CapabilityFinding[];\n /** The non-ok findings as warning strings, ready for `LiveSession.warnings`. */\n warnings: string[];\n /** True when everything needed for the session to act as the agent is usable. */\n usable: boolean;\n}\n\nexport interface CapabilityInput {\n store: StoreSnapshot;\n /**\n * The rendered SBPL fragment the session will run under.\n *\n * Absent in `direct` mode, which applies no policy \u2014 so nothing can be *denied* and reachability\n * is not a policy question there. That is why `sandboxed` is separate rather than inferred.\n */\n fragment?: string | undefined;\n /** Whether the session runs under Safehouse. Direct mode has no policy to fail. */\n sandboxed: boolean;\n /** What the provider configuration implies about keychain access (C13). */\n providerAuth: ProviderAuth;\n /** Where the agent keychain would be. */\n keychainPath: string;\n}\n\n/** Can this process read the file? Runs at the user's UID, which is what the session inherits for file access. */\nfunction isReadable(path: string): boolean {\n try {\n accessSync(path, constants.R_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Does the fragment grant this path to the session?\n *\n * Deliberately a check on the **rendered policy text** rather than on the inputs that produced it.\n * Re-deriving \"the supervisor passed an agentRoot\" would be circular \u2014 it would pass whenever the\n * caller believed it had done the right thing, which is exactly the belief the C15 defect\n * contradicted. The fragment is the artifact that gets applied, so the fragment is what is checked.\n *\n * SBPL is last-match-wins and the agent's grant is emitted after the host denies, so the presence of\n * an allow is the property to look for. It is matched as a `subpath` grant of the exact root.\n */\nfunction fragmentGrants(fragment: string, path: string): boolean {\n // A `subpath` grant of the root is what covers everything beneath it. Escaped exactly as\n // `renderPolicyFragment` writes it, so the two cannot drift in quoting without this noticing.\n return fragment.includes(`(subpath ${JSON.stringify(path)})`);\n}\n\n/** Does the fragment grant the macOS keychain access a service-only provider lookup needs (C13)? */\nfunction fragmentGrantsKeychain(fragment: string): boolean {\n return (\n fragment.includes('(global-name \"com.apple.SecurityServer\")') &&\n // The search list is load-bearing: without it `security` silently falls back to\n // [login, System], does not see the agent keychain, and every lookup fails with no useful\n // error (C13). A grant without it looks correct and is not.\n //\n // Matched as the RULE, not as a bare substring, and that distinction was found by a test: the\n // generated fragment also *describes* the file in a comment (\"The search list lives in\n // com.apple.security.plist\"), so a substring check would pass on a fragment whose only mention\n // of the search list is prose. Matching the `(home-literal \u2026)` form is what makes it a check.\n fragment.includes('(home-literal \"/Library/Preferences/com.apple.security.plist\")')\n );\n}\n\n/**\n * Assess what the session will be able to do with the agent credentials.\n *\n * Pure: every input is a fact passed in, so each branch is a test case rather than something that\n * requires a sandbox to observe. Ordering is stable (gitconfig, gh-config, ssh-key, keychain) so the\n * warnings do not shuffle between runs.\n */\nexport function assessAgentCapabilities(input: CapabilityInput): CapabilityAssessment {\n const { store, fragment, sandboxed, providerAuth, keychainPath } = input;\n const findings: CapabilityFinding[] = [];\n\n const root = store.root;\n const granted = (path: string): boolean =>\n // In direct mode no policy applies, so the only question is readability \u2014 which the checks\n // below answer separately. Returning `true` here keeps \"unreachable\" meaning \"the policy\n // denies it\", rather than firing on every direct-mode session.\n !sandboxed || (fragment !== undefined && fragmentGrants(fragment, path));\n\n // --- gitconfig: the agent's commit identity, via GIT_CONFIG_GLOBAL ---------------------------\n const gitconfigPath = join(root, \"gitconfig\");\n if (!store.has.gitconfig) {\n findings.push({\n id: \"gitconfig\",\n state: \"not-provisioned\",\n message:\n `the agent has no gitconfig at ${gitconfigPath}, which GIT_CONFIG_GLOBAL points at. ` +\n `Commits would have no identity and git would read no agent settings. ` +\n `Provision it (git config --file ${gitconfigPath} user.name \u2026).`,\n });\n } else if (!isReadable(gitconfigPath)) {\n findings.push({\n id: \"gitconfig\",\n state: \"unreachable\",\n message:\n `the agent gitconfig exists at ${gitconfigPath} but is not readable by its owner, so no ` +\n `process at this UID can use it. This is a permissions problem, not a missing setup step.`,\n });\n } else if (!granted(root)) {\n findings.push({\n id: \"gitconfig\",\n state: \"unreachable\",\n message:\n `the session's policy does not grant ${root}, while GIT_CONFIG_GLOBAL points into it. ` +\n `The agent would be configured to read a file its own sandbox denies \u2014 a Pi Central bug, ` +\n `not a setup step.`,\n });\n }\n\n // --- gh config: GH_CONFIG_DIR ----------------------------------------------------------------\n const ghDir = join(root, \"gh\");\n if (!store.has.ghConfig) {\n findings.push({\n id: \"gh-config\",\n state: \"not-provisioned\",\n message:\n `the agent has no gh config directory at ${ghDir}, which GH_CONFIG_DIR points at. ` +\n `gh would fall back to the host's config, find it denied, and refuse to start. ` +\n `Provision it (gh auth login with GH_CONFIG_DIR set, or copy an existing gh config).`,\n });\n } else if (!granted(root)) {\n findings.push({\n id: \"gh-config\",\n state: \"unreachable\",\n message:\n `the session's policy does not grant ${root}, while GH_CONFIG_DIR points into it. ` +\n `gh would read the host's config instead, find that denied, and fail \u2014 a Pi Central bug.`,\n });\n } else if (!isReadable(ghDir)) {\n findings.push({\n id: \"gh-config\",\n state: \"unreachable\",\n message:\n `the agent gh config directory exists at ${ghDir} but is not readable by its owner, ` +\n `so gh cannot read it. This is a permissions problem, not a missing setup step.`,\n });\n }\n\n // --- ssh key: needed for SSH push and for signing --------------------------------------------\n if (!store.has.sshKey) {\n findings.push({\n id: \"ssh-key\",\n state: \"not-provisioned\",\n message:\n `the agent has no SSH key under ${join(root, \"ssh\")}. It can still push over HTTPS with its ` +\n `gh token, but not over SSH, and it cannot sign commits. Provision a key to use either.`,\n });\n }\n\n // --- signing: the configuration, NOT the key (issue #27) --------------------------------------\n //\n // Reported separately from `ssh-key` above, and the separation is the whole point of the issue: the\n // key was present the entire time signing was silently off, so a check that looks for the key\n // cannot see this. This one reads the gitconfig and asserts the settings that make the key DO\n // something.\n //\n // Only when there is a gitconfig at all: with none, `commit.gpgsign` is unset and git does not\n // sign, which is exactly what the `gitconfig` finding above already says. Two findings for one\n // missing file would be noise, and noise trains the reader to ignore warnings.\n if (store.has.gitconfig) {\n const gitconfigPath = join(root, \"gitconfig\");\n const settings = readSigning(readGitconfigText(gitconfigPath));\n const audit = auditSigning(settings, root);\n // The material, checked separately from the settings: a correct gitconfig pointing at an absent\n // key is *complete config* by text and *broken* in fact, and the repair differs.\n const material = signingMaterial(root);\n if (audit.needsFix) {\n const commands = signingFixCommands(root).map((command) => ` ${command}`).join(\"\\n\");\n findings.push({\n id: \"signing\",\n state: audit.state === \"misconfigured\" ? \"unreachable\" : \"not-provisioned\",\n message:\n `the agent's gitconfig does not sign commits or tags: ${audit.problems.join(\"; \")}. ` +\n `Commits would be created unsigned, SILENTLY \u2014 the key material is present, so nothing ` +\n `else would report a problem (ADR-0010 \u00A74). This is an explicit fix, because the agent ` +\n `root is yours to maintain; Pi Central does not rewrite it:\\n${commands}`,\n });\n } else if (!material.key || !material.allowedSigners) {\n findings.push({\n id: \"signing\",\n state: \"not-provisioned\",\n message:\n `the agent's gitconfig is configured to sign (gpg.format=ssh, commit.gpgsign=true) but ` +\n `${!material.key ? `the signing key ${signingKeyPath(root)} is missing` : \"\"}` +\n `${!material.key && !material.allowedSigners ? \" and \" : \"\"}` +\n `${!material.allowedSigners ? `the allowed-signers file ${allowedSignersPath(root)} is missing` : \"\"}. ` +\n `Every commit would fail, rather than be created unsigned.`,\n });\n }\n }\n\n // --- keychain: only relevant when the provider config needs it (C13) --------------------------\n //\n // Reported only when it MATTERS, and only in sandboxed mode. A session whose provider keys are\n // stored inline needs no keychain, and a DIRECT-mode session has no policy that could deny the\n // lookup \u2014 warning there would be noise, and noise trains the reader to ignore warnings.\n if (providerAuth.needsKeychain && sandboxed) {\n if (fragment === undefined) {\n findings.push({\n id: \"keychain\",\n state: \"unreachable\",\n message:\n `the provider config at ${providerAuth.path} resolves keys via a command ` +\n `(${providerAuth.commandProviders.join(\", \")}), but this sandboxed session has no policy ` +\n `fragment at all. The keychain lookup would fail. This is a Pi Central bug.`,\n });\n } else if (!fragmentGrantsKeychain(fragment)) {\n findings.push({\n id: \"keychain\",\n state: \"unreachable\",\n message:\n `the provider config at ${providerAuth.path} resolves keys via a command ` +\n `(${providerAuth.commandProviders.join(\", \")}), but the session's policy grants no ` +\n `keychain access. The session would start, report idle, and then be unable to reach a ` +\n `model (C13) \u2014 a Pi Central bug, not a setup step.`,\n });\n } else if (store.has.keychain && !isReadable(keychainPath)) {\n findings.push({\n id: \"keychain\",\n state: \"unreachable\",\n message:\n `the agent keychain exists at ${keychainPath} but is not readable by its owner. ` +\n `This is a permissions problem, not a missing setup step.`,\n });\n }\n }\n\n const warnings = findings\n .filter((finding) => finding.state !== \"ok\")\n .map((finding) => `${label(finding)}: ${finding.message}`);\n\n return {\n findings,\n warnings,\n usable: findings.every((finding) => finding.state === \"ok\"),\n };\n}\n\n/**\n * The gitconfig's text, or empty.\n *\n * A read failure yields empty text, which audits as \"not configured to sign\" \u2014 the correct reading:\n * if it cannot be read, the session cannot be signed by it either. Returning a *healthy* value here\n * would be the C12/C15 mistake of letting an unreadable file look like a working one.\n */\nfunction readGitconfigText(path: string): string {\n try {\n return readFileSync(path, \"utf8\");\n } catch {\n return \"\";\n }\n}\n\nfunction label(finding: CapabilityFinding): string {\n const name = {\n gitconfig: \"agent git identity\",\n \"gh-config\": \"agent GitHub credentials\",\n \"ssh-key\": \"agent SSH key\",\n keychain: \"provider keychain access\",\n signing: \"agent commit signing\",\n }[finding.id];\n const kind = finding.state === \"not-provisioned\" ? \"not provisioned\" : \"provisioned but unreachable\";\n return `${name} ${kind}`;\n}\n\n/** The keychain path convention, so callers do not each re-derive it. */\nexport function agentKeychainPath(agentRoot: string): string {\n return join(agentRoot, KEYCHAIN_FILE);\n}\n", "/**\n * Commit and tag signing for an agent identity (issue #27).\n *\n * ## What was wrong\n *\n * ADR-0010 \u00A74 states that SSH push **and commit signing** are required for agents. The adopted agent\n * tree satisfied the first and not the second: its `gitconfig` set\n *\n * [commit] gpgsign = false\n * [tag] gpgsign = false\n *\n * with no `signingkey` and no `gpg.format`. Signing key material was present the whole time \u2014 a key\n * at `ssh/github_login_ed25519` and a matching `ssh/allowed_signers` \u2014 so commits were unsigned\n * **silently**, while every check that looked for the key reported success.\n *\n * That is the same shape as C15 and issue #28: the material exists, the configuration that makes it\n * *do* anything does not, and \"is the key there?\" cannot tell the difference.\n *\n * ## What this module does, and \u2014 more importantly \u2014 what it does not\n *\n * It **reads, parses, and audits**. It does not write.\n *\n * The agent root is `external` in this deployment (ADR-0010 \u00A75): it is shared with an interactive\n * terminal setup and is maintained by the user. Pi Central must never rewrite it silently, so the fix\n * here is an **explicit, confirmed action** \u2014 the exact `git config` commands are produced as text\n * and printed, and a person runs them. `signingFixCommands` exists for that and is part of the public\n * surface precisely so the fix is reproducible from the warning.\n *\n * ## The check that carries the security weight\n *\n * `user.signingkey` pointing **outside** the agent root is not a misconfiguration of degree, it is a\n * different failure: the agent would sign as the *host user*. `ssh` receives a path, not key\n * contents (ADR-0010 \u00A74), so a path is the only thing standing between \"the agent signed\" and \"the\n * user signed\". An in-root signing key is therefore an assertion, not a style preference, and it is\n * tested \u2014 including that a key one directory up is refused.\n *\n * ## Why the parser is hand-written\n *\n * `git config --list` is the obvious alternative and it is wrong here: it **resolves** includes and\n * applies system and global config, so it would report the *effective* value while hiding that the\n * agent's own file disagrees \u2014 and would read the host's configuration to do it. The audit must be\n * about the agent's own file, so the file is parsed. Only the handful of keys below are consulted,\n * and the parser covers exactly the syntax `git config` emits (`[section]`, `[section \"sub\"]`,\n * `key = value`), which is asserted against real `git config` output in the tests rather than\n * assumed.\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join, resolve, sep } from \"node:path\";\n\n/** The private key an agent signs with. The same key it pushes over SSH with (ADR-0010 \u00A74). */\nexport function signingKeyPath(root: string): string {\n return join(root, \"ssh\", \"github_login_ed25519\");\n}\n\n/** The `allowed_signers` file that lets a signature be verified without the host's configuration. */\nexport function allowedSignersPath(root: string): string {\n return join(root, \"ssh\", \"allowed_signers\");\n}\n\n/** The signing-related settings, as they appear in the agent's own gitconfig. */\nexport interface SigningSettings {\n /** `commit.gpgsign`. `undefined` means unset, which \u2014 like `false` \u2014 means git does not sign. */\n commitGpgsign?: string | undefined;\n /** `tag.gpgsign`. */\n tagGpgsign?: string | undefined;\n /** `gpg.format`. Must be `ssh`; the default is `openpgp`, which would look for gpg. */\n format?: string | undefined;\n /** `user.signingkey` \u2014 a PATH, because ssh takes paths and not key contents. */\n signingKey?: string | undefined;\n /** `gpg.ssh.allowedSignersFile`. Without it a signature cannot be verified at all. */\n allowedSignersFile?: string | undefined;\n}\n\n/** The dotted key `git config` uses, mapped to the field it fills. */\nconst KEYS: Record<string, keyof SigningSettings> = {\n [normalizeKey(\"commit.gpgsign\")]: \"commitGpgsign\",\n [normalizeKey(\"tag.gpgsign\")]: \"tagGpgsign\",\n [normalizeKey(\"gpg.format\")]: \"format\",\n [normalizeKey(\"user.signingkey\")]: \"signingKey\",\n [normalizeKey(\"gpg.ssh.allowedSignersFile\")]: \"allowedSignersFile\",\n};\n\n/**\n * Normalise a dotted git config key the way git compares it.\n *\n * **The section and the final key are case-insensitive; the subsection is case-sensitive.** That is\n * git's rule, and getting it wrong is not cosmetic: git writes `gpg.ssh.allowedSignersFile` as\n * `[gpg \"ssh\"] allowedSignersFile`, so a parser that lowercased the whole key produced\n * `gpg.ssh.allowedsignersfile` while the lookup table said `\u2026allowedSignersFile` \u2014 and a store that\n * was configured *exactly as instructed* was reported as missing that setting. Found by running the\n * audit against a store configured by the fix commands themselves rather than by hand.\n *\n * The subsection is left verbatim because it can legitimately match a remote or key name, where case\n * carries meaning.\n */\nexport function normalizeKey(key: string): string {\n const parts = key.split(\".\");\n if (parts.length < 2) return key.toLowerCase();\n const last = parts.length - 1;\n return parts.map((part, index) => (index === 0 || index === last ? part.toLowerCase() : part)).join(\".\");\n}\n\n/**\n * Parse a gitconfig into dotted keys.\n *\n * Only the syntax `git config` writes is handled, which is the point: this file is written by git,\n * not by hand, in the supported path. A line it cannot parse is skipped rather than guessed at \u2014\n * guessing would let a malformed file report a *healthier* state than it has.\n *\n * `[section \"subsection\"]` becomes `section.subsection.key`, which is what git does \u2014 and it is\n * needed here, because `git config` writes `gpg.ssh.allowedSignersFile` as `[gpg \"ssh\"]`. A parser\n * that only handled `[section]` would miss that key entirely and report every correctly-configured\n * store as incomplete.\n */\nexport function parseGitConfig(text: string): Map<string, string> {\n const values = new Map<string, string>();\n let prefix = \"\";\n for (const raw of text.split(\"\\n\")) {\n const line = raw.trim();\n if (line === \"\" || line.startsWith(\"#\") || line.startsWith(\";\")) continue;\n if (line.startsWith(\"[\")) {\n // `[user]`, `[gpg \"ssh\"]`, or git's whitespace-tolerant `[user \"x\"]`.\n const match = /^\\[([^\\]\"\\s]+)(?:\\s+\"([^\"]*)\")?\\]$/.exec(line);\n // Section lowercased (case-insensitive), subsection verbatim (case-sensitive), exactly as\n // git compares them.\n prefix =\n match?.[2] !== undefined\n ? `${match[1]!.toLowerCase()}.${match[2]}`\n : (match?.[1]?.toLowerCase() ?? \"\");\n continue;\n }\n const match = /^([A-Za-z][A-Za-z0-9-]*)\\s*=\\s*(.*)$/.exec(line);\n if (!match || prefix === \"\") continue;\n // git keeps any value, including a trailing comment, verbatim. Trailing whitespace is trimmed;\n // nothing else is interpreted, because interpreting it is how a path gets read as \"fine\".\n values.set(normalizeKey(`${prefix}.${match[1]!}`), match[2]!.trim());\n }\n return values;\n}\n\n/** The signing settings, as configured in the agent's own gitconfig. */\nexport function readSigning(text: string): SigningSettings {\n const values = parseGitConfig(text);\n const signing: SigningSettings = {};\n for (const [key, field] of Object.entries(KEYS)) {\n const value = values.get(key);\n if (value !== undefined && value !== \"\") signing[field] = value;\n }\n return signing;\n}\n\n/** Read an agent's gitconfig and return its signing settings. Never throws. */\nexport function readSigningFromGitconfig(path: string): SigningSettings {\n let text: string;\n try {\n text = readFileSync(path, \"utf8\");\n } catch {\n return {};\n }\n return readSigning(text);\n}\n\nexport type SigningState = \"ok\" | \"disabled\" | \"incomplete\" | \"misconfigured\";\n\nexport interface SigningAudit {\n state: SigningState;\n /**\n * One sentence per problem, each naming what is wrong. Several can apply at once, because\n * \"signing is off\" and \"the key points at the host\" are independent facts.\n */\n problems: string[];\n /** Whether this store needs the explicit fix at all. */\n needsFix: boolean;\n}\n\n/**\n * Is `path` inside `root`?\n *\n * Compared by resolved **segments**, not by string prefix: `/a/agent-other` is not inside `/a/agent`,\n * and a `startsWith` would say it is. That distinction is not hypothetical \u2014 it is the exact bug\n * issue #25 had to fix for its sibling-clone case.\n */\nexport function isInside(root: string, path: string): boolean {\n const from = resolve(root);\n const to = resolve(path);\n if (to === from) return true;\n return to.startsWith(from.endsWith(sep) ? from : from + sep);\n}\n\n/**\n * Audit the signing configuration.\n *\n * Ordering is deliberate: **misconfiguration is reported before absence**. A `user.signingkey`\n * pointing at the host's key means the agent is already configured to sign as the user, and telling\n * the reader only that \"signing is off\" would hide the more serious of the two facts.\n */\nexport function auditSigning(settings: SigningSettings, root: string): SigningAudit {\n const problems: string[] = [];\n const keyPath = signingKeyPath(root);\n const signersPath = allowedSignersPath(root);\n\n // --- What is present but wrong. Checked first, because it is the dangerous half. --------------\n let misconfigured = false;\n if (settings.signingKey !== undefined && !isInside(root, settings.signingKey)) {\n misconfigured = true;\n problems.push(\n `user.signingkey is ${settings.signingKey}, which is OUTSIDE the agent root ${root}. ` +\n `ssh takes a key path rather than key contents, so this would sign commits as whoever owns ` +\n `that key \u2014 a leak of the user's identity, not a formatting problem.`,\n );\n }\n if (settings.allowedSignersFile !== undefined && !isInside(root, settings.allowedSignersFile)) {\n misconfigured = true;\n problems.push(\n `gpg.ssh.allowedSignersFile is ${settings.allowedSignersFile}, which is outside the agent ` +\n `root ${root}. Verification would consult the host's trust list instead of the agent's.`,\n );\n }\n if (settings.format !== undefined && settings.format !== \"ssh\") {\n misconfigured = true;\n problems.push(\n `gpg.format is ${JSON.stringify(settings.format)}, not \"ssh\". The default is openpgp, which ` +\n `looks for a gpg installation the session does not need and may not have.`,\n );\n }\n if (misconfigured) return { state: \"misconfigured\", problems, needsFix: true };\n\n // --- What is switched off. ---------------------------------------------------------------------\n //\n // `undefined` counts as off, not as \"unknown\": git's own default for `commit.gpgsign` is false, so\n // an unset key produces unsigned commits exactly as `= false` does. Treating unset as neutral\n // would report a store that never signs as healthy.\n const disabled =\n !truthy(settings.commitGpgsign) || !truthy(settings.tagGpgsign);\n if (disabled) {\n if (!truthy(settings.commitGpgsign)) problems.push(\"commit.gpgsign is not true\");\n if (!truthy(settings.tagGpgsign)) problems.push(\"tag.gpgsign is not true\");\n return { state: \"disabled\", problems, needsFix: true };\n }\n\n // --- What is missing. --------------------------------------------------------------------------\n if (settings.format === undefined) problems.push(\"gpg.format is unset (must be ssh)\");\n if (settings.signingKey === undefined) problems.push(`user.signingkey is unset (must be ${keyPath})`);\n if (settings.allowedSignersFile === undefined) {\n problems.push(`gpg.ssh.allowedSignersFile is unset (must be ${signersPath})`);\n }\n if (problems.length > 0) return { state: \"incomplete\", problems, needsFix: true };\n\n return { state: \"ok\", problems: [], needsFix: false };\n}\n\n/** `true` only for git's affirmative spellings. `1`/`yes`/`on` are git's too, so they are honoured. */\nfunction truthy(value: string | undefined): boolean {\n if (value === undefined) return false;\n return [\"true\", \"1\", \"yes\", \"on\"].includes(value.toLowerCase());\n}\n\n/**\n * Does the material signing needs actually exist on disk?\n *\n * Separate from the audit because it answers a different question: an audit of a correctly-written\n * gitconfig that points at a key which is not there is *incomplete config* by text and *missing\n * material* in fact, and the repairs differ. The caller has both facts and can say which.\n */\nexport function signingMaterial(root: string): { key: boolean; allowedSigners: boolean } {\n return { key: existsSync(signingKeyPath(root)), allowedSigners: existsSync(allowedSignersPath(root)) };\n}\n\n/**\n * The exact commands that bring a store into compliance.\n *\n * Returned as data rather than executed. The agent root is user-maintained in `external` ownership,\n * so Pi Central reports and instructs; a person runs these. That ordering is the requirement, not a\n * convenience \u2014 see the module comment.\n */\nexport function signingFixCommands(root: string): string[] {\n return [\n `git config --file ${join(root, \"gitconfig\")} commit.gpgsign true`,\n `git config --file ${join(root, \"gitconfig\")} tag.gpgsign true`,\n `git config --file ${join(root, \"gitconfig\")} gpg.format ssh`,\n `git config --file ${join(root, \"gitconfig\")} user.signingkey ${signingKeyPath(root)}`,\n `git config --file ${join(root, \"gitconfig\")} gpg.ssh.allowedSignersFile ${allowedSignersPath(root)}`,\n ];\n}\n", "/**\n * The bespoke policy layer: an SBPL fragment appended after Safehouse's generated rules\n * (ADR-0010, SANDBOX.md).\n *\n * Why a fragment rather than editing the generated policy: `agent.sb` is regenerated by\n * Safehouse, so hand-edits are lost on upgrade. Appended rules are emitted **after** the\n * generated ones, and SBPL is **last-match-wins**, so a deny here overrides any broader allow\n * the base policy contains. That property is the whole mechanism.\n *\n * Why it is needed at all: Safehouse's denial is by *default*, not by *choice*. Its generated\n * policy deliberately grants things a coding agent commonly needs, and some of those are the\n * host user's credentials:\n *\n * (allow file-read* (home-prefix \"/.gitconfig\")) ;; F12 route to the host token\n * (allow file-read* file-write* (home-subpath \"/.config/gh\")) ;; the host's gh token\n * (allow file-read* file-write* (home-subpath \"/.pi\")) ;; C6: all transcripts\n *\n * A session that can use those is not running as the agent; it is running as the user.\n *\n * ## What is denied, and what deliberately is not\n *\n * **Identity** credentials are denied \u2014 a leak there is the *user's* identity, used as the\n * agent: the host gitconfig, the host gh config, the user's ssh-agent, and the user's whole\n * `~/.ssh` tree (the directory, not a list of key names \u2014 a name list fails open).\n *\n * **Infrastructure** credentials are not. Provider keys (the LLM the agent runs on) are a\n * capability the user must grant: there is no \"agent's own litellm key\", so denying it would\n * not tighten isolation, only stop the agent thinking. `~/.pi/agent/auth.json` is\n * **allowed** \u2014 it holds `!command` indirections, not secrets, and Pi reads it during startup,\n * where a deny produces EPERM and Pi exits 1 (C12). Amendment 1 to ADR-0010 has the full\n * account of that bug.\n *\n * ## Two rules that keep this file honest\n *\n * 1. **Host paths are derived from `HOME_DIR`**, which the generated policy defines before\n * appended rules are emitted, so no user's home is baked in.\n * 2. **Denials are narrow.** `~/.config` and `~/.local/share` are granted only as *literals*\n * for traversal, and tools rely on walking them; the deny targets the credential subpath,\n * not the root.\n */\n\nexport interface FragmentOptions {\n /**\n * The agent's own credential tree.\n *\n * Required in practice: without it the agent cannot read its own `gitconfig` or `gh` config,\n * while `GH_CONFIG_DIR`/`GIT_CONFIG_GLOBAL` point straight at them \u2014 so the session would be\n * configured to use credentials it is not permitted to read.\n */\n agentRoot?: string;\n /**\n * Grant the keychain access Pi needs to resolve provider keys (C13).\n *\n * `auth.json` resolves each key with a service-only lookup such as\n * `!security find-generic-password -ws 'litellm'`, which needs `com.apple.SecurityServer`\n * to read **any** keychain, and the user-wide search list to find the agent keychain.\n *\n * Safe because the grant is bounded: the login and System keychain files are denied, and\n * Safehouse grants no keychain access at all by default (`keychain integration: not\n * included`), so this is additive rather than a hole punched in an existing protection.\n *\n * Off means a session will start and then be unable to reach a model \u2014 correct only for a\n * deliberately offline session.\n */\n keychain?: boolean;\n /** The agent keychain to grant, when `keychain` is on. */\n keychainPath?: string;\n}\n\n/**\n * Ancestor directory literals for a path, mirroring what Safehouse emits for the workdir.\n *\n * Why this is necessary: path resolution in SBPL needs read access to **every** component, so\n * a `(subpath <agentRoot>)` grant alone is not enough. Safehouse grants ancestor literals for\n * the *workdir* and says why:\n *\n * Agents call readdir() on every ancestor of the working directory during startup. If only\n * file-read-metadata is granted, the agent cannot list directory contents.\n *\n * Our agent root is not the workdir, so it gets no such grant: the base policy covers `~/.config`\n * as a literal but not `~/.config/pi-central/agents`. Without these literals a session would\n * start normally, then be unable to reach the very credentials it is configured with \u2014 the same\n * shape of failure as the `auth.json` deny (C12).\n *\n * `literal` rather than `subpath` is deliberate, and matches Safehouse: it grants the directory\n * entry itself (listing immediate children), not recursive access to what is under it.\n */\nexport function ancestorLiterals(path: string): string[] {\n if (!path.startsWith(\"/\")) throw new Error(`path must be absolute: ${JSON.stringify(path)}`);\n const parts = path.split(\"/\").filter((part) => part !== \"\");\n const out = [' (literal \"/\")'];\n let current = \"\";\n for (const part of parts) {\n current += `/${part}`;\n out.push(` (literal ${JSON.stringify(current)})`);\n }\n return out;\n}\n\n/** Paths derived from `HOME_DIR`, so the fragment is portable between machines. */\nconst HOST_DENIES: { comment: string[]; rules: string[] }[] = [\n {\n comment: [\n \"Host git configuration (closes the credential-helper route)\",\n \"The host's global gitconfig may set credential.helper = !gh auth git-credential,\",\n \"which resolves a token from the login keychain - a path no file deny on ~/.ssh\",\n \"would stop. The agent uses its own GIT_CONFIG_GLOBAL instead (ADR-0010 \u00A73).\",\n ],\n rules: [\n \"(deny file-read*\",\n ' (home-prefix \"/.gitconfig\")',\n ' (home-subpath \"/.config/git\")',\n ' (home-literal \"/.gitattributes\")',\n \")\",\n ],\n },\n {\n comment: [\n \"Host GitHub CLI config\",\n \"On macOS gh keeps its token in the keychain, but ~/.config/gh may still hold config\",\n \"for the user's own account, and the write grant in particular was a config-tampering\",\n \"surface. The agent uses its own GH_CONFIG_DIR. ~/.config stays traversable so tool\",\n \"discovery (mise, gh, git) still works.\",\n ],\n rules: [\"(deny file-read* file-write*\", ' (home-subpath \"/.config/gh\")', \")\", \"(allow file-read-metadata\", ' (home-literal \"/.config\")', \")\"],\n },\n {\n comment: [\n \"The user's SSH world, closed wholesale\",\n \"\",\n \"This was three NAME-based regexes (id_*, *_rsa, *_ed25519) plus a deny on\",\n \"~/.ssh/agent, and the list failed twice over - it was REDUNDANT where it looked\",\n \"essential and DANGEROUS where it looked harmless:\",\n \"\",\n \" - Redundant: the base policy grants only ~/.ssh/config and ~/.ssh/known_hosts, as\",\n \" literals. Every other path under ~/.ssh was already denied by `(deny default)`.\",\n \" - Dangerous: the base workdir grant is\",\n \" `(allow file-read* file-write* (subpath HOME))` and is emitted BEFORE this fragment.\",\n \" For a session whose cwd is $HOME that grant covers all of ~/.ssh, so the\",\n \" enumeration became the only thing there - and it did not name cupserver, jenkins,\",\n \" or sven1103-agent-strongbox, which are real private keys, nor ~/.ssh/config\",\n \" itself, which maps 58 hosts including internal infrastructure.\",\n \"\",\n \"A list of key NAMES is not a boundary: it fails open on the next key anyone creates\",\n \"under a name nobody predicted. The directory is the unit, so the directory is denied.\",\n \"\",\n \"Why this is safe, measured rather than assumed: the agent does not read this\",\n \"directory at all. Its git-over-SSH runs `ssh -F <agentRoot>/ssh/config` through\",\n \"core.sshCommand, and `-F` REPLACES the per-user config rather than adding to it.\",\n \"With HOME pointed at an empty directory - no ~/.ssh whatsoever - the agent still\",\n \"authenticated to GitHub and produced a signature git verified as\",\n \"`Good \\\"git\\\" signature for sven1103-agent`.\",\n \"\",\n \"The second block is deliberate duplication, not tidiness. SBPL resolves a conflict\",\n \"by last-matching rule, and Safehouse's own profiles note that a more specific filter\",\n \"can beat a broader one regardless of order (`10-system-runtime.sb`). The base emits\",\n \"`(home-literal \\\"/.ssh/config\\\")` - a literal, strictly more specific than a subpath -\",\n \"so the base's own paths are named again at their own specificity, and a deny wins\",\n \"under either reading.\",\n ],\n rules: [\n \"(deny file-read* file-write* file-read-metadata\",\n ' (home-subpath \"/.ssh\")',\n \")\",\n \"(deny file-read* file-write* file-read-metadata\",\n ' (home-literal \"/.ssh\")',\n ' (home-literal \"/.ssh/config\")',\n ' (home-literal \"/.ssh/known_hosts\")',\n ' (home-literal \"/.ssh/known_hosts.old\")',\n ' (home-literal \"/.ssh/allowed_signers\")',\n ' (home-subpath \"/.ssh/config.d\")',\n \") ;; the base policy's own narrower grants, restated at matching specificity\",\n \"\",\n \";; The ssh-agent needs a NETWORK deny as well, since a file deny does not cover\",\n \";; connect() on a unix socket. The file half is subsumed by the wholesale deny above.\",\n \";; A session with agent access can sign as the user with any loaded key WITHOUT\",\n \";; reading a key file; the agent uses its own key with IdentitiesOnly=yes and needs\",\n \";; no agent (ADR-0010 \u00A74). Defence in depth: the spawn environment is sanitized too\",\n \";; (C11). Both layers are applied, since either alone is a single point of failure.\",\n \"(deny network-outbound\",\n ' (remote unix-socket (path-regex #\"^/private/tmp/com\\\\.apple\\\\.launchd\\\\.[^/]+/Listeners$\"))',\n ' (remote unix-socket (path-regex #\"^/tmp/com\\\\.apple\\\\.launchd\\\\.[^/]+/Listeners$\"))',\n ' (remote unix-socket (path-regex #\"^/private/var/run/com\\\\.apple\\\\.launchd\\\\.[^/]+/Listeners$\"))',\n ' (remote unix-socket (path-regex #\"^/var/run/com\\\\.apple\\\\.launchd\\\\.[^/]+/Listeners$\"))',\n ' (remote unix-socket (path-regex (string-append \"^\" HOME_DIR \"/\\\\\\\\.ssh/agent(/.*)?$\")))',\n \")\",\n ],\n },\n];\n\n/**\n * Render the fragment.\n *\n * Deterministic: same options produce byte-identical output, so a generated file can be\n * diffed to detect drift (ADR-0010 \u00A75, the drift badge).\n */\nexport function renderPolicyFragment(options: FragmentOptions = {}): string {\n const { agentRoot, keychain = false, keychainPath } = options;\n if (agentRoot !== undefined && !agentRoot.startsWith(\"/\")) {\n throw new Error(`agent root must be an absolute path: ${JSON.stringify(agentRoot)}`);\n }\n if (keychain && (keychainPath === undefined || !keychainPath.startsWith(\"/\"))) {\n throw new Error(\"keychain access requires an absolute keychainPath\");\n }\n\n const out: string[] = [\n \";; ---------------------------------------------------------------------------\",\n \";; Pi Central \u2014 appended policy fragment (ADR-0010).\",\n \";;\",\n \";; Generated; do not edit by hand. Deterministic, so drift is diffable.\",\n \";; Appended AFTER Safehouse's generated rules: SBPL is last-match-wins, so these\",\n \";; denies override the base policy's broader allows.\",\n \";;\",\n \";; Purpose: the session runs as the AGENT, not as the user.\",\n \";;\",\n \";; Identity credentials are closed. Provider credentials are NOT denied, and that is\",\n \";; deliberate: the agent runs on the user's model access, so there is no agent-owned\",\n \";; substitute. Denying it would stop the agent thinking, not tighten isolation.\",\n \";; See ADR-0010 Amendment 1.\",\n \";; ---------------------------------------------------------------------------\",\n \"\",\n ];\n\n for (const block of HOST_DENIES) {\n out.push(...block.comment.map((line) => (line === \"\" ? \";;\" : `;; ${line}`)));\n out.push(...block.rules, \"\");\n }\n\n // --- The agent's own credentials -------------------------------------------------------\n // Placed after the host denies: the same `gitconfig`/`gh` names exist under both roots, and\n // the agent's copy must win. Without this the session is configured (via GH_CONFIG_DIR and\n // GIT_CONFIG_GLOBAL) to use files it is not permitted to read.\n if (agentRoot !== undefined) {\n out.push(\n \";; --- The agent's OWN credentials -----------------------------------------------\",\n \";; Read-write: gh writes cache/state under GH_CONFIG_DIR, and git may update the\",\n \";; agent's config. This is the point of the fragment - the denies above only help if\",\n \";; the agent can still reach what it legitimately owns.\",\n \";;\",\n \";; Ancestor literals are required, not decorative: SBPL needs read access to every path\",\n \";; component, and Safehouse only emits ancestor grants for the workdir. Without them the\",\n \";; agent cannot traverse into its own tree.\",\n \"(allow file-read*\",\n ...ancestorLiterals(agentRoot),\n \")\",\n \"(allow file-read* file-write*\",\n ` (subpath ${JSON.stringify(agentRoot)})`,\n \")\",\n \"\",\n );\n }\n\n // --- Provider-key resolution (C13) -----------------------------------------------------\n if (keychain) {\n out.push(\n \";; --- Provider key resolution (C13) ----------------------------------------------\",\n \";; auth.json resolves each provider key with a service-only lookup, e.g.\",\n \";; !security find-generic-password -ws 'litellm'\",\n \";; That needs SecurityServer to read ANY keychain, and the user-wide search list to\",\n \";; find the agent keychain. Neither is granted by Safehouse by default.\",\n \";;\",\n \";; The grant is bounded by denying the login and System keychain FILES: the keychain is\",\n \";; all-or-nothing via IPC, so least privilege (the agent's own keychain) is the\",\n \";; control, not concealment.\",\n \"(allow mach-lookup\",\n ' (global-name \"com.apple.SecurityServer\")',\n \")\",\n \"(deny file-read* file-write*\",\n ' (home-subpath \"/Library/Keychains\")',\n ' (subpath \"/Library/Keychains\")',\n \")\",\n \"(allow file-read* file-write*\",\n ` (literal ${JSON.stringify(keychainPath)})`,\n \")\",\n \";; The search list lives in com.apple.security.plist. Without read access, `security`\",\n \";; silently falls back to the built-in [login, System] list, does NOT see the agent\",\n \";; keychain, and every service-only lookup fails with no useful error. The file holds\",\n \";; only the search list - no secret.\",\n \"(allow file-read*\",\n ' (home-literal \"/Library/Preferences/com.apple.security.plist\")',\n \")\",\n \"\",\n );\n }\n\n return out.join(\"\\n\");\n}\n\n/** Write the fragment into the state root, returning its path. Deterministic content. */\nexport function fragmentPath(stateRoot: string): string {\n return `${stateRoot}/policy/fragment.sb`;\n}\n", "/**\n * Reading a session's working directory \u2014 confined to it (ADR-0007, issue #8).\n *\n * ## Who this runs as, and why that is not a gap\n *\n * These functions use plain `fs` in the **orchestrator** process, at the user's UID. There is no\n * `safehouse` invocation, no sidecar, and no round trip through the session's RPC `bash`. ADR-0007\n * decides this: Safehouse confines the *agent* processes, which is what constrains model-generated\n * actions. The file browser serves the *person*, and the person's own UID is already the correct\n * privilege level for reading their own files. Routing it through the agent's sandbox would be\n * slower, more complex, and would conflate two actors.\n *\n * The consequence, stated because the UI must not misrepresent it: **this can read files the agent\n * cannot.** The browser shows \"your files, as you\", never \"what the agent can see\".\n *\n * ## Containment is the whole feature\n *\n * Because the privilege is the user's, the only thing standing between a path from the UI and the\n * rest of the disk is the check below. ADR-0007's guardrails are therefore load-bearing, not\n * hygiene:\n *\n * 1. **Canonicalize with `realpath`, then verify containment.** Never compare string prefixes:\n * that fails on `..`, on symlinks, and on case-insensitive filesystems.\n * 2. **Resolve symlinks *before* the containment check**, so a link pointing out of the tree is\n * refused rather than followed.\n * 3. **Cap reads, and refuse binary content** rather than rendering replacement characters.\n * 4. **Report the OS error code**, never a guess. A permission failure was once reported as a\n * missing directory elsewhere in this project (C14's neighbourhood), and the two need\n * different fixes.\n *\n * ## Two roots, because a symlinked cwd is ordinary on macOS\n *\n * `root` is what the caller asked for (`/tmp/x`), `realRoot` is what the OS says it is\n * (`/private/tmp/x` on macOS). Containment is checked against `realRoot`, because the *resolved*\n * candidate comes back in OS form and comparing it against the un-resolved root would refuse a\n * perfectly good path. Both are derived here so no caller has to remember.\n */\nimport { lstatSync, readdirSync, readFileSync, realpathSync, statSync } from \"node:fs\";\nimport { isAbsolute, join, resolve, sep } from \"node:path\";\n// Shared with the UI so a size in the tree and a size in a refusal cannot disagree (protocol/bytes.ts).\nimport { formatBytes } from \"@pi-central/protocol\";\nexport { formatBytes };\n\n/**\n * The largest file this will read, in bytes.\n *\n * 256 KB, matching `registry.ts`'s transcript head cap \u2014 the same figure and the same reason: it is\n * large enough for any source file a person browses and small enough that a stray log or archive\n * cannot be pulled into memory. A file above it is **refused, not clipped**: the issue requires that\n * an oversized file \"says so rather than truncating silently\", and a partial view of a file is a\n * wrong answer to \"what is in this file\".\n */\nexport const MAX_FILE_BYTES = 256 * 1024;\n\n/**\n * How much of a file to inspect when deciding whether it is binary.\n *\n * A NUL byte in the first 8 KB is git's own heuristic and is what makes this cheap: it needs one\n * `read` of a constant size, not the whole file.\n */\nexport const BINARY_SNIFF_BYTES = 8 * 1024;\n\n/** Why a path or read was refused. A closed set, so callers must handle each rather than print it. */\nexport type FileFailure =\n /** The path was absolute. The UI only speaks relative paths, so this is always a mistake. */\n | \"absolute-path\"\n /**\n * Not a usable path at all \u2014 a NUL byte, or a non-string. Distinct from `escapes-cwd`: a NUL is\n * malformed input rather than a boundary violation, and reporting it as \"outside the working\n * directory\" would name a place the path never reached.\n */\n | \"invalid-path\"\n /** A `..` segment or a symlink took the resolved path outside the session's cwd. */\n | \"escapes-cwd\"\n /** The path exists but is not a directory, and a listing was requested. */\n | \"not-a-directory\"\n /** A read was requested for a directory. */\n | \"is-directory\"\n /** Larger than {@link MAX_FILE_BYTES}; refused rather than truncated. */\n | \"too-large\"\n /** Not text: a NUL byte, or the bytes are not valid UTF-8. */\n | \"binary\"\n | \"not-found\"\n /** The OS denied access. `code` carries its errno, which is the useful part. */\n | \"permission\"\n /** Anything else, with the OS code preserved. */\n | \"unreadable\";\n\nexport interface FileError {\n reason: FileFailure;\n /** The raw errno (`EACCES`, `EPERM`, `ENOENT`, \u2026) when the OS produced one. */\n code?: string | undefined;\n /** A sentence for the UI. Names the code when there is one \u2014 never a substituted guess. */\n message: string;\n}\n\nexport interface DirEntry {\n name: string;\n kind: \"file\" | \"directory\" | \"symlink\" | \"other\";\n /**\n * For a symlink, what it points AT (`stat`, following the link), or `missing` when it dangles.\n *\n * The `kind` stays `symlink` \u2014 that is what the entry *is*, and collapsing a link into its target\n * would hide the fact that following it may leave the root. But a UI needs to know whether a link\n * is enterable, and without this it would treat every link as a file: clicking a `docs -> ../docs`\n * link would send a read for a directory and surface \"That is a directory\", which is true and\n * useless. Established with `stat` (following) alongside `lstat` (not), so this is a *description*\n * and not a safety decision \u2014 `resolveWithin` still decides that when the path is actually entered.\n */\n linkTarget?: \"file\" | \"directory\" | \"other\" | \"missing\" | undefined;\n /** Size in bytes, for regular files only. */\n size?: number | undefined;\n /** A dotfile. Returned anyway when `includeHidden` is set; the flag is why. */\n hidden: boolean;\n /**\n * The entry could not be `lstat`ed.\n *\n * Reported rather than dropped: an entry that exists but cannot be inspected is a fact the user\n * needs, and silently omitting it would make the tree disagree with `ls` for no stated reason.\n */\n unreadable?: boolean | undefined;\n}\n\nexport interface DirListing {\n /** The directory, relative to the session cwd and normalized. `\"\"` means the cwd itself. */\n path: string;\n entries: DirEntry[];\n /** True when the directory held more entries than `limit`. */\n truncated: boolean;\n}\n\nexport interface FileContent {\n path: string;\n text: string;\n /** Bytes read. Equal to the file's size, because an oversized file is refused, not clipped. */\n bytes: number;\n}\n\nexport type Result<T> = { ok: true; value: T } | { ok: false; error: FileError };\n\n/** The default cap on entries returned for one directory, so a huge tree cannot stall the tab. */\nexport const DEFAULT_ENTRY_LIMIT = 2000;\n\n/**\n * Is `child` the same path as `parent`, or inside it?\n *\n * Compared on **path segments**, not string prefixes: `contains(\"/a/b\", \"/a/bc\")` is false, which a\n * bare `startsWith` gets wrong. ADR-0007 forbids prefix comparison for exactly this reason, and\n * this repository has already been bitten by the prefix form once (`own-installation.ts`, issue #25,\n * where `pi-central-dev` looked like it was inside `pi-central`).\n *\n * Both arguments are expected to be resolved (no `..`, no symlinks) \u2014 that is the caller's job, and\n * `resolveWithin` is the caller.\n */\nexport function containsPath(parent: string, child: string): boolean {\n if (parent === child) return true;\n const withSep = parent.endsWith(sep) ? parent : parent + sep;\n return child.startsWith(withSep);\n}\n\n/** Turn an errno into a {@link FileError}, preserving the code. */\nfunction fromErrno(error: unknown, what: string, relPath: string): FileError {\n const code = (error as NodeJS.ErrnoException | undefined)?.code;\n const label = relPath === \"\" ? \"the working directory\" : `\"${relPath}\"`;\n switch (code) {\n case \"ENOENT\":\n return { reason: \"not-found\", code, message: `${what} ${label}: no such file or directory (ENOENT)` };\n case \"EACCES\":\n case \"EPERM\":\n return { reason: \"permission\", code, message: `${what} ${label}: permission denied (${code})` };\n case \"ENOTDIR\":\n return { reason: \"not-a-directory\", code, message: `${what} ${label}: not a directory (ENOTDIR)` };\n case \"EISDIR\":\n return { reason: \"is-directory\", code, message: `${what} ${label}: is a directory (EISDIR)` };\n default:\n // No code, or one not worth special-casing. The code is still reported when present; what is\n // forbidden is inventing one.\n return {\n reason: \"unreadable\",\n code,\n message: code === undefined ? `${what} ${label}: ${String(error)}` : `${what} ${label}: ${String(error)} (${code})`,\n };\n }\n}\n\n/**\n * Resolve a UI-supplied relative path against the session's cwd, refusing anything outside it.\n *\n * The order is the security property, so it is not rearranged for tidiness:\n *\n * 1. Reject absolute paths and NUL bytes outright.\n * 2. `resolve()` the candidate \u2014 this is what collapses `..`, so `../x` becomes a sibling that\n * step 3 can then reject.\n * 3. Check containment against the root *lexically*, which catches `..` before any I/O.\n * 4. `realpathSync()` \u2014 this is where symlinks are followed, and it fails if nothing is there.\n * 5. Check containment **again** against the resolved root. A symlink that pointed outside is a\n * real path outside, so it is refused here.\n *\n * Steps 3 and 5 overlap for a path that EXISTS \u2014 `realpath` resolves `..` too, so 5 would refuse an\n * escaping path with or without 3. 3 is kept for two reasons, both about the ANSWER rather than the\n * verdict:\n *\n * - It refuses without touching the disk. `../does-not-exist` would otherwise report `not-found`,\n * which describes the child rather than the escape and sends the reader to the wrong fix. This\n * is the same failure shape as a permission error reported as a missing directory.\n * - It runs before `realpathSync` on the ROOT, so a cwd that has been deleted still reports as a\n * missing working directory rather than as a path that escaped.\n */\nexport function resolveWithin(root: string, relative: string): Result<{ path: string; relPath: string }> {\n if (typeof relative !== \"string\" || relative.includes(\"\\0\")) {\n // `invalid-path`, not `escapes-cwd`: a NUL byte is malformed input, not a boundary violation.\n // Calling it an escape names a place the path never reached and sends the reader to the wrong\n // fix \u2014 the same misreporting as a permission failure shown as a missing file.\n return {\n ok: false,\n error: {\n reason: \"invalid-path\",\n message: \"the path contains a NUL byte, which no filesystem path can\",\n },\n };\n }\n if (isAbsolute(relative)) {\n // Refused rather than interpreted. The UI deals only in paths relative to the session's cwd, so\n // an absolute one means a caller is confused \u2014 and \"it happened to be inside the root\" would be\n // a coincidence, not a guarantee.\n return {\n ok: false,\n error: {\n reason: \"absolute-path\",\n message: `\"${relative}\" is an absolute path; paths are relative to the session's working directory`,\n },\n };\n }\n\n const relPath = normalizeRelative(relative);\n\n // The root must itself resolve; a session whose cwd has been deleted is a distinct failure from a\n // bad child path, so it is reported as one.\n let realRoot: string;\n try {\n realRoot = realpathSync(root);\n } catch (error) {\n return { ok: false, error: fromErrno(error, \"the session's working directory is unavailable:\", \"\") };\n }\n\n const candidate = resolve(realRoot, relPath);\n // Lexical containment first: catches `..` with no filesystem access at all.\n if (!containsPath(realRoot, candidate)) {\n return {\n ok: false,\n error: {\n reason: \"escapes-cwd\",\n message: `\"${relative}\" resolves outside the session's working directory`,\n },\n };\n }\n\n let real: string;\n try {\n real = realpathSync(candidate);\n } catch (error) {\n return { ok: false, error: fromErrno(error, \"cannot open\", relPath) };\n }\n\n // Containment again, on the RESOLVED path. This is the symlink check: a link inside the tree that\n // points out of it resolves to a target outside, and only here is that visible.\n if (!containsPath(realRoot, real)) {\n return {\n ok: false,\n error: {\n reason: \"escapes-cwd\",\n message:\n `\"${relative}\" is a symlink whose target lies outside the session's working directory, ` +\n `so it is refused rather than followed`,\n },\n };\n }\n\n return { ok: true, value: { path: real, relPath } };\n}\n\n/**\n * Normalize a relative path for display and comparison.\n *\n * `\"\"` and `\".\"` both mean the cwd itself, so they collapse to `\"\"` \u2014 one spelling, so the UI's\n * \"which directory am I in\" comparison cannot fail on a cosmetic difference. Separators are\n * normalized to `/` so a relative path is comparable regardless of platform.\n */\nexport function normalizeRelative(relative: string): string {\n const parts: string[] = [];\n for (const part of relative.split(/[/\\\\]+/)) {\n if (part === \"\" || part === \".\") continue;\n // `..` is deliberately NOT collapsed here. `resolve()` in `resolveWithin` is what handles it,\n // and collapsing it by hand would hide an escape from the containment check rather than expose\n // it. Keeping the segment means `../x` stays a path that step 3 refuses.\n parts.push(part);\n }\n return parts.join(\"/\");\n}\n\n/** Hidden means dot-prefixed, which is the convention on every platform this runs on. */\nexport function isHidden(name: string): boolean {\n return name.startsWith(\".\");\n}\n\n/**\n * List one directory, confined to `root`.\n *\n * Not recursive: directories are expanded lazily by the caller, one level per request. A recursive\n * walk would read the whole tree on every open, which is both slow and a much larger read than the\n * user asked for.\n */\nexport function listDirectory(\n root: string,\n relative: string,\n options: { includeHidden?: boolean; limit?: number } = {},\n): Result<DirListing> {\n const resolvedPath = resolveWithin(root, relative);\n if (!resolvedPath.ok) return resolvedPath;\n const { path, relPath } = resolvedPath.value;\n\n const limit = options.limit ?? DEFAULT_ENTRY_LIMIT;\n const includeHidden = options.includeHidden ?? false;\n\n let names: string[];\n try {\n names = readdirSync(path);\n } catch (error) {\n return { ok: false, error: fromErrno(error, \"cannot list\", relPath) };\n }\n\n const entries: DirEntry[] = [];\n let truncated = false;\n\n for (const name of names) {\n if (!includeHidden && isHidden(name)) continue;\n if (entries.length >= limit) {\n truncated = true;\n break;\n }\n entries.push(describeEntry(join(path, name), name));\n }\n\n // Directories first, then by name. Case-insensitive so `README` and `readme.md` sort together,\n // which is what a person expects from a file tree even on a case-sensitive volume.\n entries.sort((a, b) => {\n const aDir = a.kind === \"directory\" ? 0 : 1;\n const bDir = b.kind === \"directory\" ? 0 : 1;\n if (aDir !== bDir) return aDir - bDir;\n return a.name.localeCompare(b.name, undefined, { sensitivity: \"base\" });\n });\n\n return { ok: true, value: { path: relPath, entries, truncated } };\n}\n\n/** `lstat`, so a symlink is reported as a symlink rather than as whatever it points at. */\nfunction describeEntry(fullPath: string, name: string): DirEntry {\n try {\n const stats = lstatSync(fullPath);\n if (stats.isSymbolicLink()) {\n // Whether it is safe to enter is decided when it IS entered (`resolveWithin`), not here \u2014 a\n // link pointing inside the tree is perfectly usable, and refusing it at listing time would\n // hide a legitimate path. What IS decided here is what the link points at, so the UI can tell\n // an enterable link from a file-like one and from a broken one.\n return { name, kind: \"symlink\", linkTarget: linkTargetOf(fullPath), hidden: isHidden(name) };\n }\n if (stats.isDirectory()) return { name, kind: \"directory\", hidden: isHidden(name) };\n if (stats.isFile()) return { name, kind: \"file\", size: stats.size, hidden: isHidden(name) };\n return { name, kind: \"other\", hidden: isHidden(name) };\n } catch {\n return { name, kind: \"other\", hidden: isHidden(name), unreadable: true };\n }\n}\n\n/**\n * What a symlink points at, following it.\n *\n * `stat`, not `lstat` \u2014 that is the whole point. A dangling link reports `missing` rather than\n * throwing: a broken link is a fact worth showing, not a read error, and the entry still exists.\n */\nfunction linkTargetOf(fullPath: string): \"file\" | \"directory\" | \"other\" | \"missing\" {\n try {\n const stats = statSync(fullPath);\n if (stats.isDirectory()) return \"directory\";\n if (stats.isFile()) return \"file\";\n return \"other\";\n } catch {\n return \"missing\";\n }\n}\n\n/**\n * Read a text file, confined to `root`.\n *\n * Three refusals, each of which the issue requires to be *stated* rather than worked around:\n *\n * - **Too large** \u2014 `too-large`, with the size. Not a clipped read: see {@link MAX_FILE_BYTES}.\n * - **Binary** \u2014 `binary`. Detected two ways, because either alone misses a class: a NUL byte in\n * the head (git's heuristic), and a failing strict UTF-8 decode. The decode is what prevents the\n * replacement-character rendering the issue names, since a file that is not valid UTF-8 would\n * otherwise be shown as a corrupted version of itself.\n * - **A directory** \u2014 `is-directory`, rather than an EISDIR surfacing as a generic failure.\n */\nexport function readFileText(root: string, relative: string): Result<FileContent> {\n const resolvedPath = resolveWithin(root, relative);\n if (!resolvedPath.ok) return resolvedPath;\n const { path, relPath } = resolvedPath.value;\n\n let stats;\n try {\n stats = lstatSync(path);\n } catch (error) {\n return { ok: false, error: fromErrno(error, \"cannot open\", relPath) };\n }\n if (stats.isDirectory()) {\n return { ok: false, error: { reason: \"is-directory\", message: `\"${relPath}\" is a directory` } };\n }\n if (stats.size > MAX_FILE_BYTES) {\n return {\n ok: false,\n error: {\n reason: \"too-large\",\n message:\n `\"${relPath}\" is ${formatBytes(stats.size)}, above the ${formatBytes(MAX_FILE_BYTES)} limit ` +\n `for reading a file here, so it was not read`,\n },\n };\n }\n\n let buffer: Buffer;\n try {\n buffer = readFileSync(path);\n } catch (error) {\n return { ok: false, error: fromErrno(error, \"cannot read\", relPath) };\n }\n\n const sniff = buffer.subarray(0, BINARY_SNIFF_BYTES);\n if (sniff.includes(0)) {\n return {\n ok: false,\n error: {\n reason: \"binary\",\n message: `\"${relPath}\" is a binary file (it contains NUL bytes), so it was not rendered as text`,\n },\n };\n }\n\n let text: string;\n try {\n // `fatal` is the point: a lenient decode would produce U+FFFD replacement characters, which is\n // precisely the \"rendered as replacement characters\" failure the issue calls out.\n text = new TextDecoder(\"utf-8\", { fatal: true }).decode(buffer);\n } catch {\n return {\n ok: false,\n error: {\n reason: \"binary\",\n message: `\"${relPath}\" is not valid UTF-8, so it was not rendered as text`,\n },\n };\n }\n\n return { ok: true, value: { path: relPath, text, bytes: buffer.byteLength } };\n}\n\n", "/**\n * The record\u2192state interpretation, as a pure function.\n *\n * This lives outside the supervisor on purpose. The rules here are the ones that are silent\n * and expensive when wrong \u2014 above all, **completion is `agent_settled`, never `agent_end`**\n * (C8) \u2014 so they are worth testing directly rather than through a spawned process, a\n * credential, and a sandbox.\n *\n * `interpretRecord` decides; the supervisor applies. Keeping the decision pure means the\n * tests exercise the real code path, not a paraphrase of it.\n */\nimport {\n BLOCKING_DIALOG_METHODS,\n FIRE_AND_FORGET_UI_METHODS,\n isBlockingDialog,\n type LiveSession,\n type PendingDialog,\n type PiRecord,\n} from \"@pi-central/protocol\";\n\n/** What the supervisor should change after seeing one record. */\nexport interface RecordEffect {\n /** Fields to merge into the session. */\n patch?: Partial<LiveSession>;\n /** A dialog to register, if this record opened one. */\n dialog?: PendingDialog;\n /** Whether fresh stats should be fetched (settled is the natural moment). */\n refreshStats?: boolean;\n}\n\n/** Neither user nor assistant: a tool result, a system note, a log line. */\nfunction countAsMessage(record: PiRecord): boolean {\n const message = record.message as { role?: string } | undefined;\n return message?.role === \"user\" || message?.role === \"assistant\";\n}\n\n/**\n * Interpret one Pi record against the session's current state.\n *\n * `session` is passed in read-only because a couple of decisions depend on current state\n * (an outstanding dialog changes what \"settled\" means).\n */\nexport function interpretRecord(session: LiveSession, record: PiRecord): RecordEffect {\n switch (record.type) {\n case \"agent_start\":\n return { patch: { state: \"streaming\" } };\n\n case \"agent_end\":\n // Deliberately no patch (C8). Retries, compaction, steering and follow-ups can all\n // follow agent_end; calling it \"done\" would mark a working session idle and invite a\n // teardown or a re-prompt mid-run.\n return {};\n\n case \"compaction_end\":\n // A stats refresh, and ONLY a stats refresh (#5). Compaction's whole purpose is to shrink the\n // context, so `contextPercent` is stale the moment it finishes \u2014 the meter would keep showing\n // the pressure that was just relieved, and the reader's next decision (prompt now, or compact\n // again?) would be made on the old number. An `agent_settled`-only refresh would hide the drop\n // for the entire rest of the turn.\n //\n // No state patch: a compaction can be followed by a retry, so it is not completion (C8).\n return { refreshStats: true };\n\n case \"agent_settled\":\n // THIS is completion (C8).\n return {\n patch: { state: session.dialogs.length > 0 ? \"awaiting-approval\" : \"idle\" },\n refreshStats: true,\n };\n\n case \"message_end\":\n return countAsMessage(record) ? { patch: { messageCount: session.messageCount + 1 } } : {};\n\n case \"queue_update\": {\n const steering = (record.steering as unknown[] | undefined)?.length ?? 0;\n const followUp = (record.followUp as unknown[] | undefined)?.length ?? 0;\n return { patch: { pending: steering + followUp } };\n }\n\n case \"thinking_level_changed\":\n return { patch: { thinkingLevel: record.level as string | undefined } };\n\n case \"extension_ui_request\": {\n // Classified by METHOD, and the unknown-method default is \"blocking\" (see\n // `isBlockingDialog`): notify/setStatus/setWidget arrive in a burst at startup and must\n // never be dialogs, while an unrecognized method must never be silently dropped -- an\n // unanswerable request that Pi is waiting on hangs the session with no visible cause.\n if (!isBlockingDialog(record)) return {};\n const dialog: PendingDialog = {\n id: String(record.id),\n method: String(record.method),\n title: record.title as string | undefined,\n message: record.message as string | undefined,\n options: record.options as string[] | undefined,\n placeholder: record.placeholder as string | undefined,\n prefill: record.prefill as string | undefined,\n receivedAt: Date.now(),\n };\n return { dialog, patch: { state: \"awaiting-approval\" } };\n }\n\n default:\n return {};\n }\n}\n\n/**\n * Exposed so a test can assert the classification matches Pi's documented method families.\n *\n * This file previously carried its own copy of the blocking-method list, which is a second\n * source of truth for a security-relevant decision -- the two would drift. It now uses the\n * protocol's list, and the default for an unrecognized method is handled there.\n */\nexport const blockingDialogMethods: readonly string[] = [...BLOCKING_DIALOG_METHODS];\n\n/** Exposed so a test can assert the non-dialog set matches Pi's fire-and-forget methods. */\nexport const fireAndForgetUiMethods: readonly string[] = [...FIRE_AND_FORGET_UI_METHODS];\n", "/**\n * The orchestrator's WebSocket transport (Phase 2, ROADMAP).\n *\n * Bridges the `Supervisor` to browser clients using the typed envelopes in\n * `@pi-central/protocol`. Deliberately thin: it routes, serializes, and enforces two\n * boundaries, and holds no session logic of its own. Anything that needs to know about Pi\n * belongs in the supervisor, which is testable without a socket.\n *\n * Two boundaries, both structural rather than conventional:\n *\n * 1. **Control is a closed union.** A client cannot name an arbitrary operation; the\n * protocol has no shape for one. An unknown `kind` is rejected, not ignored, so a UI\n * version skew surfaces as an error instead of silently doing nothing.\n *\n * 2. **Events carry untrusted content and are never re-parsed as control.** The envelope\n * direction field makes that machine-checkable, so the trust boundary does not depend\n * on the reader remembering it.\n *\n * Loopback is **not** a security boundary (C5): any local process can connect. The socket\n * therefore grants exactly the capabilities of the user running the orchestrator, which is\n * why it must not be exposed beyond `127.0.0.1` and why the client count is bounded.\n */\nimport { createServer, type Server } from \"node:http\";\nimport { WebSocketServer, type WebSocket } from \"ws\";\nimport {\n PROTOCOL_VERSION,\n type Control,\n type Envelope,\n type Event,\n type HistoricalSession,\n type InstallFacts,\n type LiveSession,\n type PiRecord,\n type ServerPayload,\n} from \"@pi-central/protocol\";\nimport { findOwnPackage } from \"./package-meta.ts\";\nimport type { Supervisor } from \"./supervisor.ts\";\nimport type { FileError } from \"./files.ts\";\nimport { discover } from \"./registry.ts\";\nimport type { Config } from \"./config.ts\";\nimport { createUiRequestHandler, resolveUiDistDir } from \"./static-ui.ts\";\n\nexport interface TransportOptions {\n host?: string;\n port?: number;\n /** Cap concurrent clients. Loopback is reachable by any local process (C5). */\n maxClients?: number;\n /**\n * The built UI to serve at `/` (issue #24). Absent \u2192 `resolveUiDistDir()`, i.e. the repository's\n * own `packages/ui/dist`.\n *\n * Set to `null` to serve no UI at all, which is what a test that only cares about the socket\n * wants: otherwise every transport test would depend on whether the UI happened to be built.\n */\n uiDir?: string | null;\n}\n\nexport interface TransportAddress {\n host: string;\n port: number;\n url: string;\n}\n\n/** Every control kind the orchestrator understands, for the runtime check in `#onMessage`. */\nconst KNOWN_CONTROL_KINDS: ReadonlySet<string> = new Set<Control[\"kind\"]>([\n \"list\",\n \"start\",\n \"prompt\",\n \"clearQueue\",\n \"abort\",\n \"stop\",\n \"rename\",\n \"setModel\",\n \"listModels\",\n \"setThinkingLevel\",\n \"listThinkingLevels\",\n \"answerDialog\",\n \"resume\",\n \"listFiles\",\n \"readFile\",\n]);\n\n/** Reject a malformed envelope with a reason, rather than closing silently. */\nexport function parseControl(raw: string): Control | { error: string } {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return { error: \"not JSON\" };\n }\n if (typeof parsed !== \"object\" || parsed === null) return { error: \"not an object\" };\n\n const envelope = parsed as Partial<Envelope> & { dir?: string; payload?: unknown };\n // The direction is checked, not assumed. An event-shaped message arriving as control is\n // rejected here rather than being coerced into a command.\n if (envelope.dir !== \"client\u2192orchestrator\") {\n return { error: `wrong direction: expected 'client\u2192orchestrator', got '${String(envelope.dir)}'` };\n }\n if (envelope.protocol !== PROTOCOL_VERSION) {\n return { error: `protocol mismatch: expected ${PROTOCOL_VERSION}, got ${String(envelope.protocol)}` };\n }\n const payload = envelope.payload as Control | undefined;\n if (typeof payload !== \"object\" || payload === null || typeof (payload as Control).kind !== \"string\") {\n return { error: \"payload has no kind\" };\n }\n return payload;\n}\n\nexport class Transport {\n #supervisor: Supervisor;\n #config: Config;\n #options: Required<TransportOptions>;\n #server: Server | undefined;\n #wss: WebSocketServer | undefined;\n #clients = new Set<WebSocket>();\n #address: TransportAddress | undefined;\n #install: InstallFacts | undefined;\n\n constructor(supervisor: Supervisor, config: Config, options: TransportOptions = {}) {\n this.#supervisor = supervisor;\n this.#config = config;\n this.#options = {\n host: options.host ?? \"127.0.0.1\",\n port: options.port ?? 0,\n maxClients: options.maxClients ?? 8,\n uiDir: options.uiDir === undefined ? resolveUiDistDir() : options.uiDir,\n };\n }\n\n get address(): TransportAddress | undefined {\n return this.#address;\n }\n\n /** The UI directory being served, or `null` when the socket is the only surface. */\n get uiDir(): string | null {\n return this.#options.uiDir;\n }\n\n /** Bind and start serving. Resolves with the actual address (port 0 \u2192 a real port). */\n async listen(): Promise<TransportAddress> {\n if (this.#server) throw new Error(\"transport is already listening\");\n\n // One listener, two protocols. `/ws` upgrades to the socket; every other path is the built\n // UI. Serving both from one origin is what makes `connect-src 'self'` in the CSP truthful\n // rather than aspirational (ADR-0006), and it means the dashboard needs no dev server.\n //\n // The UI directory may legitimately not exist (nothing built yet). That is not a startup\n // failure \u2014 the socket is still useful, and a CLI smoke run should not require a UI build \u2014\n // so the handler reports the missing build per request, naming the fix, and the entry point\n // prints the same fact once at startup.\n const uiHandler = this.#options.uiDir === null ? undefined : createUiRequestHandler({ root: this.#options.uiDir });\n\n const server = createServer((req, res) => {\n if (uiHandler) {\n uiHandler(req, res);\n return;\n }\n res.writeHead(426, { \"content-type\": \"text/plain\" });\n res.end(\"Pi Central speaks WebSocket only, and no UI directory is configured.\\n\");\n });\n\n const wss = new WebSocketServer({ server, maxPayload: 8 * 1024 * 1024 });\n this.#server = server;\n this.#wss = wss;\n\n wss.on(\"connection\", (socket) => this.#onConnection(socket));\n\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(this.#options.port, this.#options.host, () => {\n server.off(\"error\", reject);\n resolve();\n });\n });\n\n const addr = server.address();\n if (addr === null || typeof addr === \"string\") throw new Error(\"unexpected server address\");\n this.#address = {\n host: this.#options.host,\n port: addr.port,\n url: `ws://${this.#options.host}:${addr.port}`,\n };\n return this.#address;\n }\n\n #onConnection(socket: WebSocket): void {\n if (this.#clients.size >= this.#options.maxClients) {\n // Say why. A silent close looks like a network fault.\n this.#send(socket, { kind: \"error\", message: `too many clients (max ${this.#options.maxClients})` });\n socket.close(1013, \"too many clients\");\n return;\n }\n this.#clients.add(socket);\n\n // On connect, the client needs the whole picture: what is live, and what is on disk.\n // Sent as one snapshot rather than two events so a client never renders a half-state.\n this.#send(socket, { kind: \"hello\", protocol: PROTOCOL_VERSION });\n void this.#sendSnapshot(socket);\n\n socket.on(\"message\", (data) => void this.#onMessage(socket, String(data)));\n socket.on(\"close\", () => this.#clients.delete(socket));\n socket.on(\"error\", () => this.#clients.delete(socket));\n }\n\n /**\n * Facts about this installation, for the Settings surface (issue #58).\n *\n * Computed once and cached: `findOwnPackage` walks the directory tree, and this is sent on every\n * connection. The version cannot change while the process runs, so recomputing it per connection\n * would be work done to produce a value already known to be identical.\n */\n #installFacts(): InstallFacts {\n if (this.#install !== undefined) return this.#install;\n const own = findOwnPackage();\n this.#install = {\n // \"unknown\" rather than a substituted \"0.0.0\": a version that cannot be determined is a\n // packaging fault, and saying so is truthful. A plausible-looking default would be believed,\n // and every question asked afterwards would start from a false premise.\n version: own?.version ?? \"unknown\",\n sessionsRoot: this.#config.sessionsRoot,\n workspaceRoot: this.#config.workspaceRoot,\n agentRoot: this.#config.agentRoot,\n agentRootManaged: this.#config.agentRootManaged,\n protocol: PROTOCOL_VERSION,\n };\n return this.#install;\n }\n\n async #sendSnapshot(socket: WebSocket): Promise<void> {\n let history: HistoricalSession[] = [];\n let historyTotal = 0;\n try {\n const discovered = await discover(this.#config);\n history = discovered.sessions;\n historyTotal = discovered.total;\n } catch (error) {\n // Discovery reads the real session store, which can be absent or unreadable. A\n // snapshot with no history is still useful; failing the whole connection is not.\n this.#send(socket, { kind: \"error\", message: `discovery failed: ${(error as Error).message}` });\n }\n this.#send(socket, {\n kind: \"snapshot\",\n live: this.#supervisor.list(),\n history,\n historyTotal,\n home: this.#config.home,\n installationRoot: this.#config.installationRoot,\n install: this.#installFacts(),\n });\n\n // Then the CURRENT conversation for each live session, FETCHED rather than replayed.\n //\n // The `transcript` event fires once when a session starts, so a client that connects later \u2014\n // after a browser reload, say \u2014 receives none of it. An earlier version replayed a copy cached\n // at session start; for a session started fresh that copy held its opening, empty state, so a\n // reload REPLACED a correct transcript with an empty one and the session appeared to load\n // forever. Re-fetching is the only way to be current.\n for (const session of this.#supervisor.list()) {\n if (session.state === \"stopped\" || session.state === \"failed\") continue;\n void this.#sendTranscript(socket, session.id);\n }\n }\n\n /** Fetch one session's conversation for one client, and report failure as a failure. */\n async #sendTranscript(socket: WebSocket, sessionId: string): Promise<void> {\n const result = await this.#supervisor.fetchTranscript(sessionId);\n if (!result.ok) {\n this.#send(socket, { kind: \"transcriptError\", sessionId, message: result.message });\n return;\n }\n this.#send(socket, { kind: \"transcript\", sessionId, messages: result.messages });\n }\n\n async #onMessage(socket: WebSocket, raw: string): Promise<void> {\n const control = parseControl(raw);\n if (\"error\" in control) {\n this.#send(socket, { kind: \"error\", message: `bad control: ${control.error}` });\n return;\n }\n\n // TypeScript makes the switch below exhaustive over `Control`, but that guarantee is\n // compile-time only: an unknown `kind` from a newer client would reach the end of the\n // switch and do NOTHING. Silence looks like a broken button, so it is checked at runtime\n // too. (Found by a test: this case originally fell through and hung the client.)\n if (!KNOWN_CONTROL_KINDS.has(control.kind)) {\n this.#send(socket, { kind: \"error\", message: `bad control: unknown kind '${control.kind}'` });\n return;\n }\n\n try {\n await this.#dispatch(control);\n } catch (error) {\n this.#send(socket, { kind: \"error\", message: (error as Error).message });\n }\n }\n\n /**\n * Route one control message.\n *\n * The switch is exhaustive over `Control`, so adding a command to the protocol without\n * handling it here is a compile error rather than a silent no-op.\n */\n async #dispatch(control: Control): Promise<void> {\n switch (control.kind) {\n case \"list\": {\n const discovered = await discover(this.#config);\n this.#broadcast({ kind: \"history\", history: discovered.sessions, total: discovered.total });\n this.#broadcast({\n kind: \"snapshot\",\n live: this.#supervisor.list(),\n history: discovered.sessions,\n historyTotal: discovered.total,\n home: this.#config.home,\n installationRoot: this.#config.installationRoot,\n });\n return;\n }\n case \"start\": {\n const result = this.#supervisor.start({\n ...(control.cwd ? { cwd: control.cwd } : {}),\n ...(control.name ? { name: control.name } : {}),\n ...(control.mode ? { mode: control.mode } : {}),\n });\n if (!result.ok) {\n this.#broadcast({ kind: \"error\", message: result.message });\n return;\n }\n this.#broadcast({ kind: \"session\", session: result.session });\n return;\n }\n case \"resume\": {\n const discovered = await discover(this.#config);\n const target = discovered.sessions.find((s) => s.path === control.path);\n if (!target) {\n this.#broadcast({ kind: \"error\", message: `no such session: ${control.path}` });\n return;\n }\n const result = this.#supervisor.resume(target, control.mode ? { mode: control.mode } : {});\n if (!result.ok) {\n this.#broadcast({ kind: \"error\", message: result.message });\n return;\n }\n this.#broadcast({ kind: \"session\", session: result.session });\n return;\n }\n case \"prompt\":\n // `behavior` is forwarded only when the client chose one; the supervisor decides what a\n // missing choice means (see `Supervisor#prompt`).\n await this.#supervisor.prompt(\n control.sessionId,\n control.message,\n ...(control.behavior ? [control.behavior] : []),\n );\n return;\n case \"clearQueue\": {\n await this.#supervisor.clearQueue(control.sessionId, control.andStop);\n return;\n }\n case \"abort\":\n await this.#supervisor.abort(control.sessionId);\n return;\n case \"stop\":\n await this.#supervisor.stop(control.sessionId);\n return;\n case \"rename\":\n await this.#supervisor.rename(control.sessionId, control.name);\n return;\n case \"listModels\": {\n const result = await this.#supervisor.listModels(control.sessionId);\n // \"empty\" and \"failed\" are different events: an empty picker would claim the session has\n // no models when the truth is that the question failed. This is the same distinction\n // `transcriptError` exists for.\n if (result.ok) this.#broadcast({ kind: \"models\", sessionId: control.sessionId, models: result.models });\n else this.#broadcast({ kind: \"modelsError\", sessionId: control.sessionId, message: result.message });\n return;\n }\n case \"setModel\": {\n // Not caught here: `#dispatch` reports a throw as an error event, which is what a refusal\n // from Pi must become \u2014 a visible reason, never a silent no-op.\n await this.#supervisor.setModel(control.sessionId, control.provider, control.modelId);\n return;\n }\n case \"listThinkingLevels\": {\n const result = await this.#supervisor.listThinkingLevels(control.sessionId);\n if (result.ok) this.#broadcast({ kind: \"thinkingLevels\", sessionId: control.sessionId, levels: result.levels });\n else this.#broadcast({ kind: \"thinkingLevelsError\", sessionId: control.sessionId, message: result.message });\n return;\n }\n case \"setThinkingLevel\":\n await this.#supervisor.setThinkingLevel(control.sessionId, control.level);\n return;\n case \"answerDialog\":\n this.#supervisor.answerDialog(control.sessionId, control.dialogId, control.answer);\n return;\n // --- File browsing (issue #8, ADR-0007) ------------------------------------------------\n //\n // Resolved through the SUPERVISOR, not straight to `files.ts`, because the root is the\n // session's cwd and only the supervisor knows it. That keeps one answer to \"which directory is\n // this session confined to\" \u2014 and it means a request for an unknown session is refused as\n // such, rather than being resolved against some path the transport guessed.\n //\n // Everything below is read at the ORCHESTRATOR's UID, deliberately (ADR-0007). No sandbox is\n // involved: Safehouse confines agents, and this serves the person.\n case \"listFiles\": {\n const result = this.#supervisor.listFiles(control.sessionId, control.path, {\n ...(control.includeHidden === undefined ? {} : { includeHidden: control.includeHidden }),\n });\n if (result.ok) {\n this.#broadcast({\n kind: \"files\",\n sessionId: control.sessionId,\n // The path as the CLIENT sent it, not the normalized one the orchestrator resolved.\n // The client keys its cache, its `loading` state and its expansion flag by the path it\n // requested, so echoing a different spelling would file the result under one key while\n // the node waited on another \u2014 leaving it in \"Reading\u2026\" forever. Normalization stays\n // inside the orchestrator, where it is used for the filesystem work and for messages.\n path: control.path,\n entries: result.value.entries,\n truncated: result.value.truncated,\n });\n } else {\n this.#broadcastFileError(control.sessionId, control.path, \"list\", result.error);\n }\n return;\n }\n case \"readFile\": {\n const result = this.#supervisor.readFile(control.sessionId, control.path);\n if (result.ok) {\n this.#broadcast({\n kind: \"file\",\n sessionId: control.sessionId,\n // As sent, for the same reason as `files`: one key for the client's bookkeeping.\n path: control.path,\n text: result.value.text,\n bytes: result.value.bytes,\n });\n } else {\n this.#broadcastFileError(control.sessionId, control.path, \"read\", result.error);\n }\n return;\n }\n }\n }\n\n /**\n * Report a refused file request.\n *\n * `path` is echoed as the client SENT it, not as it resolved \u2014 the complaint is about what was\n * asked for, and a resolved path would describe something the user never typed.\n *\n * `request` distinguishes a failed listing from a failed read, so the client routes the error to the\n * right cache instead of guessing which request it answered.\n */\n #broadcastFileError(\n sessionId: string,\n path: string,\n request: \"list\" | \"read\",\n error: FileError,\n ): void {\n this.#broadcast({\n kind: \"fileError\",\n request,\n sessionId,\n path,\n reason: error.reason,\n ...(error.code === undefined ? {} : { code: error.code }),\n message: error.message,\n });\n }\n\n /** Fan out to every client. One slow client must not stall the others. */\n #broadcast(event: Event): void {\n for (const socket of this.#clients) this.#send(socket, event);\n }\n\n #send(socket: WebSocket, event: ServerPayload): void {\n if (socket.readyState !== socket.OPEN) return;\n const envelope = { dir: \"orchestrator\u2192client\", protocol: PROTOCOL_VERSION, payload: event };\n socket.send(JSON.stringify(envelope));\n }\n\n /**\n * Forward supervisor events to clients.\n *\n * Called by the entry point after construction, because it wires the supervisor's\n * `EventEmitter` to the socket. Kept out of the constructor so a test can build a\n * transport without an emitting supervisor.\n */\n attach(): void {\n this.#supervisor.on(\"update\", (session: LiveSession) => {\n this.#broadcast({ kind: \"session\", session });\n });\n this.#supervisor.on(\"record\", (sessionId: string, record: PiRecord) => {\n this.#broadcast({ kind: \"record\", sessionId, record });\n });\n // Replayed history, kept distinct from the live stream (see the `transcript` event).\n this.#supervisor.on(\"transcript\", (sessionId: string, messages: unknown[]) => {\n this.#broadcast({ kind: \"transcript\", sessionId, messages });\n });\n this.#supervisor.on(\"transcriptError\", (sessionId: string, message: string) => {\n this.#broadcast({ kind: \"transcriptError\", sessionId, message });\n });\n // The queue's TEXTS (issue #3). `LiveSession.pending` carries the count on every update;\n // this carries what is IN the queue, which is what the composer shows and what the user is\n // told will happen.\n this.#supervisor.on(\"queue\", (sessionId: string, steering: string[], followUp: string[]) => {\n this.#broadcast({ kind: \"queue\", sessionId, steering, followUp });\n }); this.#supervisor.on(\n \"queueCleared\",\n (sessionId: string, steering: string[], followUp: string[]) => {\n this.#broadcast({ kind: \"queueCleared\", sessionId, steering, followUp });\n },\n );\n this.#supervisor.on(\"error\", (sessionId: string, message: string) => {\n this.#broadcast({ kind: \"error\", message, sessionId });\n });\n }\n\n async close(): Promise<void> {\n for (const socket of this.#clients) socket.close(1001, \"server shutting down\");\n this.#clients.clear();\n const wss = this.#wss;\n const server = this.#server;\n this.#wss = undefined;\n this.#server = undefined;\n this.#address = undefined;\n if (wss) await new Promise<void>((resolve) => wss.close(() => resolve()));\n if (server) await new Promise<void>((resolve) => server.close(() => resolve()));\n }\n}\n", "/**\n * What version of Pi Central is this, and where does the package live (issue #52).\n *\n * ## Why this reads `package.json` instead of carrying a constant\n *\n * `pi-central --version` must report **the version that was published**, and the published version\n * is by definition the one in the shipped `package.json`. A TypeScript constant would be a second\n * place to update on every release, and the failure mode of getting it wrong is the worst kind: the\n * tool would confidently report a version it is not, and every bug report that followed would begin\n * with a false premise. So the number is read from the artifact itself and never restated.\n *\n * (The `protocol` package solves the opposite problem with a constant plus a pinning test \u2014 see\n * `packages/protocol/test/wire-surface.test.ts`. That is right there because `PROTOCOL_VERSION` is a\n * *contract* someone must consciously decide. A release version is a *fact about the artifact*, so\n * it is read, not asserted.)\n *\n * ## Why it walks up looking for a NAME\n *\n * The two layouts are genuinely different and both must work:\n *\n * - **From source** (`npm run server`): this module lives at\n * `packages/orchestrator/src/package-meta.ts`, and the owning package is three levels up. But\n * the first `package.json` on the way up is `packages/orchestrator/package.json`, whose name is\n * `@pi-central/orchestrator` and whose version merely happens to match today.\n * - **From the bundle** (`bin/pi-central.js`): the owning package is one level up.\n *\n * Matching on `name === \"pi-central\"` rather than taking the first `package.json` found is what\n * makes this correct in the first case instead of accidentally correct. \"Take the nearest\n * package.json\" would pick the wrong file and look right, which is the shape of bug this project\n * keeps finding.\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join, parse } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/** The name of the published package. The one an update check looks up (issue #55). */\nexport const PACKAGE_NAME = \"pi-central\";\n\nexport interface PackageMeta {\n /** Absolute path to the package's own `package.json`. */\n path: string;\n /** Absolute path to the package root \u2014 where `bin/`, `dist/` and `package.json` live. */\n root: string;\n name: string;\n version: string;\n}\n\n/**\n * Walk up from `from` until a `package.json` naming this package is found.\n *\n * Returns `undefined` rather than throwing when none is found, because the callers have different\n * right answers for that: `--version` should say it cannot tell, and the UI resolution should fall\n * back to a path candidate. A throw here would turn a missing file into a crash.\n */\nexport function findOwnPackage(from: string = import.meta.url): PackageMeta | undefined {\n let directory = dirname(fileURLToPath(from));\n // Bounded by the filesystem root, and `parse(dir).root === dir` is the portable stop condition \u2014\n // comparing against \"/\" would miss the drive-letter root on Windows.\n for (;;) {\n const candidate = join(directory, \"package.json\");\n if (existsSync(candidate)) {\n const parsed = readPackageJson(candidate);\n if (parsed?.name === PACKAGE_NAME && typeof parsed.version === \"string\") {\n return { path: candidate, root: directory, name: parsed.name, version: parsed.version };\n }\n }\n const parent = dirname(directory);\n if (parent === directory || directory === parse(directory).root) return undefined;\n directory = parent;\n }\n}\n\n/** Read a `package.json`, or `undefined` if it is missing or not readable as an object. */\nexport function readPackageJson(path: string): { name?: unknown; version?: unknown } | undefined {\n try {\n const parsed: unknown = JSON.parse(readFileSync(path, \"utf8\"));\n if (typeof parsed !== \"object\" || parsed === null) return undefined;\n return parsed as { name?: unknown; version?: unknown };\n } catch {\n // A malformed package.json is a packaging error, and the reporting belongs to whoever needs the\n // value \u2014 not to a helper that would then have to invent an error type for it.\n return undefined;\n }\n}\n", "/**\n * Session discovery (ADR-0009).\n *\n * Pi Central reads `~/.pi/agent/sessions/<encoded-cwd>/*.jsonl` directly. It does NOT call\n * `SessionManager.listAll()`, which is a runtime import that would load Pi's agent core into\n * this unsandboxed, privileged process (ADR-0002/ADR-0009).\n *\n * Deliberate limits:\n * - exactly one level of directories under the root; no recursive walk\n * - only regular files ending in `.jsonl` (see the trap below)\n * - at most `HEAD_BYTES` of each file is read, so a huge transcript is cheap to skip\n *\n * ## The sidecar-directory trap\n *\n * Pi creates a sidecar directory next to each session file, named after the session with\n * the `.jsonl` suffix dropped, holding subagent runs:\n *\n * <project>/\n * 2026-\u2026_<id>.jsonl <- the session file\n * 2026-\u2026_<id>/ <- sidecar DIR (no .jsonl suffix)\n * <uuid>/run-0/session.jsonl\n *\n * The directory name has no `.jsonl` suffix, so a suffix filter already excludes it. The\n * real hazard is **recursion**: a walk that descended into the sidecar would surface every\n * subagent `run-0/session.jsonl` as a top-level session. So this module enumerates exactly\n * one level and takes only regular files. `isFile()` is checked rather than assumed; it\n * costs one call and makes the invariant explicit instead of incidental.\n */\nimport { createReadStream, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { HistoricalSession } from \"@pi-central/protocol\";\nimport { isScratch, type Config } from \"./config.ts\";\n\n/** Pi's session header. Versioned; asserted on read. */\nexport interface SessionHeader {\n type: \"session\";\n version: number;\n id: string;\n timestamp: string;\n cwd: string;\n parentSession?: string;\n}\n\nexport const KNOWN_SESSION_VERSIONS = new Set([3]);\n\n/**\n * How much of a session file discovery is willing to read.\n *\n * Discovery is a *list* operation over hundreds of files, so it caps its read rather than\n * scanning each transcript to the end. The cost of that choice is stated plainly:\n *\n * - `messageCount` is a FLOOR, not exact, for sessions larger than this window\n * - `name` is best-effort; `session_info` can appear anywhere, so a name set late in a\n * large session will not be seen\n *\n * Measured on this machine: 833 real sessions in ~900 ms, 0 unparseable. Scanning every\n * file to the end would be correct and several times slower for a field the rail does not\n * need yet. The floor can be visibly wrong: for one 2.8 MB session the head window counted\n * 69 messages of 869 records (429 user+assistant). That is why a live session's count is\n * corrected from `get_state`, which is authoritative, the moment it is supervised.\n *\n * Full counts and names belong to a full read, which happens when a session is opened, not\n * when the list is drawn.\n */\nconst HEAD_BYTES = 256 * 1024;\n\n/** Mirror Pi's directory encoding: strip leading slash, replace `/` and `:` with `-`. */\nexport function encodeSessionDir(cwd: string): string {\n return `--${cwd.replace(/^[/\\\\]/, \"\").replace(/[/\\\\:]/g, \"-\")}--`;\n}\n\n/** Best-effort inverse, used only when a header lacks a cwd (old sessions). */\nexport function decodeSessionDir(dirName: string): string {\n const inner = dirName.replace(/^--/, \"\").replace(/--$/, \"\");\n return `/${inner.replace(/-/g, \"/\")}`;\n}\n\n/** Read up to HEAD_BYTES of a file as utf8, streamed so a large file is not loaded whole. */\nasync function readHead(path: string): Promise<string> {\n return new Promise((resolve) => {\n const chunks: Buffer[] = [];\n let size = 0;\n const stream = createReadStream(path, { start: 0, end: HEAD_BYTES - 1 });\n stream.on(\"data\", (chunk) => {\n chunks.push(chunk as Buffer);\n size += (chunk as Buffer).length;\n });\n stream.on(\"error\", () => resolve(\"\"));\n stream.on(\"close\", () => resolve(Buffer.concat(chunks, size).toString(\"utf8\")));\n });\n}\n\ninterface ParsedHead {\n header: SessionHeader;\n name: string | undefined;\n firstMessage: string;\n messageCount: number;\n}\n\n/**\n * Parse the records we need from the head window.\n *\n * `messageCount` is a lower bound and `name` is best-effort \u2014 see `HEAD_BYTES`. Both are\n * reported as found, never inflated.\n */\nfunction parseHead(text: string): ParsedHead | null {\n const lines = text.split(\"\\n\");\n let header: SessionHeader | null = null;\n let name: string | undefined;\n let firstMessage = \"\";\n let messageCount = 0;\n\n for (const line of lines) {\n if (!line) continue;\n let record: Record<string, unknown>;\n try {\n record = JSON.parse(line) as Record<string, unknown>;\n } catch {\n if (!header) continue;\n break; // a partial trailing line; stop rather than mis-count\n }\n if (!header) {\n if (record.type !== \"session\") return null;\n if (typeof record.id !== \"string\" || typeof record.cwd !== \"string\") return null;\n header = {\n type: \"session\",\n version: Number(record.version ?? 0),\n id: record.id,\n timestamp: String(record.timestamp ?? \"\"),\n cwd: record.cwd,\n ...(typeof record.parentSession === \"string\" ? { parentSession: record.parentSession } : {}),\n };\n continue;\n }\n if (record.type === \"session_info\" && typeof record.name === \"string\") {\n name = record.name.trim() || undefined;\n continue;\n }\n if (record.type === \"message\") {\n messageCount += 1;\n if (!firstMessage) {\n const message = record.message as { role?: string; content?: unknown } | undefined;\n if (message?.role === \"user\") firstMessage = flattenContent(message.content).slice(0, 200);\n }\n }\n }\n\n return header ? { header, name, firstMessage, messageCount } : null;\n}\n\nfunction flattenContent(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === \"string\") return part;\n if (part && typeof part === \"object\" && \"text\" in part) return String((part as { text: unknown }).text);\n return \"\";\n })\n .join(\" \");\n }\n return \"\";\n}\n\nexport interface DiscoveryResult {\n sessions: HistoricalSession[];\n /**\n * How many session files exist, BEFORE the `limit` is applied.\n *\n * This exists because the UI states a total as a fact, and passing it the length of a\n * truncated array makes it assert something false. Observed on this machine: 1115 session\n * files, a limit of 500, and a rail reading \"500 sessions on disk\" -- a misleading machine\n * value stated confidently, which is precisely the class of thing `format.ts` has tests\n * against. A total the caller cannot distinguish from a page size will eventually be shown\n * as one.\n */\n total: number;\n /** Files that looked like sessions but could not be parsed. Surfaced, not hidden. */\n skipped: { path: string; reason: string }[];\n}\n\n/** Discover every session file under the configured root, newest first. */\nexport async function discover(config: Config, limit = 500): Promise<DiscoveryResult> {\n const sessions: HistoricalSession[] = [];\n const skipped: { path: string; reason: string }[] = [];\n\n let projectDirs: string[];\n try {\n projectDirs = readdirSync(config.sessionsRoot, { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n } catch {\n return { sessions, total: 0, skipped }; // no sessions yet is not an error\n }\n\n for (const projectDir of projectDirs) {\n const projectPath = join(config.sessionsRoot, projectDir);\n let entries;\n try {\n entries = readdirSync(projectPath, { withFileTypes: true });\n } catch {\n continue;\n }\n\n for (const entry of entries) {\n if (!entry.name.endsWith(\".jsonl\")) continue;\n // The trap: a sidecar directory holds subagent runs. Its name has no `.jsonl` suffix,\n // so the check above already excludes it -- but assert regularity explicitly, and\n // never recurse, so a nested run-0/session.jsonl can never be surfaced as a session.\n if (!entry.isFile()) continue;\n\n const path = join(projectPath, entry.name);\n let stats;\n try {\n stats = statSync(path);\n } catch {\n continue;\n }\n\n const parsed = parseHead(await readHead(path));\n if (!parsed) {\n skipped.push({ path, reason: \"not a Pi session file (no valid header)\" });\n continue;\n }\n if (!KNOWN_SESSION_VERSIONS.has(parsed.header.version)) {\n skipped.push({ path, reason: `unknown session version ${parsed.header.version}` });\n continue;\n }\n\n const cwd = parsed.header.cwd || decodeSessionDir(projectDir);\n sessions.push({\n path,\n id: parsed.header.id,\n cwd,\n ...(parsed.name ? { name: parsed.name } : {}),\n created: parsed.header.timestamp,\n modifiedMs: stats.mtimeMs,\n bytes: stats.size,\n messageCount: parsed.messageCount,\n firstMessage: parsed.firstMessage,\n cwdExists: directoryExists(cwd),\n scratch: isScratch(config, cwd),\n });\n }\n }\n\n sessions.sort((a, b) => b.modifiedMs - a.modifiedMs);\n // `total` is measured after the sort but before the slice, so it is the true count rather\n // than the size of the page returned.\n return { sessions: sessions.slice(0, limit), total: sessions.length, skipped };\n}\n\nfunction directoryExists(dir: string): boolean {\n try {\n return statSync(dir).isDirectory();\n } catch {\n return false;\n }\n}\n", "/**\n * Serving the built UI from the orchestrator (issue #24).\n *\n * Until now the orchestrator spoke WebSocket only: every HTTP request got `426 Upgrade\n * Required`, which meant a Vite dev server was *mandatory* to see anything. That has a cost\n * beyond convenience \u2014 Vite watches `packages/ui/src`, so an agent editing a component reloads\n * the interface you are using out from under you, mid-edit, into a syntax-error overlay\n * ([DOGFOODING.md](../../../docs/DOGFOODING.md)). A built UI served by the orchestrator removes\n * the watcher, so editing the UI can no longer disturb the running one.\n *\n * It also makes ADR-0006's CSP *truthful*. The policy says `connect-src 'self'`, which is only\n * meaningful if the socket really is same-origin with the document. Serving both from one\n * listener achieves that by construction, rather than by a dev-server proxy configuration that\n * happens to be correct.\n *\n * ## What this listener is not\n *\n * **It is not a static file server for the filesystem.** It serves exactly one directory, and\n * only paths that resolve inside it. Two failure modes are worth naming because each looks like\n * success from the outside:\n *\n * 1. *Escaping the root.* `%2e%2e%2f` is not decoded by `new URL`, so string-matching a\n * \"normalized\" URL is not enough \u2014 the decode has to happen here first. Traversal is then\n * stopped by rejecting a `..` segment outright (a denial, not a 404 \u2014 the two are different\n * facts). Containment-on-the-canonical-path is a backstop for a *different* defect class,\n * not for traversal: `posix.normalize` clamps `..` at the root, which was established by\n * removing the ban and watching the suite fail. See `isInside`.\n * 2. *Becoming a filesystem oracle.* A 404 for a path outside the root and a 404 for a\n * non-existent path inside it are indistinguishable and both fine. A 403 for the former\n * is deliberate: an escape attempt is worth reporting, not hiding.\n *\n * Loopback is not a boundary (C5): this listener grants whatever it serves to any local process\n * that asks. That is the same grant the WebSocket already carries, which is why it must not be\n * bound beyond `127.0.0.1` \u2014 the transport guarantees that, not this module.\n */\nimport { createReadStream, existsSync, statSync } from \"node:fs\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { extname, join, posix, resolve, sep } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { findOwnPackage } from \"./package-meta.ts\";\n\n/**\n * The Content-Security-Policy, as a header.\n *\n * ADR-0006's policy is also in `<meta http-equiv>` in `packages/ui/index.html`, and the two must\n * agree. They are not redundant: a **header applies before the document is parsed**, so it also\n * constrains the document's own subresource loads; the meta tag is enforced from the point it is\n * reached, which is after the parser has already seen the `<head>`. Sending only the meta tag\n * means the policy arrives late.\n *\n * Two policies that drift are worse than one, so the duplication is guarded by a test that parses\n * the meta tag out of `index.html` and asserts it equals this string\n * (`packages/orchestrator/test/static-ui.test.ts`). Change one without the other and CI fails.\n */\nexport const UI_CSP =\n \"default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; \" +\n \"font-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'\";\n\n/** Content types for the file kinds a Vite build emits. Anything else is refused, not guessed. */\nconst CONTENT_TYPES: Readonly<Record<string, string>> = {\n \".html\": \"text/html; charset=utf-8\",\n \".js\": \"text/javascript; charset=utf-8\",\n \".mjs\": \"text/javascript; charset=utf-8\",\n \".css\": \"text/css; charset=utf-8\",\n \".json\": \"application/json; charset=utf-8\",\n \".map\": \"application/json; charset=utf-8\",\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".webp\": \"image/webp\",\n \".gif\": \"image/gif\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".txt\": \"text/plain; charset=utf-8\",\n};\n\n/**\n * Where the built UI lives.\n *\n * Derived from this module's own URL rather than configured, for the same reason issue #25 gives\n * for the installation path: a configured path would be correct for exactly one checkout \u2014 the\n * author's.\n *\n * There are **two** layouts, and a single relative path cannot serve both (issue #52):\n *\n * - **From source** \u2014 the built UI is `packages/ui/dist`, where `vite build` writes.\n * - **From the published bundle** \u2014 the built UI is `dist/ui`, where the build script copies it.\n *\n * The old single candidate (`../../ui/dist`, relative to THIS MODULE) is right for the first and\n * wrong for the second: from `bin/pi-central.js` it resolves to `<parent-of-package>/ui/dist`, which\n * is *outside the installed package* and therefore matches whatever the consumer happens to have\n * there. Deriving both candidates from the **package root** instead \u2014 found by name, via\n * `findOwnPackage` \u2014 means neither can escape the package, so a stray directory cannot be mistaken\n * for the UI. That is the same \"own installation\" resolution the self-hosting warning uses (#25).\n *\n * The first candidate containing an `index.html` wins, and **the order depends on the layout this\n * module was loaded from.** That is not decoration: both directories can exist at once, because a\n * checkout where someone has run `npm run build:package` has `dist/ui` (the packaged copy) AND\n * `packages/ui/dist` (the fresh `vite build` output). A fixed order would then serve the wrong one\n * \u2014 and the symptom is the worst kind: a developer edits a component, rebuilds, sees no change,\n * because the orchestrator has been serving a stale copy from the other directory all along.\n *\n * So a module loaded from source prefers the workspace output, and a bundled module prefers the\n * packaged one. When neither exists the same distinction decides which path the startup message\n * names, so a developer who has not built the UI yet is told about `packages/ui/dist`.\n *\n * `PI_CENTRAL_UI_DIST` overrides everything, which is what a test or an unusual install needs.\n *\n * `from` is injectable so the packaged shape can be tested without building a tarball: a test hands\n * it a `file:///\u2026/bin/pi-central.js` URL inside a temp package and asserts which directory comes\n * back.\n */\nexport function resolveUiDistDir(\n env: NodeJS.ProcessEnv = process.env,\n from: string = import.meta.url,\n): string {\n const explicit = env[\"PI_CENTRAL_UI_DIST\"];\n if (explicit !== undefined && explicit !== \"\") return resolve(explicit);\n\n const own = findOwnPackage(from);\n if (own === undefined) {\n // No owning package.json: fall back to the module-relative workspace path. Not an error \u2014 it\n // means an unusual layout, and the caller reports a missing UI the same way it always would.\n return fileURLToPath(new URL(\"../../ui/dist\", from));\n }\n\n const packaged = join(own.root, \"dist\", \"ui\");\n const workspace = join(own.root, \"packages\", \"ui\", \"dist\");\n // `runningFromSource` is a statement about where this module was loaded FROM, and it is what\n // resolves the case where both directories exist. A heuristic, but a checkable one, and getting it\n // wrong costs a developer a rebuild they cannot explain.\n const fromSource = runningFromSource(from);\n const candidates = fromSource ? [workspace, packaged] : [packaged, workspace];\n for (const candidate of candidates) {\n if (hasBuiltUi(candidate)) return candidate;\n }\n // Neither exists: a message problem, not a serving problem. Name the path this layout expects.\n return fromSource ? workspace : packaged;\n}\n\n/** Is `from` inside a `packages/<name>/src/` tree, i.e. a checkout running from source? */\nfunction runningFromSource(from: string): boolean {\n return /[\\\\/]packages[\\\\/][^\\\\/]+[\\\\/]src[\\\\/]/.test(fileURLToPath(from));\n}\n\nexport type StaticResolution =\n | { kind: \"file\"; path: string }\n | { kind: \"denied\"; reason: string };\n\n/**\n * Is `candidate` inside `root` (or equal to it)?\n *\n * Exported and tested directly, because it is a **backstop for a different defect class** than the\n * traversal ban, and the difference is not obvious \u2014 it was established by falsification, not by\n * reasoning. Removing the `..` ban and re-running the suite showed this check does *not* catch it:\n * `posix.normalize` **clamps** `..` at the root, so `/../../etc/passwd` normalizes to `/etc/passwd`\n * and then resolves *inside* the root to `<root>/etc/passwd`. A rooted path cannot normalize its\n * way out, so containment-on-the-normalized-path is never the thing that stops traversal. The\n * explicit `..` ban is load-bearing, and it is tested as such.\n *\n * What this does guard is the case where a candidate is passed in **absolute** \u2014 which is what a\n * refactor that stopped stripping the leading `/` would produce: `resolve(root, \"/etc/passwd\")` is\n * `/etc/passwd`, not `<root>/etc/passwd`. The `startsWith(root)` form also refuses a sibling whose\n * name merely begins with the root's (`/var/root-ui` against `/var/root`), which a string prefix\n * test would wave through.\n */\nexport function isInside(root: string, candidate: string): boolean {\n const rootPath = resolve(root);\n const target = resolve(candidate);\n if (target === rootPath) return true;\n return target.startsWith(rootPath.endsWith(sep) ? rootPath : rootPath + sep);\n}\n\n/**\n * Map a raw request target to a path inside `root`, or refuse it.\n *\n * Pure, and deliberately the whole security decision: the handler below only turns a result into\n * a response. Every branch is a test case rather than a code path someone has to remember.\n *\n * Two checks, and which one carries the weight is worth stating precisely, because the intuitive\n * answer is wrong:\n *\n * 1. Decode exactly once (a `%` that does not decode is a refusal, not a silent literal), then\n * reject any `..` segment. **This is the check that stops traversal.** `/../package.json` is\n * *denied* rather than quietly normalized to `/package.json` \u2014 an escape attempt and a typo\n * are different events, and only one of them should be reported.\n * 2. Canonicalize the joined path and require it to still be inside `root` (`isInside`). This is a\n * backstop for an *absolute* candidate \u2014 the shape a refactor produces if the leading-slash\n * strip below is ever dropped \u2014 and for a sibling whose name prefixes the root. It does **not**\n * substitute for check 1: `posix.normalize` clamps `..` at the root, so it can never produce\n * an escape from a rooted path. Verified by removing check 1 and watching the suite fail.\n */\nexport function resolveStaticPath(root: string, requestTarget: string): StaticResolution {\n const rootPath = resolve(root);\n\n // Strip the query and fragment before anything else: `?x=../..` is not a path.\n const target = requestTarget.split(\"?\")[0]!.split(\"#\")[0]!;\n\n let decoded: string;\n try {\n decoded = decodeURIComponent(target);\n } catch {\n // A malformed escape (`%zz`, a truncated `%2`) is a refusal. `decodeURIComponent` throwing is\n // the honest signal that this is not a path we understand.\n return { kind: \"denied\", reason: \"the request target is not valid percent-encoding\" };\n }\n\n if (decoded.includes(\"\\0\")) return { kind: \"denied\", reason: \"the request target contains a NUL byte\" };\n if (!decoded.startsWith(\"/\")) return { kind: \"denied\", reason: \"the request target is not an absolute path\" };\n\n // Check 1: an explicit traversal segment. Reported, not normalized away.\n if (decoded.split(\"/\").includes(\"..\")) {\n return { kind: \"denied\", reason: \"the request target contains a '..' segment\" };\n }\n\n // `posix.normalize` (not `path.normalize`): the wire format is always slash-separated, even on\n // a platform whose filesystem separator is not. It also collapses `//` and `.` segments.\n //\n // Note that it CLAMPS a leading `..` at the root: `/../../x` becomes `/x`, not an escape. That is\n // why check 1 above cannot be dropped in favour of this one \u2014 see the note on `isInside`.\n const relative = posix.normalize(decoded).replace(/^\\/+/, \"\");\n\n // Check 2: containment, on the canonical path. Guards an absolute candidate and a prefix-named\n // sibling; it is not what stops `..`.\n const candidate = resolve(rootPath, relative);\n if (!isInside(rootPath, candidate)) {\n return { kind: \"denied\", reason: \"the request target resolves outside the served UI root\" };\n }\n return { kind: \"file\", path: candidate };\n}\n\nexport interface UiServerOptions {\n /** The directory to serve. Usually `resolveUiDistDir()`. */\n root: string;\n /** Overridable only so a test can prove the drift guard fires; production uses `UI_CSP`. */\n csp?: string;\n}\n\n/**\n * Build the HTTP request handler for the built UI.\n *\n * Routing rules, and why each one is what it is:\n *\n * - An existing file is served. That is the whole point.\n * - A path that looks like a **client-side route** (`/history`, or any path whose last segment\n * has no dot) falls back to `index.html`, so a reload on a route is not a 404. A path that\n * looks like an **asset** (`.js`, `.css`, `.png`) does not: falling back there would answer a\n * missing script with an HTML document, and the browser would report a MIME-type error\n * instead of the 404 that names the actual problem.\n * - A missing build is a 500 that names the fix, never a 404 or a blank page. \"The UI is not\n * built\" and \"that file does not exist\" are different facts and must not look the same.\n */\nexport function createUiRequestHandler(options: UiServerOptions): (req: IncomingMessage, res: ServerResponse) => void {\n const root = resolve(options.root);\n const csp = options.csp ?? UI_CSP;\n const indexPath = resolve(root, \"index.html\");\n\n return (req, res) => {\n const method = req.method ?? \"GET\";\n if (method !== \"GET\" && method !== \"HEAD\") {\n res.writeHead(405, {\n \"content-type\": \"text/plain; charset=utf-8\",\n allow: \"GET, HEAD\",\n \"content-security-policy\": csp,\n });\n res.end(\"this listener serves GET and HEAD only\\n\");\n return;\n }\n\n const resolution = resolveStaticPath(root, req.url ?? \"/\");\n if (resolution.kind === \"denied\") {\n // 403, not 404: the file may well exist, and pretending otherwise hides an attempt to\n // reach outside the served root.\n sendText(res, method, 403, `refused: ${resolution.reason}\\n`, csp);\n return;\n }\n\n const path = resolution.path;\n if (isReadableFile(path)) {\n sendFile(res, method, path, csp);\n return;\n }\n\n // Not a file. Is this a client-side route, or a missing asset?\n const lastSegment = posix.basename(path);\n const looksLikeRoute = !lastSegment.includes(\".\") || path === root;\n if (looksLikeRoute) {\n if (isReadableFile(indexPath)) {\n sendFile(res, method, indexPath, csp);\n return;\n }\n sendText(\n res,\n method,\n 500,\n \"The UI has not been built, so there is nothing to serve.\\n\\n\" +\n \"Build it with:\\n npm run ui:build\\n\\n\" +\n `(looked for ${indexPath})\\n`,\n csp,\n );\n return;\n }\n\n sendText(res, method, 404, `no such file under the served UI root: ${posix.basename(path)}\\n`, csp);\n };\n}\n\nfunction isReadableFile(path: string): boolean {\n try {\n return statSync(path).isFile();\n } catch {\n return false;\n }\n}\n\nfunction sendText(res: ServerResponse, method: string, status: number, body: string, csp: string): void {\n const bytes = Buffer.byteLength(body);\n res.writeHead(status, {\n \"content-type\": \"text/plain; charset=utf-8\",\n \"content-length\": String(bytes),\n \"x-content-type-options\": \"nosniff\",\n \"content-security-policy\": csp,\n });\n // A HEAD response carries the headers and the length, and no body.\n if (method === \"HEAD\") res.end();\n else res.end(body);\n}\n\nfunction sendFile(res: ServerResponse, method: string, path: string, csp: string): void {\n const info = statSync(path);\n const type = CONTENT_TYPES[extname(path).toLowerCase()] ?? \"application/octet-stream\";\n // Vite fingerprints asset filenames, so their contents cannot change under a stable name and\n // they can be cached indefinitely. `index.html` names the current fingerprint set, so caching\n // it is exactly the mistake that serves a stale app after a rebuild.\n const immutable = path.includes(`${sep}assets${sep}`);\n res.writeHead(200, {\n \"content-type\": type,\n \"content-length\": String(info.size),\n \"content-security-policy\": csp,\n \"x-content-type-options\": \"nosniff\",\n \"cache-control\": immutable ? \"public, max-age=31536000, immutable\" : \"no-cache\",\n });\n if (method === \"HEAD\") {\n res.end();\n return;\n }\n // A stream, not a read into memory: an asset can be large, and a truncated read must surface as\n // a network error rather than a silently shortened body.\n const stream = createReadStream(path);\n stream.on(\"error\", () => res.destroy());\n stream.pipe(res);\n}\n\n/** Does a built UI exist at `root`? Used by the entry point to say so at startup. */\nexport function hasBuiltUi(root: string): boolean {\n return existsSync(resolve(root, \"index.html\"));\n}\n", "/**\n * Refuse to run unsandboxed work inside a sandbox (C1).\n *\n * Seatbelt does not nest: `sandbox-exec` inside an already-sandboxed process fails with\n * EPERM. Every session Pi Central spawns would therefore fail, and the failures would look\n * like ordinary spawn errors rather than a misinvocation. Phase 0 and `verify-three.ts`\n * both hit this, and both solve it by probing rather than by inspecting the environment.\n *\n * The probe is a **dry run of the real operation** \u2014 `sandbox-exec -p '(version 1)' -- true`\n * \u2014 rather than an environment check, because the environment has no reliable marker. A\n * harness that inferred \"am I sandboxed?\" from a variable would be asserting its own\n * assumption.\n *\n * There is one false negative worth naming: a sandbox that permits `sandbox-exec` would let\n * the probe succeed even though nesting is unwise. That direction is the safe one \u2014 the\n * cost is a confusing failure later, never a silently unsandboxed session.\n */\nimport { spawnSync } from \"node:child_process\";\n\n/** The exit code used when Pi Central refuses to start because it is itself sandboxed. */\nexport const SANDBOX_REFUSAL_CODE = 2;\n\nexport interface SandboxProbe {\n /** True when `sandbox-exec` can apply a trivial policy here, i.e. nesting works. */\n canNest: boolean;\n /** Diagnostic detail for the failure message. */\n detail: string;\n}\n\n/**\n * Probe whether a policy can be applied in this environment.\n *\n * Exported separately from the message so tests can exercise it directly. Callers that only\n * want to fail should use `assertNotSandboxed`.\n */\nexport function probeSandbox(execPath = \"/usr/bin/sandbox-exec\"): SandboxProbe {\n const result = spawnSync(execPath, [\"-p\", \"(version 1)(allow default)\", \"--\", \"/usr/bin/true\"], {\n encoding: \"utf8\",\n timeout: 10_000,\n });\n\n // A spawn failure (ENOENT) means no `sandbox-exec` at all: on a non-Darwin host, or in a\n // sanitized PATH. That is its own condition (C10) and not a nesting problem.\n if (result.error) {\n return { canNest: false, detail: `cannot run ${execPath}: ${result.error.message}` };\n }\n if (result.status !== 0) {\n const stderr = (result.stderr ?? \"\").trim();\n return {\n canNest: false,\n detail: stderr.length > 0 ? stderr : `exited ${result.status ?? \"on a signal\"}`,\n };\n }\n return { canNest: true, detail: \"ok\" };\n}\n\n/** Thrown so callers can distinguish \"refused to run\" from \"crashed while running\". */\nexport class SandboxedEnvironmentError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"SandboxedEnvironmentError\";\n }\n}\n\n/**\n * Refuse to proceed when a policy cannot be applied, with a message that says what to do.\n *\n * The message names the cause rather than the symptom: the previous, less specific failure\n * (\"spawn EPERM\") sent a reader looking for a bug in the launcher.\n */\nexport function assertNotSandboxed(execPath = \"/usr/bin/sandbox-exec\"): SandboxProbe {\n const probe = probeSandbox(execPath);\n if (probe.canNest) return probe;\n\n throw new SandboxedEnvironmentError(\n [\n \"Pi Central cannot start: this process is inside a sandbox, and Seatbelt does not nest (C1).\",\n \"\",\n ` probe: ${execPath} -p '(version 1)(allow default)' -- /usr/bin/true`,\n ` result: ${probe.detail}`,\n \"\",\n \"Every session would be spawned through `safehouse`, which applies a policy with\",\n \"`sandbox-exec` -- and that fails with EPERM here, so every session would fail for a\",\n \"reason that looks like a launcher bug.\",\n \"\",\n \"Run Pi Central from a plain shell (not a sandboxed terminal, editor task, or agent\",\n \"harness). The orchestrator is intentionally unsandboxed at the user's UID (ADR-0002).\",\n ].join(\"\\n\"),\n );\n}\n", "/**\n * Pi Central's entry point (Phase 2).\n *\n * Starts the orchestrator and its transport. This process is **deliberately unsandboxed**\n * (ADR-0002): it runs at the user's UID because Seatbelt does not nest (C1), and every\n * session it spawns is confined individually.\n *\n * The one hard precondition is therefore that *this* process is not itself inside a sandbox.\n * That is checked before anything else, because the failure it prevents is not loud: every\n * session would fail to spawn with EPERM and look like a launcher bug.\n */\nimport { defaultConfig } from \"./config.ts\";\nimport { Supervisor } from \"./supervisor.ts\";\nimport { Transport } from \"./transport.ts\";\nimport { assertNotSandboxed, SandboxedEnvironmentError } from \"./sandbox-guard.ts\";\nimport { hasBuiltUi, resolveUiDistDir } from \"./static-ui.ts\";\nimport { findOwnPackage, PACKAGE_NAME } from \"./package-meta.ts\";\n\ninterface Options {\n port: number;\n mode: \"sandboxed\" | \"direct\";\n /** Skip the sandbox probe. Only for a host without `sandbox-exec` at all (C10). */\n allowUnsandboxedHost: boolean;\n /** The built UI to serve, or `null` to serve the socket only (issue #24). */\n uiDir: string | null;\n /** `--version`: print and exit, without binding a port or touching config. */\n showVersion: boolean;\n /** `--help`: print and exit, likewise. */\n showHelp: boolean;\n}\n\n/**\n * The installed version, read from the shipped `package.json`.\n *\n * Never a constant: `--version` must report the version that was actually published, and the whole\n * point of asking is to establish which artifact is running. A restated number could be wrong, and a\n * wrong answer here poisons every bug report that follows it. `findOwnPackage` resolves the package\n * by NAME rather than taking the nearest `package.json`, so running from source does not report\n * `@pi-central/orchestrator`'s version by accident (see `package-meta.ts`).\n */\nfunction versionLine(): string {\n const own = findOwnPackage();\n if (own === undefined) return `${PACKAGE_NAME} (version unknown: no package.json found)`;\n return `${own.name} ${own.version}`;\n}\n\n/** What `--help` prints. Deliberately the flags that exist, and nothing aspirational. */\nfunction printHelp(): void {\n console.log(`${versionLine()}\\n\nUsage: ${PACKAGE_NAME} [options]\n\nOptions:\n --port <n> Port to listen on (default 8787; 0 asks for any free one)\n --direct Start sessions WITHOUT the sandbox (feasibility only)\n --ui-dist <dir> Serve a built UI from this directory\n --no-ui Serve the socket only (use with a dev server)\n --allow-unsandboxed-host Skip the sandbox probe, for a host with no sandbox-exec (C10)\n --version, -v Print the version and exit\n --help, -h Print this and exit\n\nPi Central starts sessions with \\`pi\\` under Agent Safehouse. Run it from a PLAIN terminal:\nSeatbelt does not nest, so a sandboxed shell cannot start a sandboxed session (C1).`);\n}\n\n/**\n * The default port.\n *\n * Fixed rather than ephemeral, because the UI's dev-server proxy has to know where to connect\n * and a random port would mean every `npm run server` needed a matching `PI_CENTRAL_WS`\n * override. A tool that requires two coordinated environment variables before it shows\n * anything is a tool that appears broken on first run. `--port` still overrides, and `--port 0`\n * still asks for an ephemeral port for the tests and for running several orchestrators side by\n * side.\n */\nconst DEFAULT_PORT = 8787;\n\nfunction parseArgs(argv: string[]): Options {\n const options: Options = {\n port: DEFAULT_PORT,\n mode: \"sandboxed\",\n allowUnsandboxedHost: false,\n uiDir: resolveUiDistDir(),\n showVersion: false,\n showHelp: false,\n };\n for (let i = 0; i < argv.length; i += 1) {\n const arg = argv[i];\n if (arg === \"--version\" || arg === \"-v\") {\n options.showVersion = true;\n } else if (arg === \"--help\" || arg === \"-h\") {\n options.showHelp = true;\n } else if (arg === \"--port\") {\n const value = Number(argv[++i]);\n // Accepts 0 explicitly: \"give me any free port\" is a real request, not a mistake.\n if (Number.isInteger(value) && value >= 0 && value <= 65535) options.port = value;\n else {\n console.error(`--port expects a number between 0 and 65535, got ${String(argv[i])}`);\n process.exit(2);\n }\n } else if (arg === \"--direct\") {\n options.mode = \"direct\";\n } else if (arg === \"--allow-unsandboxed-host\") {\n // Escape hatch for developing the UI on a non-macOS host, where Safehouse cannot run at\n // all (C10). Named explicitly so it can never be enabled by accident.\n options.allowUnsandboxedHost = true;\n } else if (arg === \"--ui-dist\") {\n const value = argv[++i];\n if (value === undefined || value === \"\") {\n console.error(\"--ui-dist expects a directory containing a built UI\");\n process.exit(2);\n }\n options.uiDir = value;\n } else if (arg === \"--no-ui\") {\n // Serve the socket only. Useful when a Vite dev server owns the UI, and when running a\n // second orchestrator beside one that already serves it.\n options.uiDir = null;\n }\n }\n return options;\n}\n\nasync function main(): Promise<number> {\n const options = parseArgs(process.argv.slice(2));\n\n // Answered before the sandbox probe and before any config is read. `--version` must work in a\n // sandbox, on a host with no Safehouse, and with no home directory \u2014 it is the question someone\n // asks when something is already wrong, so it must not be able to fail for the same reason.\n if (options.showVersion) {\n console.log(versionLine());\n return 0;\n }\n if (options.showHelp) {\n printHelp();\n return 0;\n }\n\n // Check BEFORE binding a port or reading config, so a misinvocation is the first thing the\n // user sees rather than a confusing spawn failure minutes later.\n if (!options.allowUnsandboxedHost) {\n try {\n assertNotSandboxed();\n } catch (error) {\n if (error instanceof SandboxedEnvironmentError) {\n console.error(error.message);\n return 2;\n }\n throw error;\n }\n }\n\n const config = defaultConfig();\n const supervisor = new Supervisor(config);\n const transport = new Transport(supervisor, config, { port: options.port, uiDir: options.uiDir });\n transport.attach();\n\n const address = await transport.listen();\n\n // Machine-readable line first, so a launcher or dev script can parse the port without\n // scraping the human output below.\n console.log(`PI_CENTRAL_URL=${address.url}`);\n console.log(`Pi Central listening on ${address.url}`);\n console.log(` sessions root: ${config.sessionsRoot}`);\n console.log(` default workspace: ${config.workspaceRoot}`);\n console.log(` agent root: ${config.agentRoot}${config.agentRootManaged ? \" (managed)\" : \" (external)\"}`);\n console.log(` isolation: ${options.mode}`);\n // Where Pi Central thinks its own installation is (issue #25). Printed because the per-session\n // warning is SILENT when the path cannot be identified \u2014 and \"no warning appeared\" would otherwise\n // be indistinguishable from \"the warning is broken\". Saying it once, here, makes the difference\n // between an unidentifiable path and a safe one visible.\n if (config.installationRoot === undefined) {\n console.log(\n `Self-hosting check: could not identify this installation's own directory, so sessions are\\n` +\n ` not warned when they point at it. Point a session at a checkout of Pi Central and you may\\n` +\n ` still hit the problems in docs/DOGFOODING.md.`,\n );\n } else {\n console.log(`Self-hosting check: this installation is ${config.installationRoot}`);\n }\n console.log(\"\");\n // The UI is served from this same origin (issue #24), so the dashboard needs no second process\n // and no dev server. If it has not been built, say so ONCE here \u2014 naming the fix \u2014 rather than\n // letting every request answer with the same message.\n const httpUrl = address.url.replace(/^ws/, \"http\");\n if (options.uiDir === null) {\n console.log(`UI: not served (--no-ui). Use a dev server, or drop the flag to serve a build.`);\n } else if (hasBuiltUi(options.uiDir)) {\n console.log(`Open ${httpUrl} \u2014 the UI is served from ${options.uiDir}`);\n } else {\n console.log(\n `No built UI at ${options.uiDir}\\n` +\n ` Build it with: npm run ui:build\\n` +\n ` Or serve the socket only with: --no-ui (and run npm run ui:dev)`,\n );\n }\n console.log(\"\");\n // The connection detail belongs here rather than in a README the user has to find: if the UI\n // cannot reach this port, nothing else matters. With a served build there is nothing to start \u2014\n // which is the point of issue #24 \u2014 so the advice is only printed when a dev server is actually\n // the way to get a UI.\n if (options.uiDir !== null && hasBuiltUi(options.uiDir)) {\n console.log(`Start a session from the UI at ${httpUrl}. Press Ctrl-C to stop every session and exit.`);\n } else if (options.port !== DEFAULT_PORT) {\n console.log(`Non-default port. Start the dev UI with:\\n PI_CENTRAL_WS=${address.url} npm run ui:dev`);\n console.log(\"\");\n console.log(\"Start a session from the UI. Press Ctrl-C to stop every session and exit.\");\n } else {\n console.log(`Start the dev UI in another terminal: npm run ui:dev`);\n console.log(\"\");\n console.log(\"Start a session from the UI. Press Ctrl-C to stop every session and exit.\");\n }\n\n // An orderly shutdown must close children too, or sandboxed grandchildren outlive us.\n let shuttingDown = false;\n const shutdown = async (signal: string): Promise<void> => {\n if (shuttingDown) return; // a second Ctrl-C must not re-enter this\n shuttingDown = true;\n console.log(`\\n${signal} received; stopping sessions...`);\n // The transport closes first so clients are told, rather than seeing a dead socket with\n // sessions still running behind it.\n await transport.close().catch(() => undefined);\n await supervisor.stopAll().catch(() => undefined);\n console.log(\"stopped.\");\n process.exit(0);\n };\n process.on(\"SIGINT\", () => void shutdown(\"SIGINT\"));\n process.on(\"SIGTERM\", () => void shutdown(\"SIGTERM\"));\n\n return 0;\n}\n\nmain().then(\n (code) => {\n // `main` returning 0 means \"listening and serving\"; the process stays alive on the socket.\n if (code !== 0) process.exit(code);\n },\n (error: unknown) => {\n console.error(`Pi Central failed to start: ${(error as Error).message}`);\n process.exit(1);\n },\n);\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["join", "resolve", "existsSync", "join", "existsSync", "join", "join", "resolve", "join", "EventEmitter", "existsSync", "mkdirSync", "join", "statSync", "join", "resolve", "resolve", "join", "statSync", "resolve", "accessSync", "constants", "statSync", "resolve", "accessSync", "constants", "join", "existsSync", "readFileSync", "join", "resolve", "sep", "match", "isInside", "resolve", "sep", "existsSync", "join", "readFileSync", "accessSync", "constants", "join", "gitconfigPath", "readdirSync", "readFileSync", "statSync", "join", "resolve", "sep", "sep", "label", "resolve", "readdirSync", "join", "statSync", "readFileSync", "EventEmitter", "mkdirSync", "join", "existsSync", "existsSync", "readFileSync", "join", "fileURLToPath", "readdirSync", "statSync", "join", "resolve", "readdirSync", "join", "statSync", "createReadStream", "existsSync", "statSync", "join", "resolve", "sep", "fileURLToPath", "resolve", "fileURLToPath", "join", "isInside", "sep", "statSync", "createReadStream", "existsSync", "resolve"]
|
|
7
|
+
}
|