@rubytech/create-maxy-code 0.1.84 → 0.1.86

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 (98) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/config/brand.json +1 -1
  3. package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-memory-write-passthrough.test.sh +118 -0
  4. package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +0 -102
  5. package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -1
  6. package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +33 -197
  7. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +12 -1
  8. package/payload/platform/plugins/docs/references/admin-session.md +1 -1
  9. package/payload/platform/plugins/docs/references/cloudflare.md +2 -2
  10. package/payload/platform/plugins/docs/references/deployment.md +1 -1
  11. package/payload/platform/plugins/docs/references/platform.md +1 -1
  12. package/payload/platform/plugins/docs/references/troubleshooting.md +1 -1
  13. package/payload/platform/plugins/memory/PLUGIN.md +2 -2
  14. package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
  15. package/payload/platform/services/claude-session-manager/dist/config.js +1 -8
  16. package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
  17. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  18. package/payload/server/public/assets/{brand-C_iHnsbT.css → ChatInput-CktRrxx1.css} +1 -1
  19. package/payload/server/public/assets/{brand-HqTMr1bV.js → ChatInput-D62NmYfJ.js} +5 -1
  20. package/payload/server/public/assets/Checkbox-D2XqbR29.js +1 -0
  21. package/payload/server/public/assets/admin-CvpZ5VUN.js +217 -0
  22. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-DQMCKDU2.js → architectureDiagram-Q4EWVU46-9F6y6ESU.js} +1 -1
  23. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-CqJNtiQY.js → blockDiagram-DXYQGD6D-LjGbzg0M.js} +1 -1
  24. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-BVAa1YL3.js → c4Diagram-AHTNJAMY-Bxpn5T5R.js} +1 -1
  25. package/payload/server/public/assets/channel-CY_QocHj.js +1 -0
  26. package/payload/server/public/assets/{chunk-336JU56O-XIsdqKyD.js → chunk-336JU56O-Cc2kxWVN.js} +2 -2
  27. package/payload/server/public/assets/{chunk-426QAEUC-Dw4mcMYK.js → chunk-426QAEUC-DRzEhHvW.js} +1 -1
  28. package/payload/server/public/assets/{chunk-4TB4RGXK-BvrlG2FS.js → chunk-4TB4RGXK-C8zRzH9g.js} +1 -1
  29. package/payload/server/public/assets/{chunk-5FUZZQ4R-3-UZ9191.js → chunk-5FUZZQ4R-s8V7nzOd.js} +1 -1
  30. package/payload/server/public/assets/{chunk-5PVQY5BW-DOeY60Lp.js → chunk-5PVQY5BW-KNShBlaM.js} +1 -1
  31. package/payload/server/public/assets/{chunk-EDXVE4YY-BSbGSOxN.js → chunk-EDXVE4YY-C6GvGxn3.js} +1 -1
  32. package/payload/server/public/assets/{chunk-ENJZ2VHE-CszgAcWZ.js → chunk-ENJZ2VHE-XvXhU8ZS.js} +1 -1
  33. package/payload/server/public/assets/{chunk-ICPOFSXX-rgw1I6pt.js → chunk-ICPOFSXX-OSDYUb0H.js} +1 -1
  34. package/payload/server/public/assets/{chunk-OYMX7WX6-DL4v2Nx4.js → chunk-OYMX7WX6-km_4dA-E.js} +1 -1
  35. package/payload/server/public/assets/{chunk-U2HBQHQK-CwMCwyfq.js → chunk-U2HBQHQK-BGHqDvzJ.js} +1 -1
  36. package/payload/server/public/assets/{chunk-X2U36JSP-CvKfDVvq.js → chunk-X2U36JSP-BKKSYdFO.js} +1 -1
  37. package/payload/server/public/assets/{chunk-YZCP3GAM-D7vju2_U.js → chunk-YZCP3GAM-DT_yhfJM.js} +1 -1
  38. package/payload/server/public/assets/{chunk-ZZ45TVLE-sANOwJKn.js → chunk-ZZ45TVLE-CP2JuBPA.js} +1 -1
  39. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BtwH3Rsu.js +1 -0
  40. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-tCRstAZ-.js +1 -0
  41. package/payload/server/public/assets/clone-Bc-lmwcQ.js +1 -0
  42. package/payload/server/public/assets/{dagre-CplqWwSs.js → dagre-CUhFHAG8.js} +1 -1
  43. package/payload/server/public/assets/{dagre-KV5264BT-DNbl5g9o.js → dagre-KV5264BT-Dk7osC4w.js} +1 -1
  44. package/payload/server/public/assets/data-CBeNa5rc.js +1 -0
  45. package/payload/server/public/assets/{diagram-5BDNPKRD-BCdMaYnY.js → diagram-5BDNPKRD-CdOEJbjg.js} +1 -1
  46. package/payload/server/public/assets/{diagram-G4DWMVQ6-8XhDBU0o.js → diagram-G4DWMVQ6-Dyx0MQk6.js} +1 -1
  47. package/payload/server/public/assets/{diagram-MMDJMWI5-M-RMQgtx.js → diagram-MMDJMWI5-Dw6sxLh8.js} +1 -1
  48. package/payload/server/public/assets/{diagram-TYMM5635-BBHPIBvQ.js → diagram-TYMM5635-Bm3GRH-1.js} +1 -1
  49. package/payload/server/public/assets/{erDiagram-SMLLAGMA-D6jOL-9s.js → erDiagram-SMLLAGMA-CAJ3Z9uD.js} +1 -1
  50. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CiyTYRlU.js → flowDiagram-DWJPFMVM-NrVUcQ2d.js} +1 -1
  51. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-DHSIpjfS.js → ganttDiagram-T4ZO3ILL-28YyrNXj.js} +1 -1
  52. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CEneVF3o.js → gitGraphDiagram-UUTBAWPF-D73FSDE8.js} +1 -1
  53. package/payload/server/public/assets/graph-BMD0nCx5.js +1 -0
  54. package/payload/server/public/assets/graph-labels-CSqIl6zN.js +1 -0
  55. package/payload/server/public/assets/{graphlib-CuF7rLfE.js → graphlib-n_bUAhub.js} +1 -1
  56. package/payload/server/public/assets/{infoDiagram-42DDH7IO-wGl-JoP7.js → infoDiagram-42DDH7IO-rFap2NoE.js} +1 -1
  57. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-B3TxFNi_.js → ishikawaDiagram-UXIWVN3A-KKAj626l.js} +1 -1
  58. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-B5n8No7p.js → journeyDiagram-VCZTEJTY-Do6hVThi.js} +1 -1
  59. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-ByGF39_h.js → kanban-definition-6JOO6SKY-C38qlZwy.js} +1 -1
  60. package/payload/server/public/assets/lib-D-jcbjd4.js +29 -0
  61. package/payload/server/public/assets/{line-Br0FyyFW.js → line-DsDSEpXU.js} +1 -1
  62. package/payload/server/public/assets/{mermaid-parser.core-BUcTjodx.js → mermaid-parser.core-Cvzc7ePL.js} +1 -1
  63. package/payload/server/public/assets/{mermaid.core-BKzdtZgG.js → mermaid.core-BSZExB1C.js} +3 -3
  64. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CobGMF9l.js → mindmap-definition-QFDTVHPH-CrS3b5u5.js} +1 -1
  65. package/payload/server/public/assets/page-CQUvdg_a.js +1 -0
  66. package/payload/server/public/assets/{page-Ba0BAfpu.js → page-DVEcL-Uw.js} +2 -2
  67. package/payload/server/public/assets/{pieDiagram-DEJITSTG-DXFmGcNR.js → pieDiagram-DEJITSTG-Dk-6uOEj.js} +1 -1
  68. package/payload/server/public/assets/public-BK3eOHJs.js +8 -0
  69. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-BnChStlO.js → quadrantDiagram-34T5L4WZ-CEMK5xQ0.js} +1 -1
  70. package/payload/server/public/assets/{requirementDiagram-MS252O5E-DdiBKsyO.js → requirementDiagram-MS252O5E-vTyRxQOY.js} +1 -1
  71. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-16-np_56.js → sankeyDiagram-XADWPNL6-s2z8dMpM.js} +1 -1
  72. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-D67SCRpA.js → sequenceDiagram-FGHM5R23-locoOgnm.js} +1 -1
  73. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-CvNJX0YC.js → stateDiagram-FHFEXIEX-m1d4aD0M.js} +1 -1
  74. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-B7eWHz7x.js +1 -0
  75. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BJE5YoBa.js → timeline-definition-GMOUNBTQ-HraDHuOp.js} +1 -1
  76. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-DF17ddXt.js → vennDiagram-DHZGUBPP-DkyF2rFo.js} +1 -1
  77. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-xn_2XUOo.js → wardleyDiagram-NUSXRM2D-BGScmzP0.js} +1 -1
  78. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DVF8zf3f.js → xychartDiagram-5P7HB3ND-CPJ1JLj2.js} +1 -1
  79. package/payload/server/public/brand-constants.json +1 -1
  80. package/payload/server/public/data.html +5 -5
  81. package/payload/server/public/graph.html +6 -6
  82. package/payload/server/public/index.html +8 -8
  83. package/payload/server/public/public.html +5 -5
  84. package/payload/server/server.js +2 -2
  85. package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-memory-write-revoke.test.sh +0 -248
  86. package/payload/server/public/assets/Checkbox-B34vplI7.js +0 -1
  87. package/payload/server/public/assets/admin-BkTlPuP0.js +0 -217
  88. package/payload/server/public/assets/channel-CZG4mVkt.js +0 -1
  89. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BoqQ3pHg.js +0 -1
  90. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-FoKuBK7n.js +0 -1
  91. package/payload/server/public/assets/clone-CT8AM-bU.js +0 -1
  92. package/payload/server/public/assets/data-BIDM6J5V.js +0 -1
  93. package/payload/server/public/assets/device-url-actions-nxh9PmIH.js +0 -33
  94. package/payload/server/public/assets/graph-Cm-iWFqy.js +0 -1
  95. package/payload/server/public/assets/graph-labels-CFX6KkCz.js +0 -1
  96. package/payload/server/public/assets/page-B5NRnW0t.js +0 -1
  97. package/payload/server/public/assets/public-DWfBzf83.js +0 -8
  98. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CFw2U4iL.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.84",
3
+ "version": "0.1.86",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -6,7 +6,7 @@
6
6
  "configDir": ".maxy-code",
7
7
  "tagline": "AI for Productive People",
8
8
  "strapline": "Convenience as standard.",
9
- "domain": "getmaxy.com",
9
+ "marketingUrl": "https://getmaxy.com",
10
10
  "neo4jPort": 7687,
11
11
  "ttydPort": 7681,
12
12
  "vncDisplay": 99,
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env bash
2
+ # Task 225 — passthrough regression test for memory-write / memory-update
3
+ # under the admin agent's pre-tool-use.sh.
4
+ #
5
+ # The Task 213→222 chain previously gated both writers at the top of the
6
+ # admin branch with an exit-2 block (admin direct) and an exit-0 allow
7
+ # (Task-subagent). Task 225 reversed the doctrine: admin holds the
8
+ # writers in its own surface and the case-block was deleted. This test
9
+ # pins the new behaviour so a future re-introduction of the block
10
+ # cannot land silently.
11
+ #
12
+ # Five cases:
13
+ # 1. mcp__plugin_memory_memory__memory-write admin-direct → exit 0, no rejection stderr
14
+ # 2. mcp__plugin_memory_memory__memory-update admin-direct → exit 0, no rejection stderr
15
+ # 3. mcp__plugin_memory_memory__memory-search admin-direct → exit 0, no rejection stderr (read tool control)
16
+ # 4. mcp__memory__memory-write admin-direct → exit 0 (pre-209 namespace, falls through harmlessly)
17
+ # 5. malformed JSON stdin → exit 0 (no crash; case-block removal does not break fall-through)
18
+
19
+ set -u
20
+
21
+ HOOK="$(cd "$(dirname "$0")/.." && pwd)/pre-tool-use.sh"
22
+ if [[ ! -x "$HOOK" ]]; then
23
+ echo "FAIL: $HOOK not executable" >&2
24
+ exit 1
25
+ fi
26
+
27
+ TMPFILES=()
28
+ cleanup() {
29
+ for f in "${TMPFILES[@]:-}"; do
30
+ [[ -n "$f" ]] && rm -f "$f" 2>/dev/null || true
31
+ done
32
+ }
33
+ trap cleanup EXIT
34
+
35
+ PASS=0
36
+ FAIL=0
37
+ pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
38
+ fail() { echo "FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
39
+
40
+ # Admin-direct calls land with a non-subagent transcript path. A synthetic
41
+ # empty file in a temp dir is enough — the hook no longer reads the path
42
+ # for the deleted writers' case-block, but other gates may still touch it.
43
+ ADMIN_TRANSCRIPT=$(mktemp); TMPFILES+=("$ADMIN_TRANSCRIPT")
44
+ : > "$ADMIN_TRANSCRIPT"
45
+
46
+ run_with_tool() {
47
+ local tool="$1"
48
+ local sid="$2"
49
+ local transcript_path="${3:-}"
50
+ local stdout_file; stdout_file=$(mktemp); TMPFILES+=("$stdout_file")
51
+ local stderr_file; stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
52
+ python3 -c '
53
+ import json, sys
54
+ payload = {
55
+ "hook_event_name": "PreToolUse",
56
+ "session_id": sys.argv[1],
57
+ "tool_name": sys.argv[2],
58
+ "tool_input": {"name":"Smalleys","accountId":"acct-x","scope":"shared"},
59
+ }
60
+ if sys.argv[3]:
61
+ payload["transcript_path"] = sys.argv[3]
62
+ print(json.dumps(payload, separators=(",", ":")))
63
+ ' "$sid" "$tool" "$transcript_path" | bash "$HOOK" admin >"$stdout_file" 2>"$stderr_file"
64
+ HOOK_RC=$?
65
+ HOOK_STDERR=$(cat "$stderr_file")
66
+ }
67
+
68
+ assert_passthrough() {
69
+ local label="$1"
70
+ if [[ "$HOOK_RC" -ne 0 ]]; then
71
+ fail "${label}: expected exit 0, got $HOOK_RC stderr: $HOOK_STDERR"
72
+ return
73
+ fi
74
+ if echo "$HOOK_STDERR" | grep -qF "does not write to the graph directly"; then
75
+ fail "${label}: stderr carries deleted-block rejection message: $HOOK_STDERR"
76
+ return
77
+ fi
78
+ pass "${label}: exit 0, no rejection stderr"
79
+ }
80
+
81
+ # --- 1. memory-write admin-direct passthrough ---------------------------
82
+ run_with_tool "mcp__plugin_memory_memory__memory-write" "sess-write-1" "$ADMIN_TRANSCRIPT"
83
+ assert_passthrough "admin-direct memory-write"
84
+
85
+ # --- 2. memory-update admin-direct passthrough --------------------------
86
+ run_with_tool "mcp__plugin_memory_memory__memory-update" "sess-update-1" "$ADMIN_TRANSCRIPT"
87
+ assert_passthrough "admin-direct memory-update"
88
+
89
+ # --- 3. memory-search admin-direct passthrough (read tool control) -----
90
+ run_with_tool "mcp__plugin_memory_memory__memory-search" "sess-search-1" "$ADMIN_TRANSCRIPT"
91
+ assert_passthrough "admin-direct memory-search (read tool control)"
92
+
93
+ # --- 4. pre-209 namespace mcp__memory__memory-write ---------------------
94
+ # The bare mcp__memory__ namespace was never in the admin runtime surface
95
+ # after Task 203's plugin_ prefix rename; this case pins that it still
96
+ # falls through harmlessly after the case-block removal.
97
+ run_with_tool "mcp__memory__memory-write" "sess-old-ns-1" "$ADMIN_TRANSCRIPT"
98
+ assert_passthrough "pre-209 namespace mcp__memory__memory-write"
99
+
100
+ # --- 5. malformed JSON stdin — hook must not crash ----------------------
101
+ stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
102
+ stdout_file=$(mktemp); TMPFILES+=("$stdout_file")
103
+ echo "not-valid-json{{{" | bash "$HOOK" admin >"$stdout_file" 2>"$stderr_file"
104
+ HOOK_RC=$?
105
+ HOOK_STDERR=$(cat "$stderr_file")
106
+ if [[ "$HOOK_RC" -ne 0 ]]; then
107
+ fail "malformed payload: expected exit 0, got $HOOK_RC stderr: $HOOK_STDERR"
108
+ elif echo "$HOOK_STDERR" | grep -qF "does not write to the graph directly"; then
109
+ fail "malformed payload: stderr carries deleted-block rejection message: $HOOK_STDERR"
110
+ else
111
+ pass "malformed payload: exit 0, hook does not crash"
112
+ fi
113
+
114
+ # --- Summary -----------------------------------------------------------
115
+ echo "---"
116
+ echo "PASSED: $PASS FAILED: $FAIL"
117
+ [[ "$FAIL" -eq 0 ]] || exit 1
118
+ exit 0
@@ -21,108 +21,6 @@ TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"'
21
21
  # ---------------------------------------------------------------------------
22
22
  if [ "$AGENT_TYPE" = "admin" ]; then
23
23
 
24
- # ── Task 213 (rev. Task 222): memory-write/memory-update gate ────────────
25
- # Admin does not write to the graph directly. The contract (Task 214 +
26
- # admin IDENTITY.md) is that admin delegates a write to `database-operator`
27
- # via the Task tool; database-operator holds the writers in its agent
28
- # frontmatter and executes the write inside its own in-window subagent
29
- # turn.
30
- #
31
- # The PreToolUse payload includes `transcript_path`, the JSONL the
32
- # invoking session writes turns to. Claude Code stores a subagent's
33
- # turns in a separate file at
34
- # <project-key>/<sessionId>/subagents/agent-<agentId>.jsonl
35
- # so the path pattern is the deterministic discriminator between admin
36
- # direct calls (parent JSONL) and Task subagent calls (subagent JSONL).
37
- #
38
- # The hook gates the two memory writers per-tool-use:
39
- # * Task-subagent caller (path matches subagent pattern)
40
- # → allow, emit caller=subagent:<subagent_type> source=hook-passthrough result=allowed
41
- # * admin direct caller (any other path)
42
- # → block exit 2, emit caller=admin source=hook result=rejected
43
- # * unparseable payload (no JSON, no transcript_path)
44
- # → fail-closed: block exit 2, emit caller=unknown source=hook result=rejected
45
- # The subagent_type enrichment is best-effort: the subagent's own JSONL
46
- # does not encode its agent name, so the hook reads the parent transcript
47
- # (sibling `<sessionId>.jsonl` to the subagent dir) and extracts the
48
- # `subagent_type` from the most recent `Task` tool_use. If that read
49
- # fails for any reason the caller is logged as bare `subagent` and the
50
- # call is still allowed.
51
- case "$TOOL_NAME" in
52
- mcp__plugin_memory_memory__memory-write|mcp__plugin_memory_memory__memory-update)
53
- VERDICT=$(printf '%s' "$INPUT" | python3 -c '
54
- import sys, json, os, re
55
- tool_name = sys.argv[1]
56
- try:
57
- d = json.load(sys.stdin)
58
- except Exception:
59
- print("block|unknown|unknown|" + tool_name)
60
- sys.exit(0)
61
- sid = d.get("session_id") or "unknown"
62
- tp = d.get("transcript_path") or ""
63
- m = re.search(r"/subagents/agent-[A-Za-z0-9_-]+\.jsonl$", tp)
64
- if not m:
65
- caller = "admin" if tp else "unknown"
66
- print("block|" + sid + "|" + caller + "|" + tool_name)
67
- sys.exit(0)
68
- parent = tp[: m.start()] + ".jsonl"
69
- subagent_type = None
70
- if os.path.isfile(parent):
71
- try:
72
- with open(parent, "r", encoding="utf-8") as f:
73
- for ln in f:
74
- ln = ln.strip()
75
- if not ln:
76
- continue
77
- try:
78
- r = json.loads(ln)
79
- except Exception:
80
- continue
81
- if r.get("type") != "assistant":
82
- continue
83
- msg = r.get("message") or {}
84
- content = msg.get("content")
85
- if not isinstance(content, list):
86
- continue
87
- for b in content:
88
- if (isinstance(b, dict) and b.get("type") == "tool_use"
89
- and b.get("name") == "Task"):
90
- st = (b.get("input") or {}).get("subagent_type")
91
- if isinstance(st, str) and st:
92
- subagent_type = st
93
- except Exception:
94
- subagent_type = None
95
- caller = "subagent:" + subagent_type if subagent_type else "subagent"
96
- print("allow|" + sid + "|" + caller + "|" + tool_name)
97
- ' "$TOOL_NAME" 2>/dev/null)
98
- [ -z "$VERDICT" ] && VERDICT="block|unknown|unknown|${TOOL_NAME}"
99
- DECISION=$(printf '%s' "$VERDICT" | cut -d'|' -f1)
100
- SESSION_ID=$(printf '%s' "$VERDICT" | cut -d'|' -f2)
101
- CALLER=$(printf '%s' "$VERDICT" | cut -d'|' -f3)
102
- if [ "$DECISION" = "allow" ]; then
103
- LINE_BODY="role=admin sessionId=${SESSION_ID} tool=${TOOL_NAME} caller=${CALLER} source=hook-passthrough result=allowed"
104
- else
105
- LINE_BODY="role=admin sessionId=${SESSION_ID} tool=${TOOL_NAME} caller=${CALLER} source=hook result=rejected"
106
- fi
107
- UI_PORT="${MAXY_UI_INTERNAL_PORT:-}"
108
- if [ -n "$UI_PORT" ]; then
109
- curl -sS -o /dev/null -X POST \
110
- -H 'Content-Type: application/json' \
111
- --max-time 2 \
112
- --data "$(python3 -c '
113
- import sys, json
114
- print(json.dumps({"tag":"admin-tool-revoke","level":"info","line":sys.argv[1]}))
115
- ' "$LINE_BODY")" \
116
- "http://127.0.0.1:${UI_PORT}/api/admin/log-ingest" 2>/dev/null || true
117
- fi
118
- if [ "$DECISION" = "allow" ]; then
119
- exit 0
120
- fi
121
- echo "Blocked: admin agent does not write to the graph directly. Delegate the write to database-operator via the Task tool; database-operator holds the memory-write and memory-update tools and will execute the write on admin's behalf." >&2
122
- exit 2
123
- ;;
124
- esac
125
-
126
24
  # ── Code directory protection ────────────────────────────────────────────
127
25
  case "$TOOL_NAME" in
128
26
  Write|Edit)
@@ -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 — 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. |
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, and prints it as `OAUTH_URL: <url>` on its own stdout — the admin UI's `ActionLogPanel` renders that line as a clickable Authorize link with `target=_blank` so the operator authorizes Cloudflare in a new tab of their own browser. The script does not spawn a browser of its own; cloudflared's OAuth callback writes `~/.cloudflared/cert.pem` regardless of which browser completed the Authorize click. 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
@@ -14,14 +14,12 @@
14
14
  # hostnames. The script writes the ingress rule for them but prints an
15
15
  # explicit ACTION REQUIRED message naming the manual dashboard step.
16
16
  #
17
- # Step 1 owns the browser-spawn deterministically. The wrapper extracts
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
21
- # cloudflared's optimistic xdg-open, no CDP auto-click, no DOM matcher.
22
- # The operator clicks the zone row + Authorize on the VNC themselves.
23
- # This is the wrappers-faithfully-relay-third-party-cli doctrine;
24
- # any layer that auto-clicks Authorize for the operator is forbidden.
17
+ # Step 1 surfaces the argotunnel URL on stdout as an `OAUTH_URL:` line.
18
+ # The admin UI renders that line as a clickable link (target=_blank) so the
19
+ # operator authorizes Cloudflare in the same browser they are already using
20
+ # to talk to the admin chat. The script spawns no browser of its own — the
21
+ # operator's local browser is the canonical operator-visible surface; the
22
+ # brand VNC iframe is derived/optional and never the OAuth-completion path.
25
23
  # cloudflared's stdout+stderr is teed line-by-line into STREAM_LOG_PATH so
26
24
  # the chat UI's server-side tailer renders live progress in-turn.
27
25
 
@@ -99,67 +97,12 @@ phase_line setup-tunnel step=start brand="${BRAND}" port="${PORT}" hostnames="${
99
97
  CFG_DIR="${HOME}/.${BRAND}/cloudflared"
100
98
  mkdir -p "${CFG_DIR}"
101
99
 
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.
100
+ # brand.json path resolution. Step 5b reads `.operatorEmail` from this file
101
+ # when OPERATOR_EMAIL is not in env; absent file is a tolerated no-op.
108
102
  SETUP_TUNNEL_BRAND_JSON=""
109
103
  if [ -n "${MAXY_PLATFORM_ROOT:-}" ] && [ -f "${MAXY_PLATFORM_ROOT}/config/brand.json" ]; then
110
104
  SETUP_TUNNEL_BRAND_JSON="${MAXY_PLATFORM_ROOT}/config/brand.json"
111
105
  fi
112
- BRAND_VNC_DISPLAY=":99"
113
- if [ -n "${SETUP_TUNNEL_BRAND_JSON}" ] && command -v jq >/dev/null 2>&1; then
114
- _bvd=$(jq -r '.vncDisplay // empty' "${SETUP_TUNNEL_BRAND_JSON}" 2>/dev/null || true)
115
- if [ -n "${_bvd}" ] && [ "${_bvd}" -eq "${_bvd}" ] 2>/dev/null; then
116
- BRAND_VNC_DISPLAY=":${_bvd}"
117
- fi
118
- fi
119
-
120
- # read the install-time-resolved non-snap Chromium binary path so
121
- # the OAuth-URL spawn below uses the same binary vnc.sh's start_chrome runs.
122
- # Hardcoded `/usr/bin/chromium` would re-introduce the snap-AppArmor failure
123
- # on Ubuntu Noble laptop. The installer writes this file under platform/config/
124
- # during installSystemDeps; the spawn at step=browser-spawn fails loud if the
125
- # file is absent rather than silently falling back to /usr/bin/chromium.
126
- #
127
- # four discrete branches (env-unset / file-missing / file-empty /
128
- # binary-not-executable) so the failure message names the actual condition.
129
- # Pre-957 the four were conflated into one "cannot resolve a non-snap Chromium
130
- # binary" message, which masked the env-propagation defect that closed three
131
- # prior tasks (836, 562, 862) and silently re-fired here.
132
- if [ -z "${MAXY_PLATFORM_ROOT:-}" ]; then
133
- phase_line setup-tunnel step=chromium-resolve result=error reason=env-unset var=MAXY_PLATFORM_ROOT
134
- echo "ERROR: setup-tunnel.sh: MAXY_PLATFORM_ROOT is unset in the script's env." >&2
135
- echo " The action runner must declare MAXY_PLATFORM_ROOT in the cloudflare-setup" >&2
136
- echo " whitelist env: block (platform/ui/server/lib/action-runner.ts) — systemd-run --user" >&2
137
- echo " does not inherit the parent process env into the transient unit." >&2
138
- exit 1
139
- fi
140
- SETUP_TUNNEL_CHROMIUM_PATH_FILE="${MAXY_PLATFORM_ROOT}/config/chromium-binary.path"
141
- if [ ! -r "${SETUP_TUNNEL_CHROMIUM_PATH_FILE}" ]; then
142
- phase_line setup-tunnel step=chromium-resolve result=error reason=path-file-missing path="${SETUP_TUNNEL_CHROMIUM_PATH_FILE}"
143
- echo "ERROR: setup-tunnel.sh: chromium-binary.path file is missing or unreadable." >&2
144
- echo " Expected: ${SETUP_TUNNEL_CHROMIUM_PATH_FILE}" >&2
145
- echo " Re-run the installer to provision Chromium." >&2
146
- exit 1
147
- fi
148
- SETUP_TUNNEL_CHROMIUM_BIN="$(head -n1 "${SETUP_TUNNEL_CHROMIUM_PATH_FILE}" | tr -d '[:space:]')"
149
- if [ -z "${SETUP_TUNNEL_CHROMIUM_BIN}" ]; then
150
- phase_line setup-tunnel step=chromium-resolve result=error reason=path-file-empty path="${SETUP_TUNNEL_CHROMIUM_PATH_FILE}"
151
- echo "ERROR: setup-tunnel.sh: chromium-binary.path is empty." >&2
152
- echo " File: ${SETUP_TUNNEL_CHROMIUM_PATH_FILE}" >&2
153
- echo " Re-run the installer to provision Chromium." >&2
154
- exit 1
155
- fi
156
- if [ ! -x "${SETUP_TUNNEL_CHROMIUM_BIN}" ]; then
157
- phase_line setup-tunnel step=chromium-resolve result=error reason=binary-not-executable bin="${SETUP_TUNNEL_CHROMIUM_BIN}" path="${SETUP_TUNNEL_CHROMIUM_PATH_FILE}"
158
- echo "ERROR: setup-tunnel.sh: ${SETUP_TUNNEL_CHROMIUM_BIN} is not an executable file." >&2
159
- echo " Resolved from: ${SETUP_TUNNEL_CHROMIUM_PATH_FILE}" >&2
160
- echo " Re-run the installer to provision Chromium." >&2
161
- exit 1
162
- fi
163
106
 
164
107
  # --------------------------------------------------------------------------
165
108
  # Step 1: OAuth login. Corresponds to runbook Step 1.
@@ -174,32 +117,26 @@ fi
174
117
  # but failed before the mv;
175
118
  # promote and skip cloudflared.
176
119
  # both missing → spawn cloudflared, surface
177
- # the URL on the Pi VNC,
178
- # wait for the operator's
179
- # click, poll for cert.pem,
180
- # mv.
120
+ # the OAuth URL on stdout for
121
+ # the admin UI to render as a
122
+ # clickable link, poll for
123
+ # cert.pem, mv.
181
124
  #
182
- # Control flow when both certs are missing
183
- # relay third-party CLI):
125
+ # Control flow when both certs are missing:
184
126
  # 1. Spawn cloudflared with stdout+stderr teed line-by-line to
185
127
  # $STREAM_LOG_PATH with prefix [script:setup-tunnel:cloudflared]
186
128
  # (the chat-surface namespace — see _stream-log.sh header).
187
129
  # 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> &`.
190
- # Fire-and-forget chromium is already running with CDP enabled at
191
- # :9222, so the invocation IPCs the URL into the running instance as
192
- # a new tab. The spawn is intentionally NOT tracked in the EXIT trap:
193
- # it is a sibling open, not a child of cloudflared, and an orphaned
194
- # late-arriving tab is harmless. Replaces cloudflared's own optimistic
195
- # xdg-open which does not reliably target VNC :99 in this environment.
196
- # 4. Emit `step=browser-spawn` and `step=browser-drive mode=operator-click`
197
- # so the stream log records the surface state.
198
- # 5. Wait for ~/.cloudflared/cert.pem to land (180 s budget — operator
199
- # must click the zone row + Authorize on VNC).
200
- # 6. Move cert.pem into the brand-scoped path.
130
+ # 3. Print the URL on stdout as `OAUTH_URL: <url>` so the admin UI
131
+ # renders a clickable link. The script does not spawn a browser —
132
+ # the operator's local browser is the canonical surface; cloudflared's
133
+ # OAuth callback writes ~/.cloudflared/cert.pem regardless of which
134
+ # browser completed the Authorize click (it's a server-to-server poll
135
+ # against login.cloudflareaccess.org).
136
+ # 4. Wait for ~/.cloudflared/cert.pem to land (180 s budget — operator
137
+ # must click the link and Authorize on Cloudflare).
138
+ # 5. Move cert.pem into the brand-scoped path.
201
139
  #
202
- # No CDP auto-click. No DOM matcher. No consent-page driver of any kind.
203
140
  # Every failure branch exits 1 loudly naming the cause.
204
141
  # --------------------------------------------------------------------------
205
142
 
@@ -237,7 +174,7 @@ if [ ! -f "${CFG_DIR}/cert.pem" ] && [ -f "${HOME}/.cloudflared/cert.pem" ]; the
237
174
  fi
238
175
 
239
176
  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}}"
177
+ phase_line setup-tunnel step=oauth-login cert_path="${CFG_DIR}/cert.pem"
241
178
 
242
179
  URL_FILE="$(mktemp -t maxy-setup-tunnel-url.XXXXXX)"
243
180
  LAST_LINE_FILE="$(mktemp -t maxy-setup-tunnel-last.XXXXXX)"
@@ -249,16 +186,8 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
249
186
  # callback forever; subsequent setup-tunnel runs see a stale cert.pem
250
187
  # landing asynchronously and race against the new URL-extraction pass.
251
188
  CF_PIPELINE_PID=""
252
- CHROMIUM_UNIT=""
253
189
  cleanup_oauth() {
254
190
  [ -n "${CF_PIPELINE_PID}" ] && kill "${CF_PIPELINE_PID}" 2>/dev/null || true
255
- # stop the transient chromium unit on any early exit between
256
- # browser-spawn and the explicit step=browser-close site below. Best-
257
- # effort: no phase_line here because the EXIT trap fires on every path
258
- # (including the happy one where step=browser-close already ran and
259
- # auto-collected the unit). The `|| true` masks the inevitable "Unit
260
- # not loaded" return on the happy path.
261
- [ -n "${CHROMIUM_UNIT}" ] && systemctl --user stop "${CHROMIUM_UNIT}" 2>/dev/null || true
262
191
  rm -f "${URL_FILE}" "${LAST_LINE_FILE}"
263
192
  }
264
193
  trap cleanup_oauth EXIT
@@ -267,7 +196,7 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
267
196
  # extracted as it streams. The subshell holds the whole pipeline so
268
197
  # PIPESTATUS[0] (cloudflared's exit code) is reachable later.
269
198
  (
270
- DISPLAY="${DISPLAY:-${BRAND_VNC_DISPLAY}}" stdbuf -oL -eL cloudflared \
199
+ stdbuf -oL -eL cloudflared \
271
200
  --origincert "${CFG_DIR}/cert.pem" tunnel login 2>&1 |
272
201
  while IFS= read -r line; do
273
202
  ts="$(stream_log_ts)"
@@ -309,71 +238,17 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
309
238
  fi
310
239
 
311
240
  AUTH_URL="$(cat "${URL_FILE}")"
312
- phase_line setup-tunnel step=browser-drive url_extracted=1
241
+ phase_line setup-tunnel step=oauth-url-extracted url_extracted=1
313
242
 
314
243
  # Emit the URL on stdout in a shape ActionLogPanel's regex captures.
315
- # The button is a redundant re-spawn fallback by the time the chat-side
316
- # ActionLogPanel renders, the page is already on the Pi VNC chromium per
317
- # the spawn below. Button click POSTs to /api/admin/device-browser/navigate
318
- # to re-target the same URL on the VNC; same end-effect as this spawn.
244
+ # The admin UI renders this line as a clickable link (target=_blank) so
245
+ # the operator authorizes Cloudflare in the same browser they are using
246
+ # for the admin chat. The script does not spawn a browser of its own.
319
247
  printf 'OAUTH_URL: %s\n' "${AUTH_URL}"
320
248
 
321
- # Mechanically open the URL on the Pi VNC chromium. Chromium
322
- # is already running on this brand's ${BRAND_VNC_DISPLAY} with CDP enabled
323
- # (vnc.sh start_chrome at boot); invoking the resolved binary <url> against
324
- # a running instance IPCs the URL into it as a new tab. Replaces
325
- # cloudflared's own optimistic xdg-open, which does not reliably target
326
- # the brand's VNC display in this environment.
327
- #
328
- # chromium is launched under a transient systemd-user unit so
329
- # the full process tree (including any standalone chromium that lands
330
- # when no existing instance is running for IPC) lives in its own cgroup.
331
- # On cert.pem arrival the unit is stopped, SIGTERMing the whole cgroup
332
- # atomically. Pre-Task-982 the spawn was `&` fire-and-forget with no
333
- # tracked PID; the resulting orphan chromium on display :101 was the
334
- # symptom in maxy-2 2026-05-12T10:06–10:08Z. `step=browser-close
335
- # result=ok|orphan` records the teardown outcome at cert.pem mv site
336
- # below.
337
- #
338
- # Binary path: SETUP_TUNNEL_CHROMIUM_BIN is read at startup from
339
- # ${MAXY_PLATFORM_ROOT}/config/chromium-binary.path — `/usr/bin/chromium`
340
- # on Pi Bookworm and `/usr/bin/google-chrome-stable` on Ubuntu Noble laptop
341
- # where the system chromium is snap-confined. Hardcoding
342
- # `/usr/bin/chromium` here would re-introduce the AppArmor SingletonLock
343
- # failure on the laptop.
344
- CHROMIUM_UNIT="maxy-oauth-chromium-${BRAND}-$$.service"
345
- CHROMIUM_LAUNCH_DISPLAY="${DISPLAY:-${BRAND_VNC_DISPLAY}}"
346
- CHROMIUM_SPAWN_ERR="$(mktemp -t maxy-oauth-chromium-err.XXXXXX)"
347
- if systemd-run --user \
348
- --unit="${CHROMIUM_UNIT}" \
349
- --description="Maxy OAuth chromium for ${BRAND}" \
350
- --collect \
351
- --setenv=DISPLAY="${CHROMIUM_LAUNCH_DISPLAY}" \
352
- "${SETUP_TUNNEL_CHROMIUM_BIN}" "${AUTH_URL}" 2>"${CHROMIUM_SPAWN_ERR}"; then
353
- rm -f "${CHROMIUM_SPAWN_ERR}"
354
- phase_line setup-tunnel step=browser-spawn result=ok \
355
- display="${CHROMIUM_LAUNCH_DISPLAY}" url_extracted=1 unit="${CHROMIUM_UNIT}"
356
- else
357
- SPAWN_RC=$?
358
- SPAWN_STDERR="$(tr '\n' ' ' < "${CHROMIUM_SPAWN_ERR}" | head -c 300 || echo unavailable)"
359
- rm -f "${CHROMIUM_SPAWN_ERR}"
360
- # Loud-fail rather than fire-and-forget fallback: a systemd-run failure
361
- # leaves no teardown handle. Operator should see the bus-not-running
362
- # / linger-not-enabled cause.
363
- phase_line setup-tunnel step=browser-spawn result=error \
364
- reason=systemd-run-failed exit="${SPAWN_RC}" stderr="${SPAWN_STDERR}" \
365
- unit="${CHROMIUM_UNIT}"
366
- echo "ERROR: systemd-run failed to spawn chromium under transient unit (exit=${SPAWN_RC})." >&2
367
- echo " systemd-run stderr: ${SPAWN_STDERR}" >&2
368
- echo " If stderr mentions 'Failed to connect to bus', enable user-scope" >&2
369
- echo " systemd via 'loginctl enable-linger \$(whoami)' and retry." >&2
370
- exit 1
371
- fi
372
- phase_line setup-tunnel step=browser-drive mode=operator-click url="${AUTH_URL}"
373
-
374
249
  # Wait for cert.pem to land — cloudflared writes to ~/.cloudflared/cert.pem
375
250
  # regardless of --origincert, so watch the canonical location. The wait is
376
- # human-paced: the operator must click the zone row + Authorize on the VNC.
251
+ # human-paced: the operator must click the link and Authorize on Cloudflare.
377
252
  # 180 s default budget; SETUP_TUNNEL_LOGIN_TIMEOUT overrides for testing.
378
253
  # The 2-second heartbeat inside the loop is the observability contract —
379
254
  # no form-spawned script is allowed a silent poll of more than ~2 s.
@@ -397,10 +272,10 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
397
272
  echo "ERROR: Timed out after ${LOGIN_WAIT}s waiting for cert.pem to land." >&2
398
273
  exit 1
399
274
  fi
400
- # Heartbeat every 2 s after the click. t=0 is the browser-drive
401
- # phase line above; the first heartbeat fires at t=2. Without this line
402
- # the tailer sees silence for the full 1-20 s round-trip — the exact
403
- # state the heartbeat contract forbids.
275
+ # Heartbeat every 2 s. t=0 is the oauth-url-extracted phase line above;
276
+ # the first heartbeat fires at t=2. Without this line the tailer sees
277
+ # silence for the full 1-20 s round-trip — the exact state the heartbeat
278
+ # contract forbids.
404
279
  if [ "${LOGIN_WAIT}" -gt 0 ] && [ $((LOGIN_WAIT % 2)) -eq 0 ]; then
405
280
  phase_line setup-tunnel step=oauth-login result=awaiting-cert \
406
281
  elapsed="${LOGIN_WAIT}s" timeout="${LOGIN_TIMEOUT}s"
@@ -412,45 +287,6 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
412
287
  mv "${HOME}/.cloudflared/cert.pem" "${CFG_DIR}/cert.pem"
413
288
  phase_line setup-tunnel step=oauth-login result=ok \
414
289
  path="${CFG_DIR}/cert.pem" waited="${LOGIN_WAIT}s"
415
-
416
- # SIGTERM the OAuth chromium cgroup now that cert.pem has
417
- # landed. The transient unit was created above at step=browser-spawn; if
418
- # chromium IPCs'd to a running brand-VNC instance and exited cleanly, the
419
- # unit is already auto-collected and `systemctl stop` returns 0 (no-such-
420
- # unit is a benign race, not an orphan). If chromium is still alive (no
421
- # pre-existing brand-VNC instance to IPC into), SIGTERM tears the whole
422
- # cgroup atomically. `result=ok` covers both clean paths; `result=orphan`
423
- # fires only when the stop command itself fails (bus issue, race with
424
- # auto-collect that returned non-zero) — operator-visible signal that an
425
- # orphan chromium MAY still be alive on the VNC display.
426
- CHROMIUM_STOP_ERR="$(mktemp -t maxy-oauth-chromium-stop-err.XXXXXX)"
427
- if systemctl --user stop "${CHROMIUM_UNIT}" 2>"${CHROMIUM_STOP_ERR}"; then
428
- rm -f "${CHROMIUM_STOP_ERR}"
429
- phase_line setup-tunnel step=browser-close result=ok unit="${CHROMIUM_UNIT}"
430
- else
431
- STOP_RC=$?
432
- STOP_STDERR="$(tr '\n' ' ' < "${CHROMIUM_STOP_ERR}" | head -c 300 || echo unavailable)"
433
- rm -f "${CHROMIUM_STOP_ERR}"
434
- # Distinguish benign "unit already auto-collected" from a true teardown
435
- # failure via systemctl's exit-code taxonomy — never via stderr prose
436
- # parsing, which breaks on non-English locales (no-stdout-parsing-for-
437
- # control-flow doctrine). Exit code 5 is systemd's canonical "Unit not
438
- # loaded" return; --collect auto-GCs a terminated unit between the
439
- # chromium-side IPC-and-exit and our stop, producing exactly this code.
440
- # Any other non-zero exit is a real teardown failure (bus down, permission,
441
- # service still alive but stop hung).
442
- if [ "${STOP_RC}" -eq 5 ]; then
443
- phase_line setup-tunnel step=browser-close result=ok \
444
- reason=unit-auto-collected unit="${CHROMIUM_UNIT}"
445
- else
446
- phase_line setup-tunnel step=browser-close result=orphan \
447
- reason=stop-failed exit="${STOP_RC}" stderr="${STOP_STDERR}" \
448
- unit="${CHROMIUM_UNIT}"
449
- echo "WARNING: failed to stop transient chromium unit ${CHROMIUM_UNIT} (exit=${STOP_RC})." >&2
450
- echo " An orphan chromium may remain on display ${CHROMIUM_LAUNCH_DISPLAY}." >&2
451
- echo " systemctl stderr: ${STOP_STDERR}" >&2
452
- fi
453
- fi
454
290
  fi
455
291
 
456
292
  # --------------------------------------------------------------------------
@@ -22,7 +22,9 @@ Any Cloudflare action outside these four surfaces is a discipline violation —
22
22
 
23
23
  Use this when the operator wants Cloudflare set up (or re-set up) end-to-end on the device. The script handles OAuth login, tunnel creation, DNS routing for each subdomain, config.yml + tunnel.state, and dispatches the `${BRAND}.service` restart to a transient `systemd-run` unit — all in one invocation. 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. Post-restart hostname verification is out of scope for the script (connector is not up when the script exits) — verify via the next admin turn or manually with `curl -I https://<hostname>`. Apex hostnames cannot be routed by the CLI; when one is passed, the script prints an `ACTION REQUIRED` block naming the exact dashboard record to edit.
24
24
 
25
- Step 1's OAuth flow is a state machine over two observable variables: the brand-scoped cert path (`${CFG_DIR}/cert.pem`) and the OAuth-default cert path (`~/.cloudflared/cert.pem`). When the brand-scoped cert is missing but the default-path cert is present from any prior partial run, the wrapper promotes it (`mv`) and emits `step=oauth-login result=ok reason=cert-promoted-from-default-path` without re-spawning cloudflared. When both are missing, the wrapper spawns `cloudflared tunnel login`, extracts the argotunnel URL from its stdout, and the instant the URL surfaces, mechanically opens it on the brand's VNC chromium under a transient `systemd-run --user --unit=maxy-oauth-chromium-${BRAND}-$$.service` so the chromium process tree lives in its own cgroup (earlier the spawn was `&` fire-and-forget and orphaned chromium on display `:101` when no pre-existing brand-VNC chromium was available for IPC). The launch uses the install-time-resolved binary (`SETUP_TUNNEL_CHROMIUM_BIN` from `${MAXY_PLATFORM_ROOT}/config/chromium-binary.path` so Ubuntu Noble laptop's snap-replaced Google Chrome is honoured) emitting `step=browser-spawn result=ok unit=<transient-unit>` and `step=browser-drive mode=operator-click`. The operator clicks the zone row + Authorize on the VNC; cloudflared's callback writes `~/.cloudflared/cert.pem`; the wrapper's cert-poll (180 s budget) picks it up and `mv`s it to the brand-scoped path; the wrapper then `systemctl --user stop`s the transient unit, emitting `step=browser-close result=ok` (or `result=orphan reason=stop-failed` when SIGTERM didn't reach the cgroup — operator-visible signal that an orphan chromium MAY still be alive). There is no CDP auto-click, no DOM matcher, no consent-page driver — the wrapper's job is to faithfully relay `cloudflared tunnel login`, never to layer automation on top.
25
+ Step 1's OAuth flow is a state machine over two observable variables: the brand-scoped cert path (`${CFG_DIR}/cert.pem`) and the OAuth-default cert path (`~/.cloudflared/cert.pem`). When the brand-scoped cert is missing but the default-path cert is present from any prior partial run, the wrapper promotes it (`mv`) and emits `step=oauth-login result=ok reason=cert-promoted-from-default-path` without re-spawning cloudflared. When both are missing, the wrapper spawns `cloudflared tunnel login`, extracts the argotunnel URL from its stdout, and prints it on its own stdout as `OAUTH_URL: <url>` plus a `step=oauth-url-extracted url_extracted=1` phase line. The admin UI's `ActionLogPanel` renders that `OAUTH_URL` line as a clickable Authorize link with `target=_blank`, opening a new tab in the operator's own browser the canonical operator-visible OAuth surface. The wrapper does not spawn a browser of its own; cloudflared's OAuth callback polls Cloudflare server-to-server and writes `~/.cloudflared/cert.pem` regardless of which browser completed the Authorize click. The wrapper's cert-poll (180 s budget) picks the cert up and `mv`s it to the brand-scoped path. There is no CDP auto-click, no DOM matcher, no consent-page driver — the wrapper's job is to faithfully relay `cloudflared tunnel login` and surface the URL for the operator to click.
26
+
27
+ **Operator-visible-surface doctrine.** The operator's visible surface is their local browser — the one already showing the admin UI. The brand VNC iframe is a derived, optional surface that the operator may or may not have open. Any agent claim that ties a UI outcome to a Pi-side surface (a brand VNC chromium tab, a `:N` display) is wrong by construction — the operator may not be watching that surface. Surfaces the agent can trust as operator-visible: the admin chat itself, the admin UI's ActionLogPanel banner, and links the operator clicked from either of those two. Everything else is best-effort.
26
28
 
27
29
  ### How inputs reach the script
28
30
 
@@ -100,6 +102,15 @@ The YAML and JSON rendering live in a pure Node helper at
100
102
 
101
103
  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
104
 
105
+ ### Narrating OAuth progress
106
+
107
+ The script tees structured phase lines into the stream log (`[setup-tunnel] step=<phase> <key=value …>`). The admin UI renders the `OAUTH_URL` stdout line as a clickable Authorize link directly above the log panel; the operator clicks it and authorizes in a new tab of their own browser. The agent's narration job is to let that link do the work and not claim where any tab is open.
108
+
109
+ - **When `step=oauth-url-extracted url_extracted=1` appears,** the URL is already surfaced as a clickable link in the operator's chat. The agent says one short line — "click the Authorize link and I'll pick up the cert once you've authorised" — and waits. No claim about which device or screen the link opens on; the operator's browser handles that.
110
+ - **When `step=oauth-login result=ok reason=cert-promoted-from-default-path` appears,** a prior run already completed OAuth — Step 1 short-circuits to the existing cert. No operator action is needed.
111
+ - **When `step=oauth-login result=error reason=<token>` appears,** the agent restates the literal `reason=…` and any `last_line=…` field and stops per the discipline rule below. The reasons that can fire on this path are `cloudflared-exited-before-url`, `url-not-extracted`, `timeout-waiting-cert`, `cloudflared-exited-no-cert`, and `cert-promote-failed`. `timeout-waiting-cert` specifically means the operator did not click Authorize within 180 s; the remediation is a fresh `~/setup-tunnel.sh` invocation, which will land on the cert-promotion pre-flight if the operator authorised after the timeout.
112
+ - **Stream-log `result=ok` on its own steps is not narration evidence.** The agent narrates from operator-action outcomes — link clicked, cert landed — not from script-internal phase ticks.
113
+
103
114
  ### When the script exits non-zero
104
115
 
105
116
  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.
@@ -83,7 +83,7 @@ The Hono route `POST /api/admin/claude-sessions` at [`platform/ui/server/routes/
83
83
 
84
84
  **Forwarded endpoints.** One upstream call per spawn-with-message: `POST {managerBase}/spawn` (enriched body, with `initialMessage` inlined when set). The manager appends `initialMessage` as the trailing positional argv to `claude` so the CLI processes it as the session's first user turn at PTY startup — the JSONL first `role=user` line equals `initialMessage` verbatim. No follow-up `POST {managerBase}/<sessionId>/input` call, no bracketed-paste, no keystroke injection. The HTTP response streams the spawn upstream body straight through. (Task 153.)
85
85
 
86
- **Callers.** `Sidebar.tsx`'s "+ New session" click handler (sends no `initialMessage` Task 146 made Claude Code's own label canonical, so no UI-side stimulus is injected); the turn-recorder hook (loopback path — see "Turn-recorder" below); future programmatic spawns. Every caller routes through this wrapper. The on-the-wire signal that the contract held is the `[claude-session-manager:wrapper] spawn-request-in surface=<cookie|loopback>` log line followed by `forward-spawn-done`.
86
+ **Callers.** `Sidebar.tsx`'s "+ New session" click opens the `NewSessionModal` (Task 223) — no POST fires on click. Modal submit POSTs `{channel:'browser', permissionMode, model, initialMessage}` where `initialMessage` is the operator's typed text verbatim; `permissionMode` and `model` are per-session overrides local to the modal and never propagate back to the sidebar's seed state. The turn-recorder hook (loopback path — see "Turn-recorder" below) and future programmatic spawns route through the same wrapper. The on-the-wire signal that the contract held is the `[claude-session-manager:wrapper] spawn-request-in surface=<cookie|loopback>` log line followed by `forward-spawn-done`; on the cookie path it now always carries `initialMessage=yes`, which a regressed client gate would flip to `no`.
87
87
 
88
88
  ## Turn-recorder: a first-class specialist spawn with three body overrides
89
89