@rubytech/create-maxy-code 0.1.83 → 0.1.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-memory-write-revoke.test.sh +142 -24
  3. package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +131 -218
  4. package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +85 -17
  5. package/payload/platform/plugins/cloudflare/PLUGIN.md +2 -11
  6. package/payload/platform/plugins/cloudflare/scripts/__tests__/setup-tunnel-display-precedence.test.sh +194 -0
  7. package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +43 -21
  8. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +29 -10
  9. package/payload/platform/plugins/docs/references/cloudflare.md +10 -9
  10. package/payload/platform/scripts/seed-neo4j.sh +12 -0
  11. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  12. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +13 -18
  13. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  14. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  15. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md +3 -1
  16. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/template.html +17 -7
  17. package/payload/server/{chunk-TFEIFU7F.js → chunk-BDFOTLPW.js} +1 -1
  18. package/payload/server/{chunk-W33UUYYR.js → chunk-BVDLDM6O.js} +1 -1
  19. package/payload/server/{chunk-VGMVEYX7.js → chunk-JRBCOVA4.js} +64 -5
  20. package/payload/server/{cloudflare-task-tracker-7P355PSD.js → cloudflare-task-tracker-M5ONAGUT.js} +2 -2
  21. package/payload/server/maxy-edge.js +2 -2
  22. package/payload/server/public/assets/Checkbox-B34vplI7.js +1 -0
  23. package/payload/server/public/assets/{admin-BHaOSIuo.js → admin-BkTlPuP0.js} +5 -5
  24. package/payload/server/public/assets/{arc-aUiRP9AS.js → arc-DMDAZHAN.js} +1 -1
  25. package/payload/server/public/assets/architecture-YZFGNWBL-COhEvUpo.js +1 -0
  26. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-B8o-_aT-.js → architectureDiagram-Q4EWVU46-DQMCKDU2.js} +1 -1
  27. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-BcprLXNk.js → blockDiagram-DXYQGD6D-CqJNtiQY.js} +1 -1
  28. package/payload/server/public/assets/{brand-kwAWaknq.js → brand-HqTMr1bV.js} +1 -1
  29. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-CAkKlTxa.js → c4Diagram-AHTNJAMY-BVAa1YL3.js} +1 -1
  30. package/payload/server/public/assets/channel-CZG4mVkt.js +1 -0
  31. package/payload/server/public/assets/{chunk-2KRD3SAO-BK3470lx.js → chunk-2KRD3SAO-Di4bO8ir.js} +1 -1
  32. package/payload/server/public/assets/chunk-336JU56O-XIsdqKyD.js +2 -0
  33. package/payload/server/public/assets/chunk-426QAEUC-Dw4mcMYK.js +1 -0
  34. package/payload/server/public/assets/{chunk-4BX2VUAB-BOvVdJLf.js → chunk-4BX2VUAB-DyEhFk-Z.js} +1 -1
  35. package/payload/server/public/assets/{chunk-4TB4RGXK-MLx8Jj8R.js → chunk-4TB4RGXK-BvrlG2FS.js} +1 -1
  36. package/payload/server/public/assets/{chunk-55IACEB6-BwZyF7vR.js → chunk-55IACEB6-BRJOZLpU.js} +1 -1
  37. package/payload/server/public/assets/{chunk-5FUZZQ4R-DFWTZhLd.js → chunk-5FUZZQ4R-3-UZ9191.js} +1 -1
  38. package/payload/server/public/assets/{chunk-5PVQY5BW-vTE3gFPo.js → chunk-5PVQY5BW-DOeY60Lp.js} +1 -1
  39. package/payload/server/public/assets/{chunk-67CJDMHE-D5bhMrtY.js → chunk-67CJDMHE-BG6-9r6c.js} +1 -1
  40. package/payload/server/public/assets/{chunk-7N4EOEYR-Si7Lgrwc.js → chunk-7N4EOEYR-BvMbitX7.js} +1 -1
  41. package/payload/server/public/assets/{chunk-AA7GKIK3-DMuHtDqO.js → chunk-AA7GKIK3-CE8mGBD5.js} +1 -1
  42. package/payload/server/public/assets/{chunk-BSJP7CBP-L79XKVcb.js → chunk-BSJP7CBP-DP7LTBll.js} +1 -1
  43. package/payload/server/public/assets/{chunk-CIAEETIT-C0O7Upmg.js → chunk-CIAEETIT-VfnIdN-h.js} +1 -1
  44. package/payload/server/public/assets/{chunk-EDXVE4YY-Bp1I_rQl.js → chunk-EDXVE4YY-BSbGSOxN.js} +1 -1
  45. package/payload/server/public/assets/{chunk-ENJZ2VHE-DsDBZ8WH.js → chunk-ENJZ2VHE-CszgAcWZ.js} +1 -1
  46. package/payload/server/public/assets/{chunk-FMBD7UC4-C_E43NFJ.js → chunk-FMBD7UC4-w-yBZsN2.js} +1 -1
  47. package/payload/server/public/assets/{chunk-FOC6F5B3-D9lWWHAu.js → chunk-FOC6F5B3-f9cFywhd.js} +1 -1
  48. package/payload/server/public/assets/{chunk-ICPOFSXX-BKO6vxzn.js → chunk-ICPOFSXX-rgw1I6pt.js} +2 -2
  49. package/payload/server/public/assets/{chunk-K5T4RW27-DuhsNH4c.js → chunk-K5T4RW27-B_ZUrFUq.js} +1 -1
  50. package/payload/server/public/assets/{chunk-KGLVRYIC-B4-A1Abi.js → chunk-KGLVRYIC-CcWTvRlI.js} +1 -1
  51. package/payload/server/public/assets/{chunk-LIHQZDEY-BxqgHRgT.js → chunk-LIHQZDEY-D-5-peQw.js} +1 -1
  52. package/payload/server/public/assets/{chunk-ORNJ4GCN-DEYQ5WaJ.js → chunk-ORNJ4GCN-B4Z5L25I.js} +1 -1
  53. package/payload/server/public/assets/{chunk-OYMX7WX6-D-ievGGw.js → chunk-OYMX7WX6-DL4v2Nx4.js} +1 -1
  54. package/payload/server/public/assets/chunk-QZHKN3VN-CWh_0JsP.js +1 -0
  55. package/payload/server/public/assets/{chunk-U2HBQHQK-D_5PbJhp.js → chunk-U2HBQHQK-CwMCwyfq.js} +1 -1
  56. package/payload/server/public/assets/{chunk-X2U36JSP-QmmoxQya.js → chunk-X2U36JSP-CvKfDVvq.js} +1 -1
  57. package/payload/server/public/assets/{chunk-XPW4576I-CBfZXZDB.js → chunk-XPW4576I-m1Y_r88I.js} +1 -1
  58. package/payload/server/public/assets/{chunk-YZCP3GAM-BXSWZZag.js → chunk-YZCP3GAM-D7vju2_U.js} +1 -1
  59. package/payload/server/public/assets/{chunk-ZZ45TVLE-oyzperET.js → chunk-ZZ45TVLE-sANOwJKn.js} +1 -1
  60. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BoqQ3pHg.js +1 -0
  61. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-FoKuBK7n.js +1 -0
  62. package/payload/server/public/assets/clone-CT8AM-bU.js +1 -0
  63. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-Br2gjtEO.js → cose-bilkent-S5V4N54A-BhtgY3T7.js} +1 -1
  64. package/payload/server/public/assets/{dagre-65Q3iWcL.js → dagre-CplqWwSs.js} +1 -1
  65. package/payload/server/public/assets/{dagre-KV5264BT-Cyc37LVU.js → dagre-KV5264BT-DNbl5g9o.js} +1 -1
  66. package/payload/server/public/assets/data-BIDM6J5V.js +1 -0
  67. package/payload/server/public/assets/{device-url-actions-BftdR4st.js → device-url-actions-nxh9PmIH.js} +1 -1
  68. package/payload/server/public/assets/{diagram-5BDNPKRD-DKItZE_3.js → diagram-5BDNPKRD-BCdMaYnY.js} +1 -1
  69. package/payload/server/public/assets/{diagram-G4DWMVQ6-DKlYJZby.js → diagram-G4DWMVQ6-8XhDBU0o.js} +1 -1
  70. package/payload/server/public/assets/{diagram-MMDJMWI5-DLqF5x2e.js → diagram-MMDJMWI5-M-RMQgtx.js} +1 -1
  71. package/payload/server/public/assets/{diagram-TYMM5635-Cdufrh7g.js → diagram-TYMM5635-BBHPIBvQ.js} +1 -1
  72. package/payload/server/public/assets/{erDiagram-SMLLAGMA-D6eSXJWj.js → erDiagram-SMLLAGMA-D6jOL-9s.js} +1 -1
  73. package/payload/server/public/assets/{flatten-Bo6YRmWl.js → flatten-BsWEYbBB.js} +1 -1
  74. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-BB4luljp.js → flowDiagram-DWJPFMVM-CiyTYRlU.js} +1 -1
  75. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-USBgsV4V.js → ganttDiagram-T4ZO3ILL-DHSIpjfS.js} +1 -1
  76. package/payload/server/public/assets/gitGraph-7Q5UKJZL-DeTNsAO0.js +1 -0
  77. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-BqyKsQZ9.js → gitGraphDiagram-UUTBAWPF-CEneVF3o.js} +1 -1
  78. package/payload/server/public/assets/graph-Cm-iWFqy.js +1 -0
  79. package/payload/server/public/assets/graph-labels-CFX6KkCz.js +1 -0
  80. package/payload/server/public/assets/{graphlib-C5MkULnV.js → graphlib-CuF7rLfE.js} +1 -1
  81. package/payload/server/public/assets/info-OMHHGYJF-DJJ9GlS6.js +1 -0
  82. package/payload/server/public/assets/infoDiagram-42DDH7IO-wGl-JoP7.js +2 -0
  83. package/payload/server/public/assets/{isEmpty-D6Kr-M1M.js → isEmpty-BWl67LAZ.js} +1 -1
  84. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CODjk_-5.js → ishikawaDiagram-UXIWVN3A-B3TxFNi_.js} +1 -1
  85. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-RyhxP8aB.js → journeyDiagram-VCZTEJTY-B5n8No7p.js} +1 -1
  86. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-jrm_4IpR.js → kanban-definition-6JOO6SKY-ByGF39_h.js} +1 -1
  87. package/payload/server/public/assets/{line-DsE8JMaX.js → line-Br0FyyFW.js} +1 -1
  88. package/payload/server/public/assets/{linear-DOh_6k2k.js → linear-0O14Y6uf.js} +1 -1
  89. package/payload/server/public/assets/{mermaid-parser.core-F27uElsX.js → mermaid-parser.core-BUcTjodx.js} +2 -2
  90. package/payload/server/public/assets/{mermaid.core-B0PzAHME.js → mermaid.core-BKzdtZgG.js} +3 -3
  91. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-GA-xLC9K.js → mindmap-definition-QFDTVHPH-CobGMF9l.js} +1 -1
  92. package/payload/server/public/assets/{ordinal-BDi6f4xk.js → ordinal-krseTxxN.js} +1 -1
  93. package/payload/server/public/assets/packet-4T2RLAQJ-CGbvGkvF.js +1 -0
  94. package/payload/server/public/assets/{page-vRUDyjk4.js → page-B5NRnW0t.js} +1 -1
  95. package/payload/server/public/assets/{page-CXFoiyRN.js → page-Ba0BAfpu.js} +2 -2
  96. package/payload/server/public/assets/pie-ZZUOXDRM-BZy8rjFn.js +1 -0
  97. package/payload/server/public/assets/{pieDiagram-DEJITSTG-CkP9U2tn.js → pieDiagram-DEJITSTG-DXFmGcNR.js} +1 -1
  98. package/payload/server/public/assets/{public-B_gqH5Cf.js → public-DWfBzf83.js} +5 -5
  99. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-sYIgx5bu.js → quadrantDiagram-34T5L4WZ-BnChStlO.js} +1 -1
  100. package/payload/server/public/assets/radar-PYXPWWZC-DcfWIVXr.js +1 -0
  101. package/payload/server/public/assets/{reduce-CGi9ik8i.js → reduce-tk-xY6Fv.js} +1 -1
  102. package/payload/server/public/assets/{requirementDiagram-MS252O5E-CKtMUXvK.js → requirementDiagram-MS252O5E-DdiBKsyO.js} +1 -1
  103. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-CP5qebN_.js → sankeyDiagram-XADWPNL6-16-np_56.js} +1 -1
  104. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-zvkjl2R6.js → sequenceDiagram-FGHM5R23-D67SCRpA.js} +1 -1
  105. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-D-1VwYNL.js → stateDiagram-FHFEXIEX-CvNJX0YC.js} +1 -1
  106. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CFw2U4iL.js +1 -0
  107. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BSd_xlMt.js → timeline-definition-GMOUNBTQ-BJE5YoBa.js} +1 -1
  108. package/payload/server/public/assets/treeView-SZITEDCU-3WugwVdj.js +1 -0
  109. package/payload/server/public/assets/treemap-W4RFUUIX-Cf5mDLlu.js +1 -0
  110. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-D1hxlYfF.js → vennDiagram-DHZGUBPP-DF17ddXt.js} +1 -1
  111. package/payload/server/public/assets/wardley-RL74JXVD-Bv4md4b3.js +1 -0
  112. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-yNOC0LD-.js → wardleyDiagram-NUSXRM2D-xn_2XUOo.js} +1 -1
  113. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-D9D_gyVi.js → xychartDiagram-5P7HB3ND-DVF8zf3f.js} +1 -1
  114. package/payload/server/public/data.html +4 -4
  115. package/payload/server/public/graph.html +5 -4
  116. package/payload/server/public/index.html +7 -6
  117. package/payload/server/public/public.html +4 -3
  118. package/payload/server/server.js +229 -22
  119. package/payload/server/public/assets/architecture-YZFGNWBL--v-pJPNp.js +0 -1
  120. package/payload/server/public/assets/channel-C9mhUrr8.js +0 -1
  121. package/payload/server/public/assets/chunk-336JU56O-DZ53mw2n.js +0 -2
  122. package/payload/server/public/assets/chunk-426QAEUC-3s5PxkHp.js +0 -1
  123. package/payload/server/public/assets/chunk-QZHKN3VN-Bd-GrQM6.js +0 -1
  124. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BXiYcaCJ.js +0 -1
  125. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BRxESuCS.js +0 -1
  126. package/payload/server/public/assets/clone-Bz40VsQ_.js +0 -1
  127. package/payload/server/public/assets/data-DU2ZQbl-.js +0 -1
  128. package/payload/server/public/assets/gitGraph-7Q5UKJZL-CI0s_tqn.js +0 -1
  129. package/payload/server/public/assets/graph-BgLMP7mY.js +0 -1
  130. package/payload/server/public/assets/graph-labels-CUN_9NnB.js +0 -1
  131. package/payload/server/public/assets/info-OMHHGYJF-g3gYW7Qm.js +0 -1
  132. package/payload/server/public/assets/infoDiagram-42DDH7IO-BF1spWbk.js +0 -2
  133. package/payload/server/public/assets/packet-4T2RLAQJ-CT0TB9HI.js +0 -1
  134. package/payload/server/public/assets/pie-ZZUOXDRM-CXLe7TFF.js +0 -1
  135. package/payload/server/public/assets/radar-PYXPWWZC-DnPLBl-D.js +0 -1
  136. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BGmOYY_E.js +0 -1
  137. package/payload/server/public/assets/treeView-SZITEDCU-C3cb7Xwe.js +0 -1
  138. package/payload/server/public/assets/treemap-W4RFUUIX-Dc7G3Bgm.js +0 -1
  139. package/payload/server/public/assets/wardley-RL74JXVD-DtgibWAt.js +0 -1
  140. /package/payload/server/public/assets/{_baseFor-Cam2PbSt.js → _baseFor-BHtDrjIo.js} +0 -0
  141. /package/payload/server/public/assets/{array-DYRGGQae.js → array-DetWRiSa.js} +0 -0
  142. /package/payload/server/public/assets/{cytoscape.esm-nWsJMTNI.js → cytoscape.esm-C9yNhe1u.js} +0 -0
  143. /package/payload/server/public/assets/{defaultLocale-Du1XY3Dp.js → defaultLocale-_WRwicXn.js} +0 -0
  144. /package/payload/server/public/assets/{dist-BzAsli7o.js → dist-Bd4S37oi.js} +0 -0
  145. /package/payload/server/public/assets/{init-B5BXBRcm.js → init-sTEcj9YX.js} +0 -0
  146. /package/payload/server/public/assets/{katex-HOUACuRw.js → katex-s61Rgv6l.js} +0 -0
  147. /package/payload/server/public/assets/{path-CNO468J-.js → path-B0Ik7Tu9.js} +0 -0
  148. /package/payload/server/public/assets/{rough.esm-DRO6hWPh.js → rough.esm-DKRO8IF-.js} +0 -0
  149. /package/payload/server/public/assets/{src-CWiyyVfn.js → src-B6XdH6xq.js} +0 -0
@@ -30,7 +30,7 @@ The plugin registers no agent-facing MCP tools. Every Cloudflare operation is dr
30
30
 
31
31
  | Script | Purpose |
32
32
  |---|---|
33
- | [`scripts/setup-tunnel.sh`](scripts/setup-tunnel.sh) | Autonomous end-to-end setup: OAuth login, tunnel resolve (operator-supplied identity), DNS route, config + state, service restart, post-restart verification. Invocation: `~/setup-tunnel.sh <brand> <port> <admin-hostname> [<public-hostname>] [<apex-hostname>]`. Required env: `STREAM_LOG_PATH`, `ACCOUNT_DIR`, AND exactly one of `TUNNEL_ID` (operator selected an existing tunnel from `/api/admin/cloudflare/tunnels`) or `TUNNEL_NAME` (operator typed a name to create) per the operator-selected-tunnel fix. The pre-fix derivation `${BRAND}-$(hostname -s)` is removed — the operator's logged-in Cloudflare account is the source of truth for which tunnel exists. Apex hostnames print an `ACTION REQUIRED` block for the dashboard record the CLI cannot create. Step 1 (wrappers faithfully relay third-party CLI) spawns `cloudflared tunnel login`, extracts the argotunnel URL from its stdout, mechanically opens it on the brand's VNC chromium using the install-time-resolved binary (`DISPLAY=${DISPLAY:-${BRAND_VNC_DISPLAY}} "${SETUP_TUNNEL_CHROMIUM_BIN}" <url> &` — `SETUP_TUNNEL_CHROMIUM_BIN` is read from `${MAXY_PLATFORM_ROOT}/config/chromium-binary.path` at script start so Ubuntu Noble laptop's snap-replaced Google Chrome is honoured by the install-time chromium resolver), then polls for `~/.cloudflared/cert.pem` while the operator clicks the zone row + Authorize on the VNC. 180 s budget with a 2-second `step=oauth-login result=awaiting-cert` heartbeat. No CDP auto-click, no DOM matcher. |
33
+ | [`scripts/setup-tunnel.sh`](scripts/setup-tunnel.sh) | Autonomous end-to-end setup: OAuth login, tunnel resolve (operator-supplied identity), DNS route, config + state, service restart, post-restart verification. Invocation: `~/setup-tunnel.sh <brand> <port> <admin-hostname> [<public-hostname>] [<apex-hostname>]`. Required env: `STREAM_LOG_PATH`, `ACCOUNT_DIR`, AND exactly one of `TUNNEL_ID` (operator selected an existing tunnel the agent enumerates them via `cloudflared tunnel list --output json` and presents the list in chat) or `TUNNEL_NAME` (operator typed a name to create) per the operator-selected-tunnel fix. The pre-fix derivation `${BRAND}-$(hostname -s)` is removed — the operator's logged-in Cloudflare account is the source of truth for which tunnel exists. Apex hostnames print an `ACTION REQUIRED` block for the dashboard record the CLI cannot create. Step 1 (wrappers faithfully relay third-party CLI) spawns `cloudflared tunnel login`, extracts the argotunnel URL from its stdout, mechanically opens it on the brand's VNC chromium using the install-time-resolved binary (`DISPLAY=${DISPLAY:-${BRAND_VNC_DISPLAY}} "${SETUP_TUNNEL_CHROMIUM_BIN}" <url> &` — `SETUP_TUNNEL_CHROMIUM_BIN` is read from `${MAXY_PLATFORM_ROOT}/config/chromium-binary.path` at script start so Ubuntu Noble laptop's snap-replaced Google Chrome is honoured by the install-time chromium resolver), then polls for `~/.cloudflared/cert.pem` while the operator clicks the zone row + Authorize on the VNC. 180 s budget with a 2-second `step=oauth-login result=awaiting-cert` heartbeat. No CDP auto-click, no DOM matcher. |
34
34
  | [`scripts/reset-tunnel.sh`](scripts/reset-tunnel.sh) | Deletes every tunnel on the brand's CF account and wipes `${CFG_DIR}`. Does not touch the platform service, stray CNAMEs, or token-mode connectors — those require dashboard cleanup or `pkill`. Invocation: `~/reset-tunnel.sh <brand>`. No polling blocks — every long-wait is bounded by `cloudflared`'s network round-trip, so no heartbeat contract applies. |
35
35
 
36
36
  ### Skills
@@ -49,18 +49,9 @@ The plugin registers no agent-facing MCP tools. Every Cloudflare operation is dr
49
49
 
50
50
  The agent loads these references on demand via `plugin-read` as the conversation requires. They are not auto-injected into the system prompt.
51
51
 
52
- ### UI contract
53
-
54
- VNC surfacing is post-navigation, never on form submit. Two sites call `useDeviceUrlActions().onShowVnc()` and both fire only after a successful CDP nav: `DeviceUrlBlock` (tool-output URL clicks) and `ActionLogPanel.handleOauthRespawn` (the "Re-open on Pi browser" button rendered when an OAuth URL appears in the action log). The form (`cloudflare-setup-form`) must NOT call `onShowVnc()` on POST resolve — pre-warming the fullscreen overlay before the OAuth URL is on the brand chromium hides the form, the ActionLogPanel, and the very button operators must click. The `[browser-viewer] event=mount surface="overlay"` line must appear within 5 s of `[device-url:click] navigateResult=ok`, never before.
55
-
56
52
  ### Error envelope contract
57
53
 
58
- Every `POST /api/admin/cloudflare/setup` failure returns a `CloudflareSetupError` carrying in addition to `field`, `message`, `output`, `correlationId`, `streamLogPath` two structured fields the form relays into the chat as a fenced JSON block:
59
-
60
- - `inputsAlreadyHeld: { admin?: string; public?: string; apex?: string }` — the FQDNs the route composed from the submit body before `err()` fired. Pre-validation failures emit `{}` so absence is itself deterministic.
61
- - `discoveryResults: { tunnels: { id; name }[]; domains: string[] }` — the last-known snapshot from the process-lifetime `discoveryCache` Map that `GET /tunnels` and `GET /domains` write to on success.
62
-
63
- The chat relay appends both as a fenced ```` ```json ```` block under "Held by deterministic tools (do not re-solicit)". The admin agent's reply restates those values verbatim, names the literal error, and stops — re-soliciting is a doctrine violation. The retry-on-redeployment decision is owned by the route (2026-05-13 doctrine fix): `POST /api/admin/cloudflare/setup` looks up the most recent terminal `:Task {kind:"cloudflare-tunnel-login"}` on the conversation via `findMostRecentTerminalCloudflareTask`, and when the running bundle's mtime post-dates that Task's `completedAt` the respawn proceeds; otherwise the route returns the prior Task's `errorMessage` verbatim as the new envelope. The agent does not branch on `bundleMtime` and never narrates it (`[admin-reply-scrub]` records any leakage under `llm-narrates-internal-state`). The rule lives in IDENTITY.md § "Post-deterministic-error reply contract" and `.docs/agents.md` § "Intent Gate — post-deterministic-error reply contract". The shared `TunnelEntry` shape lives in [`platform/ui/app/lib/cloudflare-setup-types.ts`](../../ui/app/lib/cloudflare-setup-types.ts).
54
+ `setup-tunnel.sh` emits structured `[tunnel-install] step=… result=… …` phase lines on stdout throughout its run and exits non-zero on failure. The agent streams stdout into chat verbatim as it arrives, names the literal exit code on failure, and cites `references/reset-guide.md` for the next action. The agent never paraphrases the script's output, never summarises an `ACTION REQUIRED` block, and never re-solicits values the operator already provided re-soliciting is a doctrine violation. The general form of the post-deterministic-error reply contract (literal-error + held-values restatement) lives in IDENTITY.md § "Post-deterministic-error reply contract" and `.docs/agents.md` § "Intent Gate — post-deterministic-error reply contract"; admin-reply token scrub at `platform/ui/app/lib/admin-reply-scrub.ts` catches narration leakage under `llm-narrates-internal-state`.
64
55
 
65
56
  ## Identity model
66
57
 
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env bash
2
+ # setup-tunnel.sh OAuth display precedence — three-case test.
3
+ #
4
+ # Verifies that the chromium spawn in Step 1 (OAuth login) ALWAYS uses the
5
+ # brand-declared VNC display from brand.json, never the PTY-inherited
6
+ # DISPLAY env var. Covers the regression filed as task 221: PTY admin
7
+ # agent inherits DISPLAY=:0 from systemd, which would route the OAuth
8
+ # Authorize page onto the system display rather than the operator-visible
9
+ # brand VNC.
10
+ #
11
+ # Cases:
12
+ # 1. Positive — brand.json vncDisplay=100, DISPLAY=:0 in env
13
+ # → stream log shows `step=browser-spawn result=ok display=:100`.
14
+ # 2. Negative — brand.json missing vncDisplay field, DISPLAY=:0 in env
15
+ # → exit non-zero with `result=error reason=brand-vnc-display-unresolved`.
16
+ # 3. Negative — brand.json file absent (MAXY_PLATFORM_ROOT set, file missing)
17
+ # → exit non-zero with same reason.
18
+ #
19
+ # Mocks cloudflared (echo URL + sleep), systemd-run (no-op success), and
20
+ # the brand chromium binary via PATH override. SETUP_TUNNEL_LOGIN_TIMEOUT=1
21
+ # short-circuits the cert-poll so the positive case exits in ~1 s.
22
+ #
23
+ # Run: bash setup-tunnel-display-precedence.test.sh
24
+
25
+ set -uo pipefail
26
+
27
+ TEST_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
28
+ SCRIPT_PATH="$(dirname "${TEST_DIR}")/setup-tunnel.sh"
29
+
30
+ if [ ! -x "${SCRIPT_PATH}" ] && [ ! -r "${SCRIPT_PATH}" ]; then
31
+ echo "FAIL: setup-tunnel.sh not found at ${SCRIPT_PATH}"
32
+ exit 1
33
+ fi
34
+
35
+ if ! command -v jq >/dev/null 2>&1; then
36
+ echo "SKIP: jq is required for brand.json vncDisplay parsing in setup-tunnel.sh"
37
+ exit 0
38
+ fi
39
+
40
+ PASS=0
41
+ FAIL=0
42
+
43
+ run_case() {
44
+ local case_name="$1"
45
+ local brand_json_content="$2" # empty means "do not write the file"
46
+ local expected_exit="$3" # "nonzero" or a specific code; we only check non-zero vs result via grep
47
+ local expected_grep="$4" # pattern to grep in stream log
48
+
49
+ WORK="$(mktemp -d -t maxy-display-test.XXXXXX)"
50
+ trap 'rm -rf "$WORK"' RETURN
51
+
52
+ # Fake $HOME for ${CFG_DIR}=${HOME}/.${BRAND}/cloudflared paths
53
+ export HOME="${WORK}/home"
54
+ mkdir -p "${HOME}/.testbrand/cloudflared"
55
+ # Ensure default OAuth cert path also empty so pre-flight cert-promotion is skipped
56
+ mkdir -p "${HOME}/.cloudflared"
57
+
58
+ # MAXY_PLATFORM_ROOT layout
59
+ export MAXY_PLATFORM_ROOT="${WORK}/platform"
60
+ mkdir -p "${MAXY_PLATFORM_ROOT}/config"
61
+ # Path INSIDE the file is the chromium binary; the file itself does not
62
+ # need to be executable. The script's check is `[ -x "${path}" ]` which
63
+ # requires an absolute path — `command -v true` returns the shell
64
+ # builtin name, not a path. Pick the first existing absolute file.
65
+ if [ -x /usr/bin/true ]; then TRUE_BIN=/usr/bin/true
66
+ elif [ -x /bin/true ]; then TRUE_BIN=/bin/true
67
+ else
68
+ echo "SKIP: no /usr/bin/true or /bin/true available for chromium-binary mock"
69
+ return 0
70
+ fi
71
+ printf '%s\n' "${TRUE_BIN}" > "${MAXY_PLATFORM_ROOT}/config/chromium-binary.path"
72
+
73
+ if [ -n "${brand_json_content}" ]; then
74
+ printf '%s' "${brand_json_content}" > "${MAXY_PLATFORM_ROOT}/config/brand.json"
75
+ fi
76
+
77
+ export STREAM_LOG_PATH="${WORK}/stream.log"
78
+ : > "${STREAM_LOG_PATH}"
79
+
80
+ # PATH-override mocks
81
+ PATH_DIR="${WORK}/bin"
82
+ mkdir -p "${PATH_DIR}"
83
+
84
+ cat > "${PATH_DIR}/cloudflared" <<'MOCK'
85
+ #!/usr/bin/env bash
86
+ # Mock cloudflared: emit a URL once, then sleep long enough that the
87
+ # parent's cert-poll timeout fires.
88
+ echo "Please open the following URL in your browser:"
89
+ echo "https://dash.cloudflare.com/argotunnel?aud=test&callback=https%3A%2F%2Flogin.cloudflareaccess.org%2F"
90
+ sleep 30
91
+ MOCK
92
+ chmod +x "${PATH_DIR}/cloudflared"
93
+
94
+ cat > "${PATH_DIR}/systemd-run" <<'MOCK'
95
+ #!/usr/bin/env bash
96
+ # Mock systemd-run: succeed without launching anything.
97
+ exit 0
98
+ MOCK
99
+ chmod +x "${PATH_DIR}/systemd-run"
100
+
101
+ cat > "${PATH_DIR}/systemctl" <<'MOCK'
102
+ #!/usr/bin/env bash
103
+ # Mock systemctl: stop returns 5 (unit auto-collected) on the happy
104
+ # `--user stop` path; list-unit-files prints nothing.
105
+ case "${*}" in
106
+ *"stop"*) exit 5 ;;
107
+ *) exit 0 ;;
108
+ esac
109
+ MOCK
110
+ chmod +x "${PATH_DIR}/systemctl"
111
+
112
+ # stdbuf shim — macOS lacks GNU coreutils stdbuf. The script invokes
113
+ # `stdbuf -oL -eL cloudflared ...` purely for line-buffering of the
114
+ # cloudflared output; in this test the output is one short URL line so
115
+ # buffering doesn't matter. The shim strips the -o/-e flags and execs
116
+ # the rest of argv directly.
117
+ cat > "${PATH_DIR}/stdbuf" <<'MOCK'
118
+ #!/usr/bin/env bash
119
+ while [ $# -gt 0 ]; do
120
+ case "$1" in
121
+ -i*|-o*|-e*) shift ;;
122
+ --) shift; break ;;
123
+ *) break ;;
124
+ esac
125
+ done
126
+ exec "$@"
127
+ MOCK
128
+ chmod +x "${PATH_DIR}/stdbuf"
129
+
130
+ # Force DISPLAY=:0 to simulate PTY admin agent inheriting from systemd.
131
+ export DISPLAY=":0"
132
+ # Short-circuit cert-poll so positive case finishes in ~1 s.
133
+ export SETUP_TUNNEL_LOGIN_TIMEOUT=1
134
+ # Required by Step 2+3, but Step 1 cert-poll timeout exits before we
135
+ # get there; still set them so any drift surfaces a different failure.
136
+ export TUNNEL_NAME="setup-tunnel-display-test"
137
+
138
+ PATH="${PATH_DIR}:${PATH}" "${SCRIPT_PATH}" \
139
+ testbrand 19200 admin.test.bot \
140
+ > "${WORK}/stdout.log" 2> "${WORK}/stderr.log" &
141
+ SCRIPT_PID=$!
142
+
143
+ # Bound the wait — kill after 30 s as a safety net.
144
+ for _ in $(seq 1 60); do
145
+ if ! kill -0 "${SCRIPT_PID}" 2>/dev/null; then break; fi
146
+ sleep 0.5
147
+ done
148
+ if kill -0 "${SCRIPT_PID}" 2>/dev/null; then
149
+ kill -TERM "${SCRIPT_PID}" 2>/dev/null || true
150
+ sleep 1
151
+ kill -KILL "${SCRIPT_PID}" 2>/dev/null || true
152
+ fi
153
+ wait "${SCRIPT_PID}" 2>/dev/null
154
+ ACTUAL_EXIT=$?
155
+
156
+ if grep -qE "${expected_grep}" "${STREAM_LOG_PATH}"; then
157
+ echo "PASS: ${case_name}"
158
+ PASS=$((PASS + 1))
159
+ else
160
+ echo "FAIL: ${case_name}"
161
+ echo " expected stream log to match: ${expected_grep}"
162
+ echo " exit=${ACTUAL_EXIT}"
163
+ echo " --- stream log ---"
164
+ sed 's/^/ /' "${STREAM_LOG_PATH}" || true
165
+ echo " --- stderr ---"
166
+ sed 's/^/ /' "${WORK}/stderr.log" || true
167
+ FAIL=$((FAIL + 1))
168
+ fi
169
+ }
170
+
171
+ # Case 1 — Positive. vncDisplay=100, DISPLAY=:0 inherited.
172
+ # Expect: chromium-spawn phase line carries display=:100, NOT :0.
173
+ run_case "positive — brand.json vncDisplay=100 overrides inherited DISPLAY=:0" \
174
+ '{"vncDisplay": 100}' \
175
+ "nonzero" \
176
+ 'step=browser-spawn result=ok.*display=:100'
177
+
178
+ # Case 2 — Negative. brand.json present but no vncDisplay field.
179
+ # Expect: hard-fail with reason=brand-vnc-display-unresolved.
180
+ run_case "negative — brand.json missing vncDisplay → hard-fail" \
181
+ '{"port": 19200}' \
182
+ "nonzero" \
183
+ 'step=browser-spawn result=error reason=brand-vnc-display-unresolved'
184
+
185
+ # Case 3 — Negative. brand.json file absent altogether.
186
+ # Expect: same hard-fail.
187
+ run_case "negative — brand.json file absent → hard-fail" \
188
+ '' \
189
+ "nonzero" \
190
+ 'step=browser-spawn result=error reason=brand-vnc-display-unresolved'
191
+
192
+ echo ""
193
+ echo "Summary: ${PASS} passed, ${FAIL} failed"
194
+ [ "${FAIL}" -eq 0 ]
@@ -16,8 +16,8 @@
16
16
  #
17
17
  # Step 1 owns the browser-spawn deterministically. The wrapper extracts
18
18
  # cloudflared's argotunnel URL from its stdout (regex below) and the moment
19
- # the URL surfaces, mechanically opens it on the Pi's VNC chromium via
20
- # `DISPLAY=${DISPLAY:-${BRAND_VNC_DISPLAY}} /usr/bin/chromium <url> &` — no reliance on
19
+ # the URL surfaces, mechanically opens it on the brand's VNC chromium via
20
+ # `DISPLAY=${BRAND_VNC_DISPLAY} /usr/bin/chromium <url> &` — no reliance on
21
21
  # cloudflared's optimistic xdg-open, no CDP auto-click, no DOM matcher.
22
22
  # The operator clicks the zone row + Authorize on the VNC themselves.
23
23
  # This is the wrappers-faithfully-relay-third-party-cli doctrine;
@@ -99,17 +99,22 @@ phase_line setup-tunnel step=start brand="${BRAND}" port="${PORT}" hostnames="${
99
99
  CFG_DIR="${HOME}/.${BRAND}/cloudflared"
100
100
  mkdir -p "${CFG_DIR}"
101
101
 
102
- # Per-brand X display. Each brand owns its own Xtigervnc display
103
- # so the Chromium tab opened to drive the OAuth Authorize click lands in
104
- # THIS brand's isolated VNC, not whichever brand booted first on a shared
105
- # device. Read from the brand manifest if MAXY_PLATFORM_ROOT is exported
106
- # (set by the systemd unit and by claude-agent::buildSpawnEnv); fall back
107
- # to the inherited DISPLAY env var, then to :99 for the default brand.
102
+ # Per-brand X display. Each brand owns its own Xtigervnc display so the
103
+ # Chromium tab opened to drive the OAuth Authorize click lands in THIS
104
+ # brand's isolated VNC, not whichever brand booted first on a shared
105
+ # device. brand.json `.vncDisplay` is the sole source of truth; the
106
+ # PTY-agent's inherited DISPLAY (typically `:0` from the systemd unit) is
107
+ # the WRONG surface and is never consulted. If brand.json is absent or
108
+ # does not declare a numeric `vncDisplay`, the chromium-spawn site in
109
+ # Step 1 hard-fails with `step=browser-spawn result=error
110
+ # reason=brand-vnc-display-unresolved` rather than silently falling back
111
+ # to a system display. Re-runs that skip Step 1 (cert.pem already in the
112
+ # brand path) tolerate an unresolved display because no chromium spawns.
108
113
  SETUP_TUNNEL_BRAND_JSON=""
109
114
  if [ -n "${MAXY_PLATFORM_ROOT:-}" ] && [ -f "${MAXY_PLATFORM_ROOT}/config/brand.json" ]; then
110
115
  SETUP_TUNNEL_BRAND_JSON="${MAXY_PLATFORM_ROOT}/config/brand.json"
111
116
  fi
112
- BRAND_VNC_DISPLAY=":99"
117
+ BRAND_VNC_DISPLAY=""
113
118
  if [ -n "${SETUP_TUNNEL_BRAND_JSON}" ] && command -v jq >/dev/null 2>&1; then
114
119
  _bvd=$(jq -r '.vncDisplay // empty' "${SETUP_TUNNEL_BRAND_JSON}" 2>/dev/null || true)
115
120
  if [ -n "${_bvd}" ] && [ "${_bvd}" -eq "${_bvd}" ] 2>/dev/null; then
@@ -185,8 +190,8 @@ fi
185
190
  # $STREAM_LOG_PATH with prefix [script:setup-tunnel:cloudflared]
186
191
  # (the chat-surface namespace — see _stream-log.sh header).
187
192
  # 2. Extract the authorize URL with a tolerant regex as it streams.
188
- # 3. The instant the URL is extracted, mechanically open it on the Pi
189
- # VNC chromium via `DISPLAY=${DISPLAY:-${BRAND_VNC_DISPLAY}} /usr/bin/chromium <url> &`.
193
+ # 3. The instant the URL is extracted, mechanically open it on the brand
194
+ # VNC chromium via `DISPLAY=${BRAND_VNC_DISPLAY} /usr/bin/chromium <url> &`.
190
195
  # Fire-and-forget — chromium is already running with CDP enabled at
191
196
  # :9222, so the invocation IPCs the URL into the running instance as
192
197
  # a new tab. The spawn is intentionally NOT tracked in the EXIT trap:
@@ -237,7 +242,22 @@ if [ ! -f "${CFG_DIR}/cert.pem" ] && [ -f "${HOME}/.cloudflared/cert.pem" ]; the
237
242
  fi
238
243
 
239
244
  if [ ! -f "${CFG_DIR}/cert.pem" ]; then
240
- phase_line setup-tunnel step=oauth-login cert_path="${CFG_DIR}/cert.pem" display="${DISPLAY:-${BRAND_VNC_DISPLAY}}"
245
+ # Brand owns the OAuth chromium display unconditionally. If brand.json
246
+ # didn't declare a numeric vncDisplay, the install is broken — refuse to
247
+ # spawn chromium on whatever DISPLAY the PTY agent happened to inherit
248
+ # (typically :0 from the systemd unit, which is NOT the operator-visible
249
+ # brand VNC). Doctrine block at top of file: brand wins, every time.
250
+ if [ -z "${BRAND_VNC_DISPLAY}" ]; then
251
+ phase_line setup-tunnel step=browser-spawn result=error \
252
+ reason=brand-vnc-display-unresolved \
253
+ brand_json="${SETUP_TUNNEL_BRAND_JSON:-unset}"
254
+ echo "ERROR: setup-tunnel.sh: brand VNC display could not be resolved." >&2
255
+ echo " brand.json: ${SETUP_TUNNEL_BRAND_JSON:-unset}" >&2
256
+ echo " Required: brand.json must declare numeric 'vncDisplay'." >&2
257
+ exit 1
258
+ fi
259
+
260
+ phase_line setup-tunnel step=oauth-login cert_path="${CFG_DIR}/cert.pem" display="${BRAND_VNC_DISPLAY}"
241
261
 
242
262
  URL_FILE="$(mktemp -t maxy-setup-tunnel-url.XXXXXX)"
243
263
  LAST_LINE_FILE="$(mktemp -t maxy-setup-tunnel-last.XXXXXX)"
@@ -267,7 +287,7 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
267
287
  # extracted as it streams. The subshell holds the whole pipeline so
268
288
  # PIPESTATUS[0] (cloudflared's exit code) is reachable later.
269
289
  (
270
- DISPLAY="${DISPLAY:-${BRAND_VNC_DISPLAY}}" stdbuf -oL -eL cloudflared \
290
+ DISPLAY="${BRAND_VNC_DISPLAY}" stdbuf -oL -eL cloudflared \
271
291
  --origincert "${CFG_DIR}/cert.pem" tunnel login 2>&1 |
272
292
  while IFS= read -r line; do
273
293
  ts="$(stream_log_ts)"
@@ -342,7 +362,7 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
342
362
  # `/usr/bin/chromium` here would re-introduce the AppArmor SingletonLock
343
363
  # failure on the laptop.
344
364
  CHROMIUM_UNIT="maxy-oauth-chromium-${BRAND}-$$.service"
345
- CHROMIUM_LAUNCH_DISPLAY="${DISPLAY:-${BRAND_VNC_DISPLAY}}"
365
+ CHROMIUM_LAUNCH_DISPLAY="${BRAND_VNC_DISPLAY}"
346
366
  CHROMIUM_SPAWN_ERR="$(mktemp -t maxy-oauth-chromium-err.XXXXXX)"
347
367
  if systemd-run --user \
348
368
  --unit="${CHROMIUM_UNIT}" \
@@ -462,12 +482,13 @@ fi
462
482
  # tunnel when the device is renamed while existing CNAMEs continue to
463
483
  # point at the old one.
464
484
  #
465
- # Contract: the form (rendered via /api/admin/cloudflare/tunnels list) is
466
- # the source of truth. The endpoint passes exactly one of:
485
+ # Contract: the operator's logged-in Cloudflare account is the source of
486
+ # truth. The agent enumerates existing tunnels via `cloudflared tunnel list
487
+ # --output json` (presented in chat as a numbered list) and then invokes
488
+ # this script with exactly one of:
467
489
  # TUNNEL_ID — operator selected an existing tunnel from the list
468
490
  # TUNNEL_NAME — operator typed a name to create a new tunnel
469
- # The endpoint validates the exactly-one constraint; the script enforces it
470
- # again here as defence in depth. Setting both, or neither, is a misuse.
491
+ # Setting both, or neither, is a misuse.
471
492
  # --------------------------------------------------------------------------
472
493
 
473
494
  if [ -n "${TUNNEL_ID:-}" ] && [ -n "${TUNNEL_NAME:-}" ]; then
@@ -478,9 +499,10 @@ fi
478
499
  if [ -z "${TUNNEL_ID:-}" ] && [ -z "${TUNNEL_NAME:-}" ]; then
479
500
  phase_line setup-tunnel step=tunnel-resolve result=error reason=neither-set
480
501
  echo "ERROR: TUNNEL_ID (selected) or TUNNEL_NAME (explicit-create) is required." >&2
481
- echo " The form derives one of these from operator input via" >&2
482
- echo " /api/admin/cloudflare/tunnels re-run the form, do not" >&2
483
- echo " invoke this script directly without one of the env vars." >&2
502
+ echo " The agent enumerates existing tunnels via" >&2
503
+ echo " 'cloudflared tunnel list --output json' and passes one of" >&2
504
+ echo " these env vars on invocation. Do not invoke this script" >&2
505
+ echo " directly without one of them set." >&2
484
506
  exit 1
485
507
  fi
486
508
 
@@ -26,13 +26,23 @@ Step 1's OAuth flow is a state machine over two observable variables: the brand-
26
26
 
27
27
  ### How inputs reach the script
28
28
 
29
- Inputs arrive through the `cloudflare-setup-form` component, not agent Q&A. The onboarding skill renders the form, the user submits admin/public/apex labels and the admin password in one action, and the `/api/admin/cloudflare/setup` endpoint runs (in this order):
29
+ The agent collects inputs in plain chat, then invokes the script via Bash. Four inputs are needed: admin FQDN, optional public FQDN, optional apex FQDN, and the admin password. Ask in a single combined message when all four are already known; ask one question at a time when anything is ambiguous.
30
30
 
31
- 1. `setRemotePassword(password)` — writes the hashed password.
32
- 2. `~/setup-tunnel.sh <brand> <port> <admin-fqdn> [<public-fqdn>] [<apex-fqdn>]` — brand and port are resolved server-side from `brand.json` and `process.env.PORT`, never form inputs.
33
- 3. For each submitted hostname that is not the admin FQDN and does not start with `public.`, writes it to `~/{configDir}/alias-domains.json` so `isPublicHost()` classifies it as public.
31
+ Sequence:
34
32
 
35
- The script itself is unchanged; only its invocation path moved from "agent collects Q&A then Bash" to "form submits, endpoint spawns".
33
+ 1. **Set the admin password** before invoking the script. Use the same endpoint onboarding uses `curl -X POST http://127.0.0.1:${PORT}/api/remote-auth/set-password -H 'Content-Type: application/json' -d '{"password":"…"}'` where `${PORT}` is read from `brand.json`.
34
+ 2. **(Optional) enumerate existing tunnels** when the operator wants to pick from the logged-in account rather than create a new one. Invoke `cloudflared tunnel list --output json` via Bash, present the result as a numbered list, and let the operator type the number (use that tunnel's `id`) or a fresh name.
35
+ 3. **Invoke the script** with `~/setup-tunnel.sh <brand> <port> <admin-fqdn> [<public-fqdn>] [<apex-fqdn>]`. Pass either `TUNNEL_ID=<uuid>` (operator selected) or `TUNNEL_NAME=<name>` (operator named a new one) in env — exactly one. `STREAM_LOG_PATH` and `ACCOUNT_DIR` are resolved from `brand.json` and the `${HOME}/.${BRAND}/` layout.
36
+ 4. **Alias-domain classification.** For each non-admin hostname that does not start with `public.`, append it to `~/{configDir}/alias-domains.json` so `isPublicHost()` treats it as public. The platform server watches that file (`watchFile`, ~2 s pickup) — no restart needed. Use this idempotent Bash command per hostname (HOST is the FQDN, FILE is `~/.${BRAND}/alias-domains.json`):
37
+
38
+ ```bash
39
+ FILE=~/.${BRAND}/alias-domains.json
40
+ mkdir -p "$(dirname "$FILE")"
41
+ [ -s "$FILE" ] || echo '[]' > "$FILE"
42
+ jq --arg h "$HOST" '. + [$h] | unique' "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE"
43
+ ```
44
+
45
+ Repeated calls with the same hostname are a no-op (the `| unique` filter dedupes). Mirrors the in-process `addAliasDomain()` helper at `platform/ui/app/lib/alias-domains.ts`.
36
46
 
37
47
  ### Invocation shape (reference)
38
48
 
@@ -88,13 +98,22 @@ The YAML and JSON rendering live in a pure Node helper at
88
98
  `scripts/__tests__/tunnel-ingress.test.ts` and invoked from the shell via
89
99
  `node --experimental-strip-types`.
90
100
 
91
- The agent does not invoke the script directly during onboarding — the endpoint does. The agent's responsibility is to render the form and relay the endpoint's script output verbatim when `_componentDone` arrives. If an `ACTION REQUIRED` block appears, quote it exactly — the operator needs the specific dashboard instructions it contains.
101
+ The agent invokes the script directly via the Bash tool there is no form, no endpoint relay. Stream the script's stdout into chat verbatim as it arrives; if an `ACTION REQUIRED` block appears, quote it exactly — the operator needs the specific dashboard instructions it contains.
102
+
103
+ ### Reading the stream log before narrating progress
104
+
105
+ Chat narration is contingent on phase-line evidence, not on the script's overall `result=ok`. The script tees structured phase lines into the stream log as it runs (`[setup-tunnel] step=<phase> <key=value …>`). The agent reads them and narrates from them:
106
+
107
+ - **Before claiming "the authorise page is open on the device's screen"** (or any equivalent), the agent must have seen `step=browser-spawn result=ok display=:<N>` in the stream log AND must have confirmed `:<N>` matches the brand VNC display recorded in `brand.json` `.vncDisplay` (the chat surface the operator watches). If the operator is on a Pi where the admin chat shares brand VNC `:100`, then `display=:100` is the only value consistent with "page is open on the device". Any other value means the page is on a screen the operator cannot see.
108
+ - **If `display=:<N>` does not match `vncDisplay`,** the agent surfaces the literal `OAUTH_URL: <url>` line from the script's stdout and asks the operator to open it in their shared browser themselves. No claim that the spawn succeeded. The script's chromium spawn is treated as best-effort that may have landed on the wrong screen.
109
+ - **If `step=browser-spawn result=error`,** the agent restates the literal `reason=…` and any `stderr=…` fields and stops, per the discipline rule below. The most common error in this site is `reason=brand-vnc-display-unresolved`, which means `brand.json` does not declare a numeric `vncDisplay` and the installer has to be re-run to fix the brand manifest before another setup attempt makes sense.
110
+ - **The script's `result=ok` flag alone is not evidence that the page is operator-visible.** The display field is the gate. Read it, compare it to brand state, then narrate.
92
111
 
93
112
  ### When the script exits non-zero
94
113
 
95
- The endpoint returns `{ ok: false, field: "script", message, output }` and the form surfaces the error inline. Relay the output to the user, name the exit code, and cite `references/reset-guide.md` for the next action. Offer to re-render the form after any manual steps the script's error output named. Do not attempt a second invocation outside the form, a Playwright-driven dashboard inspection, or an alternative `cloudflared` command sequence. The discipline rule below applies.
114
+ Relay the script's stdout to the operator verbatim, name the literal exit code, and cite `references/reset-guide.md` for the next action. Do not attempt a second invocation under a different flag combination, a Playwright-driven dashboard inspection, or an alternative `cloudflared` command sequence. The discipline rule below applies.
96
115
 
97
- When the failure reason is `timeout-waiting-cert` (operator did not click Authorize within the 180 s budget), the form surfaces the timeout and the operator can re-submit. The page is still on the Pi VNC; the operator can click Authorize there and the next form submit will complete via the cert-promotion pre-flight (the cert lands in `~/.cloudflared/cert.pem` after consent, and the wrapper's `mv` runs on the next invocation). Do not suggest `~/reset-tunnel.sh` — the cert path is intact and a fresh attempt is the only remediation needed.
116
+ When the failure reason is `timeout-waiting-cert` (operator did not click Authorize within the 180 s budget), the page is still on the Pi VNC; the operator can click Authorize there and a fresh `~/setup-tunnel.sh` invocation will complete via the cert-promotion pre-flight (the cert lands in `~/.cloudflared/cert.pem` after consent, and the wrapper's `mv` runs on the next invocation). Do not suggest `~/reset-tunnel.sh` — the cert path is intact and a fresh attempt is the only remediation needed.
98
117
 
99
118
  ---
100
119
 
@@ -132,7 +151,7 @@ Example:
132
151
 
133
152
  Use this when the operator needs to do something only the Cloudflare dashboard can do: sign in, switch accounts, add a site, edit an apex CNAME, verify zone nameservers, delete a tunnel after stopping its replicas. The guide has one numbered click-path per operation. Quote the relevant click-path verbatim — the operator follows it in the browser. The agent does not drive dashboard mutations via Playwright or Chrome DevTools.
134
153
 
135
- The single exception is `list-cf-domains.sh`, which reads the domains attached to the logged-in account to populate the `cloudflare-setup-form` dropdowns. That script is deterministic (bash + raw CDP, no LLM in the decision path), invoked only by the `/api/admin/cloudflare/domains` route, and produces only a JSON `string[]` on stdout; no dashboard state is changed. Any dashboard scrape that is not this exact script is forbidden — the agent does not extend this carve-out to new scripts it writes, hypothesises, or finds. Adding a new sanctioned scrape surface requires a code change reviewed as a doctrine change, not an inline agent decision.
154
+ The single exception is `list-cf-domains.sh`, which reads the domains attached to the logged-in account so the agent can present them in chat (e.g. when the operator needs to pick which domain to route a hostname under). The agent invokes the script directly via Bash with `list-cf-domains.sh <brand>` — no route wrapper. The script is deterministic (bash + raw CDP, no LLM in the decision path) and produces only a JSON `string[]` on stdout; no dashboard state is changed. Any dashboard scrape that is not this exact script is forbidden — the agent does not extend this carve-out to new scripts it writes, hypothesises, or finds. Adding a new sanctioned scrape surface requires a code change reviewed as a doctrine change, not an inline agent decision.
136
155
 
137
156
  ---
138
157
 
@@ -144,6 +163,6 @@ When the operator's request touches Cloudflare, the agent's permitted actions ar
144
163
  - Quote `references/manual-setup.md`, `references/reset-guide.md`, or `references/dashboard-guide.md`.
145
164
  - Verify reachability via plain HTTP (`curl -I https://<hostname>`).
146
165
 
147
- The agent does not drive Cloudflare dashboard mutations via Playwright or Chrome DevTools. The single sanctioned read-only scrape is `list-cf-domains.sh`, invoked only by the `/api/admin/cloudflare/domains` route — the LLM is not in its decision path. No other dashboard-automation surface is permitted; the agent does not generalise this exception to new scripts. The agent does not synthesise `cloudflared` flag combinations from web search or prior training. The agent does not call Cloudflare API or SDK from any language. The agent does not write or mutate `cert.pem`, `tunnel.state`, `config.yml`, or `alias-domains.json` directly — `setup-tunnel.sh` and the operator manage those files.
166
+ The agent does not drive Cloudflare dashboard mutations via Playwright or Chrome DevTools. The single sanctioned read-only scrape is `list-cf-domains.sh`, invoked directly via Bash — the LLM is not in its decision path. No other dashboard-automation surface is permitted; the agent does not generalise this exception to new scripts. The agent does not synthesise `cloudflared` flag combinations from web search or prior training. The agent does not call Cloudflare API or SDK from any language. The agent does not write or mutate `cert.pem`, `tunnel.state`, `config.yml`, or `alias-domains.json` directly — `setup-tunnel.sh` and the operator manage those files.
148
167
 
149
168
  When a sanctioned surface fails, the agent reports the failure with the exact output, cites the recovery step from `references/reset-guide.md`, and stops. Improvisation — "let me try a different flag" or "let me check the dashboard myself" — is the behaviour this rule exists to prevent. See IDENTITY.md § Cloudflare operations for the unconditional form.
@@ -8,33 +8,34 @@ Each installation has its own Cloudflare account. Sign-in is OAuth in the device
8
8
  |------|--------|
9
9
  | **Product identity** (Maxy vs Real Agent) | `brand.json` (`productName`, `configDir`) — known at install. |
10
10
  | **Cloudflare account identity** | `cert.pem` from OAuth. One account per brand per device. |
11
- | **Domain scope** (which zones the operator can route) | Live Cloudflare dashboard at form-render time via `list-cf-domains.sh`, not `brand.json`. Brand identity has no authority over which domains the operator's CF account holds. When the scrape returns an unexpected count (e.g. 1 on a two-zone account), the stream log's per-poll `phase=dom-scrape-poll n=<k> count=<n> domains=[…]` trajectory + the on-disk HTML dump at `~/{configDir}/logs/list-cf-domains-<ts>-count<n>-<mode>-pid<pid>.html` (earlier platform fixes — written on every scrape outcome, not just empty ones) give the operator everything they need to triage the cause without re-running. |
12
- | **CDP port the scrape attaches to** | `brand.json` (`cdpPort`, stamped at install time) — the same brand-scoped port `vnc.sh` binds Chromium to. `list-cf-domains.sh <brand>` requires the brand arg; the helper reads `${MAXY_PLATFORM_ROOT}/config/brand.json` (no silent default). Missing brand arg, missing brand.json, or missing `cdpPort` field each exit 1 with one of three named reasons (`brand-arg-missing`, `brand-config-missing`, `cdp-port-unresolved`); the route maps all three to `field=config` so the form renders a config card naming the brand instead of a Retry button — retrying would re-fire the same wrongly-resolved spawn. |
11
+ | **Domain scope** (which zones the operator can route) | Live Cloudflare dashboard at script-invocation time via `list-cf-domains.sh`, not `brand.json`. Brand identity has no authority over which domains the operator's CF account holds. When the scrape returns an unexpected count (e.g. 1 on a two-zone account), the stream log's per-poll `phase=dom-scrape-poll n=<k> count=<n> domains=[…]` trajectory + the on-disk HTML dump at `~/{configDir}/logs/list-cf-domains-<ts>-count<n>-<mode>-pid<pid>.html` (earlier platform fixes — written on every scrape outcome, not just empty ones) give the operator everything they need to triage the cause without re-running. |
12
+ | **CDP port the scrape attaches to** | `brand.json` (`cdpPort`, stamped at install time) — the same brand-scoped port `vnc.sh` binds Chromium to. `list-cf-domains.sh <brand>` requires the brand arg; the helper reads `${MAXY_PLATFORM_ROOT}/config/brand.json` (no silent default). Missing brand arg, missing brand.json, or missing `cdpPort` field each exit 1 with one of three named reasons (`brand-arg-missing`, `brand-config-missing`, `cdp-port-unresolved`); the agent reads the exit-code reason from stderr, names the brand, and stops — retrying would re-fire the same wrongly-resolved spawn. |
13
13
  | **Local tunnel state** | `~/{configDir}/cloudflared/` — `cert.pem`, `<UUID>.json`, `config.yml`, `tunnel.state`, `alias-domains.json`. |
14
14
 
15
15
  There is no token-based auth for the operator-owned path (Mode A). To switch Cloudflare accounts, run `reset-tunnel.sh` (which deletes the cert and every tunnel on the current account), then run `setup-tunnel.sh` again — `cloudflared tunnel login` inside the setup script will pick a fresh account when you sign in.
16
16
 
17
17
  ## Setup flow
18
18
 
19
- Ask the agent to set up Cloudflare. The agent first confirms the domain is already on your Cloudflare account (if not, it quotes the dashboard click-path — see below) and then renders a single form in chat. The form collects everything in one submission:
19
+ Ask the agent to set up Cloudflare. The agent confirms the domain is already on your Cloudflare account (if not, it quotes the dashboard click-path — see below) and then collects the inputs in plain chat:
20
20
 
21
21
  - **Admin address** — the hostname that will serve the admin chat (e.g. `admin.yourdomain.com`).
22
22
  - **Public address** — optional hostname for the public agent (e.g. `public.yourdomain.com` or `chat.yourdomain.com`).
23
23
  - **Proxy apex** — optional bare-domain hostname (e.g. `yourdomain.com`) that should also serve the public agent.
24
24
  - **Admin password** — the password used to gate remote access to the admin surface.
25
25
 
26
- When you submit, the `/api/admin/cloudflare/setup` endpoint runs in strict order `setRemotePassword`, launches a `cloudflare-setup` action (earlier platform fixes: `systemd-run --user` transient unit wrapping `setup-tunnel.sh <brand> <port> <hostname...>`), and registers a post-exit handler that writes alias-domains for every non-`public.*` public or apex hostname (so e.g. `chat.yourdomain.com` is classified as public by `isPublicHost`) and closes the audit Task. When the post-exit handler observes the systemd-run unit gone before its terminal status was sampled (a known systemd-run cgroup race), it consults `tunnel.state` on disk and verifies the tunnel identity matches the form you submitted; if so, the audit Task closes `completed` despite the missed observation. Otherwise it closes `failed` with `endpoint-died-pre-reconcile` and the boot reconciler may still recover the Task at next restart. The script runs end-to-end:
26
+ Once the agent has the four inputs, it sets the admin password via `curl -X POST http://127.0.0.1:${PORT}/api/remote-auth/set-password` (the same endpoint onboarding uses), optionally enumerates existing tunnels via `cloudflared tunnel list --output json` so you can pick from the logged-in account, and then invokes `~/setup-tunnel.sh <brand> <port> <admin-hostname> [<public-hostname>] [<apex-hostname>]` via the Bash tool with either `TUNNEL_ID=<uuid>` (selected) or `TUNNEL_NAME=<name>` (new) in env. After the script returns, the agent appends each non-`public.*` public or apex hostname to `~/{configDir}/alias-domains.json` so `isPublicHost()` classifies it as public.
27
27
 
28
- - `cloudflared tunnel login` — OAuth browser sign-in. The VNC browser opens the Cloudflare authorize page; pick the account that owns your domain, click Authorize. `cert.pem` lands.
29
- - Tunnel resolution from operator-supplied identity (operator-selected-tunnel fix). The form populates a tunnel-select dropdown from `GET /api/admin/cloudflare/tunnels` (which calls `cloudflared tunnel list --output json` on your logged-in account). You either pick an existing tunnel from the list or type a name to create a new one. The form posts EXACTLY ONE of `{tunnelId, tunnelName}`; the script enforces the same constraint as defence in depth. Pre-fix the script derived `${BRAND}-$(hostname -s)` locally; that broke the operator-state-is-authoritative doctrine and silently created orphan tunnels whenever the device hostname changed. Stream log emits `step=tunnel-resolve source=operator-selected|operator-created tunnel_id=… tunnel_name=…` once the UUID is known.
28
+ The script runs end-to-end:
29
+
30
+ - `cloudflared tunnel login` — OAuth browser sign-in. The script spawns the brand's VNC chromium pointed at the Cloudflare authorize page; pick the account that owns your domain, click Authorize. `cert.pem` lands.
31
+ - Tunnel resolution from operator-supplied identity (operator-selected-tunnel fix). The script requires EXACTLY ONE of `TUNNEL_ID` or `TUNNEL_NAME` in env and exits 1 with a literal error on either-both-set or neither-set. Pre-fix the script derived `${BRAND}-$(hostname -s)` locally; that broke the operator-state-is-authoritative doctrine and silently created orphan tunnels whenever the device hostname changed. Stream log emits `step=tunnel-resolve source=operator-selected|operator-created tunnel_id=… tunnel_name=…` once the UUID is known.
30
32
  - **Zone pre-flight** — for every non-apex hostname the script queries `1.1.1.1` for the registrable parent's NS records and refuses the whole run if they don't point at Cloudflare. Stream log: `step=zone-preflight result=ok|error zones_on_account=… missing_parent_for=…`. Catches "domain not on Cloudflare"; does not catch "domain on a different Cloudflare account than `cert.pem` is bound to" — that case surfaces later via `tunnel-status`.
31
33
  - `cloudflared tunnel route dns` for each subdomain hostname. Apex hostnames cannot be routed this way — the script prints an **ACTION REQUIRED** block naming the exact dashboard record to add or edit. Stream log emits `step=route-dns hostname=… tunnel_id=…` before the call and `step=route-dns hostname=… result=ok|apex-skip|error` after; on error the bounded cloudflared stderr (≤400 chars) rides in the same phase line. **The script does not parse cloudflared's stdout** — exit code is the sole decision signal, so all three legitimate cloudflared output shapes (new record, overwrite, idempotent "already configured") are treated as success.
32
34
  - `config.yml` and `tunnel.state` written under `${CFG_DIR}`.
33
- - **Post-restart resume contract** when the script exits cleanly, the form fires a client-side resume event. The chat hook ([useAdminChat.ts](../../../../platform/ui/app/useAdminChat.ts)) waits for the brand-service to come back via `/api/health` (down-then-up), re-binds the conversation to the new server process, and sends the "Cloudflare setup completed" marker as a normal hidden chat POST that re-invokes the agent in a fresh session. No relay queue, no boot-drain, no banner. Diagnostic: `grep '\[admin-resume\] reason=post-restart' ~/{configDir}/logs/server.log` (expect one line per restart cycle), `grep '\[client-event\] kind=post-restart-resume' ~/{configDir}/logs/server.log` for the operator-visible client trace. See `.docs/web-chat.md` "Post-restart resume contract" for the full client/server contract.
34
- - `systemctl --user restart ${BRAND}.service` — restarts the platform service so the new tunnel spawns via the service's `ExecStartPre=resume-tunnel.sh`.
35
+ - `systemctl --user restart ${BRAND}.service`restarts the platform service so the new tunnel spawns via the service's `ExecStartPre=resume-tunnel.sh`. The restart fires a few seconds after the script exits so the script does not kill its own cgroup when invoked via the Bash tool; the chat UI receives a `server_shutdown` SSE frame and reconnects automatically.
35
36
  - Post-restart verification — `ps -ef | grep '[c]loudflared'` confirms the connector is alive, then `curl -I https://<hostname>` against each subdomain (up to 60 s per host) confirms a non-530 response.
36
37
 
37
- When the endpoint returns, the form surfaces the script's output and the agent relays it verbatim in chat, including any `ACTION REQUIRED` block. If something fails along the way, the form stays open with a field-scoped error (admin label, public label, password, or script exit) so you can correct and retry without re-entering everything.
38
+ The agent streams the script's stdout into chat verbatim as it arrives, including any `ACTION REQUIRED` block. If the script exits non-zero, the agent names the literal exit code and cites `reset-guide.md` for the next action no retry under a different flag, no Playwright-driven dashboard inspection.
38
39
 
39
40
  ## Getting a domain on Cloudflare
40
41
 
@@ -166,6 +166,18 @@ cat > "$ACCOUNT_SETTINGS" << SETTINGS_EOF
166
166
  { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
167
167
  ]
168
168
  },
169
+ {
170
+ "matcher": "mcp__plugin_memory_memory__memory-write",
171
+ "hooks": [
172
+ { "type": "command", "command": "bash $HOOKS_PATH/pre-tool-use.sh admin" }
173
+ ]
174
+ },
175
+ {
176
+ "matcher": "mcp__plugin_memory_memory__memory-update",
177
+ "hooks": [
178
+ { "type": "command", "command": "bash $HOOKS_PATH/pre-tool-use.sh admin" }
179
+ ]
180
+ },
169
181
  {
170
182
  "hooks": [
171
183
  { "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
@@ -1 +1 @@
1
- {"version":3,"file":"pty-spawner.d.ts","sourceRoot":"","sources":["../src/pty-spawner.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pty-spawner.d.ts","sourceRoot":"","sources":["../src/pty-spawner.ts"],"names":[],"mappings":"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"}
@@ -620,24 +620,19 @@ export async function spawnClaudeSession(deps, args) {
620
620
  : '';
621
621
  deps.logger(`tool-allowlist role=${args.role} count=${allowed.length} excluded=${excludedSample}`);
622
622
  }
623
- // Task 213 — admin operator spawns lose the memory write tools at the
624
- // CLI deny gate. Evidence in session d2aaa85e (2026-05-21): the admin
625
- // agent emitted `tool_use: memory-update` filling in for a recorder
626
- // turn that produced none, writing nodes that violated the
627
- // admin-does-not-write contract declared in IDENTITY.md. The recorder
628
- // (database-operator specialist) owns these writers; admin must never
629
- // hold them. Specialist spawns (recorder included) are governed by
630
- // their agent-frontmatter `tools:` allowlist and are not affected by
631
- // this disallow the names below intentionally use the runtime
632
- // post-Task-209 plugin namespace (`mcp__plugin_memory_memory__…`).
633
- if (args.role === 'admin' && !isSpecialistSpawn) {
634
- const disallowed = [
635
- 'mcp__plugin_memory_memory__memory-write',
636
- 'mcp__plugin_memory_memory__memory-update',
637
- ];
638
- argv.push('--disallowed-tools', disallowed.join(','));
639
- deps.logger(`pty-spawn-disallowed role=admin source=spawn-disallow tools=${disallowed.join(',')}`);
640
- }
623
+ // Task 222the admin memory-write/update revoke that Task 213 implemented
624
+ // at this CLI boundary has moved to the PreToolUse hook (see
625
+ // `platform/plugins/admin/hooks/pre-tool-use.sh`, wired in seed-neo4j.sh).
626
+ // The CLI `--disallowed-tools` flag was load-bearing in the wrong
627
+ // direction: it filters the tool surface of the spawned Claude Code
628
+ // process AND every in-window Task subagent spawned from it, so the
629
+ // admin-delegates-graph-write-to-database-operator contract introduced
630
+ // by Task 214 stopped working database-operator's frontmatter listed
631
+ // the writers, but the inherited CLI deny stripped them. The hook gates
632
+ // the same two writers per-tool-use and can distinguish admin direct
633
+ // (block) from a Task subagent caller (allow), which the CLI flag
634
+ // could not.
635
+ //
641
636
  // Task 207 — `--permission-mode bypassPermissions` for specialist spawns
642
637
  // when the caller has not explicitly set one. Headless specialists have
643
638
  // no operator to consent to write-tool invocations; default mode blocks