@rubytech/create-maxy-code 0.1.85 → 0.1.87

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 (102) 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 -217
  7. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +9 -7
  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/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  18. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +15 -15
  19. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  20. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  21. package/payload/server/public/assets/{brand-C_iHnsbT.css → ChatInput-CktRrxx1.css} +1 -1
  22. package/payload/server/public/assets/{brand-HqTMr1bV.js → ChatInput-D62NmYfJ.js} +5 -1
  23. package/payload/server/public/assets/Checkbox-D2XqbR29.js +1 -0
  24. package/payload/server/public/assets/admin-BK1F2LMD.js +217 -0
  25. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-DQMCKDU2.js → architectureDiagram-Q4EWVU46-9F6y6ESU.js} +1 -1
  26. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-CqJNtiQY.js → blockDiagram-DXYQGD6D-LjGbzg0M.js} +1 -1
  27. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-BVAa1YL3.js → c4Diagram-AHTNJAMY-Bxpn5T5R.js} +1 -1
  28. package/payload/server/public/assets/channel-CY_QocHj.js +1 -0
  29. package/payload/server/public/assets/{chunk-336JU56O-XIsdqKyD.js → chunk-336JU56O-Cc2kxWVN.js} +2 -2
  30. package/payload/server/public/assets/{chunk-426QAEUC-Dw4mcMYK.js → chunk-426QAEUC-DRzEhHvW.js} +1 -1
  31. package/payload/server/public/assets/{chunk-4TB4RGXK-BvrlG2FS.js → chunk-4TB4RGXK-C8zRzH9g.js} +1 -1
  32. package/payload/server/public/assets/{chunk-5FUZZQ4R-3-UZ9191.js → chunk-5FUZZQ4R-s8V7nzOd.js} +1 -1
  33. package/payload/server/public/assets/{chunk-5PVQY5BW-DOeY60Lp.js → chunk-5PVQY5BW-KNShBlaM.js} +1 -1
  34. package/payload/server/public/assets/{chunk-EDXVE4YY-BSbGSOxN.js → chunk-EDXVE4YY-C6GvGxn3.js} +1 -1
  35. package/payload/server/public/assets/{chunk-ENJZ2VHE-CszgAcWZ.js → chunk-ENJZ2VHE-XvXhU8ZS.js} +1 -1
  36. package/payload/server/public/assets/{chunk-ICPOFSXX-rgw1I6pt.js → chunk-ICPOFSXX-OSDYUb0H.js} +1 -1
  37. package/payload/server/public/assets/{chunk-OYMX7WX6-DL4v2Nx4.js → chunk-OYMX7WX6-km_4dA-E.js} +1 -1
  38. package/payload/server/public/assets/{chunk-U2HBQHQK-CwMCwyfq.js → chunk-U2HBQHQK-BGHqDvzJ.js} +1 -1
  39. package/payload/server/public/assets/{chunk-X2U36JSP-CvKfDVvq.js → chunk-X2U36JSP-BKKSYdFO.js} +1 -1
  40. package/payload/server/public/assets/{chunk-YZCP3GAM-D7vju2_U.js → chunk-YZCP3GAM-DT_yhfJM.js} +1 -1
  41. package/payload/server/public/assets/{chunk-ZZ45TVLE-sANOwJKn.js → chunk-ZZ45TVLE-CP2JuBPA.js} +1 -1
  42. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BtwH3Rsu.js +1 -0
  43. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-tCRstAZ-.js +1 -0
  44. package/payload/server/public/assets/clone-Bc-lmwcQ.js +1 -0
  45. package/payload/server/public/assets/{dagre-CplqWwSs.js → dagre-CUhFHAG8.js} +1 -1
  46. package/payload/server/public/assets/{dagre-KV5264BT-DNbl5g9o.js → dagre-KV5264BT-Dk7osC4w.js} +1 -1
  47. package/payload/server/public/assets/data-B3WPMQdR.js +1 -0
  48. package/payload/server/public/assets/{diagram-5BDNPKRD-BCdMaYnY.js → diagram-5BDNPKRD-CdOEJbjg.js} +1 -1
  49. package/payload/server/public/assets/{diagram-G4DWMVQ6-8XhDBU0o.js → diagram-G4DWMVQ6-Dyx0MQk6.js} +1 -1
  50. package/payload/server/public/assets/{diagram-MMDJMWI5-M-RMQgtx.js → diagram-MMDJMWI5-Dw6sxLh8.js} +1 -1
  51. package/payload/server/public/assets/{diagram-TYMM5635-BBHPIBvQ.js → diagram-TYMM5635-Bm3GRH-1.js} +1 -1
  52. package/payload/server/public/assets/{erDiagram-SMLLAGMA-D6jOL-9s.js → erDiagram-SMLLAGMA-CAJ3Z9uD.js} +1 -1
  53. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CiyTYRlU.js → flowDiagram-DWJPFMVM-NrVUcQ2d.js} +1 -1
  54. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-DHSIpjfS.js → ganttDiagram-T4ZO3ILL-28YyrNXj.js} +1 -1
  55. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CEneVF3o.js → gitGraphDiagram-UUTBAWPF-D73FSDE8.js} +1 -1
  56. package/payload/server/public/assets/graph-B6c5nlBL.js +1 -0
  57. package/payload/server/public/assets/graph-labels-BMqBNCYc.js +1 -0
  58. package/payload/server/public/assets/{graphlib-CuF7rLfE.js → graphlib-n_bUAhub.js} +1 -1
  59. package/payload/server/public/assets/{infoDiagram-42DDH7IO-wGl-JoP7.js → infoDiagram-42DDH7IO-rFap2NoE.js} +1 -1
  60. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-B3TxFNi_.js → ishikawaDiagram-UXIWVN3A-KKAj626l.js} +1 -1
  61. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-B5n8No7p.js → journeyDiagram-VCZTEJTY-Do6hVThi.js} +1 -1
  62. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-ByGF39_h.js → kanban-definition-6JOO6SKY-C38qlZwy.js} +1 -1
  63. package/payload/server/public/assets/lib-D-jcbjd4.js +29 -0
  64. package/payload/server/public/assets/{line-Br0FyyFW.js → line-DsDSEpXU.js} +1 -1
  65. package/payload/server/public/assets/{mermaid-parser.core-BUcTjodx.js → mermaid-parser.core-Cvzc7ePL.js} +1 -1
  66. package/payload/server/public/assets/{mermaid.core-BKzdtZgG.js → mermaid.core-BSZExB1C.js} +3 -3
  67. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CobGMF9l.js → mindmap-definition-QFDTVHPH-CrS3b5u5.js} +1 -1
  68. package/payload/server/public/assets/page-C438DjPa.js +1 -0
  69. package/payload/server/public/assets/{page-Ba0BAfpu.js → page-fdrpcuv5.js} +2 -2
  70. package/payload/server/public/assets/{pieDiagram-DEJITSTG-DXFmGcNR.js → pieDiagram-DEJITSTG-Dk-6uOEj.js} +1 -1
  71. package/payload/server/public/assets/public-BK3eOHJs.js +8 -0
  72. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-BnChStlO.js → quadrantDiagram-34T5L4WZ-CEMK5xQ0.js} +1 -1
  73. package/payload/server/public/assets/{requirementDiagram-MS252O5E-DdiBKsyO.js → requirementDiagram-MS252O5E-vTyRxQOY.js} +1 -1
  74. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-16-np_56.js → sankeyDiagram-XADWPNL6-s2z8dMpM.js} +1 -1
  75. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-D67SCRpA.js → sequenceDiagram-FGHM5R23-locoOgnm.js} +1 -1
  76. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-CvNJX0YC.js → stateDiagram-FHFEXIEX-m1d4aD0M.js} +1 -1
  77. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-B7eWHz7x.js +1 -0
  78. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BJE5YoBa.js → timeline-definition-GMOUNBTQ-HraDHuOp.js} +1 -1
  79. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-DF17ddXt.js → vennDiagram-DHZGUBPP-DkyF2rFo.js} +1 -1
  80. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-xn_2XUOo.js → wardleyDiagram-NUSXRM2D-BGScmzP0.js} +1 -1
  81. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DVF8zf3f.js → xychartDiagram-5P7HB3ND-CPJ1JLj2.js} +1 -1
  82. package/payload/server/public/brand-constants.json +1 -1
  83. package/payload/server/public/data.html +5 -5
  84. package/payload/server/public/graph.html +6 -6
  85. package/payload/server/public/index.html +8 -8
  86. package/payload/server/public/public.html +5 -5
  87. package/payload/server/server.js +2 -2
  88. package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-memory-write-revoke.test.sh +0 -248
  89. package/payload/platform/plugins/cloudflare/scripts/__tests__/setup-tunnel-display-precedence.test.sh +0 -194
  90. package/payload/server/public/assets/Checkbox-B34vplI7.js +0 -1
  91. package/payload/server/public/assets/admin-BkTlPuP0.js +0 -217
  92. package/payload/server/public/assets/channel-CZG4mVkt.js +0 -1
  93. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BoqQ3pHg.js +0 -1
  94. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-FoKuBK7n.js +0 -1
  95. package/payload/server/public/assets/clone-CT8AM-bU.js +0 -1
  96. package/payload/server/public/assets/data-BIDM6J5V.js +0 -1
  97. package/payload/server/public/assets/device-url-actions-nxh9PmIH.js +0 -33
  98. package/payload/server/public/assets/graph-Cm-iWFqy.js +0 -1
  99. package/payload/server/public/assets/graph-labels-CFX6KkCz.js +0 -1
  100. package/payload/server/public/assets/page-B5NRnW0t.js +0 -1
  101. package/payload/server/public/assets/public-DWfBzf83.js +0 -8
  102. 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.85",
3
+ "version": "0.1.87",
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 brand's VNC chromium via
20
- # `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,72 +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 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.
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.
113
102
  SETUP_TUNNEL_BRAND_JSON=""
114
103
  if [ -n "${MAXY_PLATFORM_ROOT:-}" ] && [ -f "${MAXY_PLATFORM_ROOT}/config/brand.json" ]; then
115
104
  SETUP_TUNNEL_BRAND_JSON="${MAXY_PLATFORM_ROOT}/config/brand.json"
116
105
  fi
117
- BRAND_VNC_DISPLAY=""
118
- if [ -n "${SETUP_TUNNEL_BRAND_JSON}" ] && command -v jq >/dev/null 2>&1; then
119
- _bvd=$(jq -r '.vncDisplay // empty' "${SETUP_TUNNEL_BRAND_JSON}" 2>/dev/null || true)
120
- if [ -n "${_bvd}" ] && [ "${_bvd}" -eq "${_bvd}" ] 2>/dev/null; then
121
- BRAND_VNC_DISPLAY=":${_bvd}"
122
- fi
123
- fi
124
-
125
- # read the install-time-resolved non-snap Chromium binary path so
126
- # the OAuth-URL spawn below uses the same binary vnc.sh's start_chrome runs.
127
- # Hardcoded `/usr/bin/chromium` would re-introduce the snap-AppArmor failure
128
- # on Ubuntu Noble laptop. The installer writes this file under platform/config/
129
- # during installSystemDeps; the spawn at step=browser-spawn fails loud if the
130
- # file is absent rather than silently falling back to /usr/bin/chromium.
131
- #
132
- # four discrete branches (env-unset / file-missing / file-empty /
133
- # binary-not-executable) so the failure message names the actual condition.
134
- # Pre-957 the four were conflated into one "cannot resolve a non-snap Chromium
135
- # binary" message, which masked the env-propagation defect that closed three
136
- # prior tasks (836, 562, 862) and silently re-fired here.
137
- if [ -z "${MAXY_PLATFORM_ROOT:-}" ]; then
138
- phase_line setup-tunnel step=chromium-resolve result=error reason=env-unset var=MAXY_PLATFORM_ROOT
139
- echo "ERROR: setup-tunnel.sh: MAXY_PLATFORM_ROOT is unset in the script's env." >&2
140
- echo " The action runner must declare MAXY_PLATFORM_ROOT in the cloudflare-setup" >&2
141
- echo " whitelist env: block (platform/ui/server/lib/action-runner.ts) — systemd-run --user" >&2
142
- echo " does not inherit the parent process env into the transient unit." >&2
143
- exit 1
144
- fi
145
- SETUP_TUNNEL_CHROMIUM_PATH_FILE="${MAXY_PLATFORM_ROOT}/config/chromium-binary.path"
146
- if [ ! -r "${SETUP_TUNNEL_CHROMIUM_PATH_FILE}" ]; then
147
- phase_line setup-tunnel step=chromium-resolve result=error reason=path-file-missing path="${SETUP_TUNNEL_CHROMIUM_PATH_FILE}"
148
- echo "ERROR: setup-tunnel.sh: chromium-binary.path file is missing or unreadable." >&2
149
- echo " Expected: ${SETUP_TUNNEL_CHROMIUM_PATH_FILE}" >&2
150
- echo " Re-run the installer to provision Chromium." >&2
151
- exit 1
152
- fi
153
- SETUP_TUNNEL_CHROMIUM_BIN="$(head -n1 "${SETUP_TUNNEL_CHROMIUM_PATH_FILE}" | tr -d '[:space:]')"
154
- if [ -z "${SETUP_TUNNEL_CHROMIUM_BIN}" ]; then
155
- phase_line setup-tunnel step=chromium-resolve result=error reason=path-file-empty path="${SETUP_TUNNEL_CHROMIUM_PATH_FILE}"
156
- echo "ERROR: setup-tunnel.sh: chromium-binary.path is empty." >&2
157
- echo " File: ${SETUP_TUNNEL_CHROMIUM_PATH_FILE}" >&2
158
- echo " Re-run the installer to provision Chromium." >&2
159
- exit 1
160
- fi
161
- if [ ! -x "${SETUP_TUNNEL_CHROMIUM_BIN}" ]; then
162
- phase_line setup-tunnel step=chromium-resolve result=error reason=binary-not-executable bin="${SETUP_TUNNEL_CHROMIUM_BIN}" path="${SETUP_TUNNEL_CHROMIUM_PATH_FILE}"
163
- echo "ERROR: setup-tunnel.sh: ${SETUP_TUNNEL_CHROMIUM_BIN} is not an executable file." >&2
164
- echo " Resolved from: ${SETUP_TUNNEL_CHROMIUM_PATH_FILE}" >&2
165
- echo " Re-run the installer to provision Chromium." >&2
166
- exit 1
167
- fi
168
106
 
169
107
  # --------------------------------------------------------------------------
170
108
  # Step 1: OAuth login. Corresponds to runbook Step 1.
@@ -179,32 +117,26 @@ fi
179
117
  # but failed before the mv;
180
118
  # promote and skip cloudflared.
181
119
  # both missing → spawn cloudflared, surface
182
- # the URL on the Pi VNC,
183
- # wait for the operator's
184
- # click, poll for cert.pem,
185
- # 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.
186
124
  #
187
- # Control flow when both certs are missing
188
- # relay third-party CLI):
125
+ # Control flow when both certs are missing:
189
126
  # 1. Spawn cloudflared with stdout+stderr teed line-by-line to
190
127
  # $STREAM_LOG_PATH with prefix [script:setup-tunnel:cloudflared]
191
128
  # (the chat-surface namespace — see _stream-log.sh header).
192
129
  # 2. Extract the authorize URL with a tolerant regex as it streams.
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> &`.
195
- # Fire-and-forget chromium is already running with CDP enabled at
196
- # :9222, so the invocation IPCs the URL into the running instance as
197
- # a new tab. The spawn is intentionally NOT tracked in the EXIT trap:
198
- # it is a sibling open, not a child of cloudflared, and an orphaned
199
- # late-arriving tab is harmless. Replaces cloudflared's own optimistic
200
- # xdg-open which does not reliably target VNC :99 in this environment.
201
- # 4. Emit `step=browser-spawn` and `step=browser-drive mode=operator-click`
202
- # so the stream log records the surface state.
203
- # 5. Wait for ~/.cloudflared/cert.pem to land (180 s budget — operator
204
- # must click the zone row + Authorize on VNC).
205
- # 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.
206
139
  #
207
- # No CDP auto-click. No DOM matcher. No consent-page driver of any kind.
208
140
  # Every failure branch exits 1 loudly naming the cause.
209
141
  # --------------------------------------------------------------------------
210
142
 
@@ -242,22 +174,7 @@ if [ ! -f "${CFG_DIR}/cert.pem" ] && [ -f "${HOME}/.cloudflared/cert.pem" ]; the
242
174
  fi
243
175
 
244
176
  if [ ! -f "${CFG_DIR}/cert.pem" ]; then
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}"
177
+ phase_line setup-tunnel step=oauth-login cert_path="${CFG_DIR}/cert.pem"
261
178
 
262
179
  URL_FILE="$(mktemp -t maxy-setup-tunnel-url.XXXXXX)"
263
180
  LAST_LINE_FILE="$(mktemp -t maxy-setup-tunnel-last.XXXXXX)"
@@ -269,16 +186,8 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
269
186
  # callback forever; subsequent setup-tunnel runs see a stale cert.pem
270
187
  # landing asynchronously and race against the new URL-extraction pass.
271
188
  CF_PIPELINE_PID=""
272
- CHROMIUM_UNIT=""
273
189
  cleanup_oauth() {
274
190
  [ -n "${CF_PIPELINE_PID}" ] && kill "${CF_PIPELINE_PID}" 2>/dev/null || true
275
- # stop the transient chromium unit on any early exit between
276
- # browser-spawn and the explicit step=browser-close site below. Best-
277
- # effort: no phase_line here because the EXIT trap fires on every path
278
- # (including the happy one where step=browser-close already ran and
279
- # auto-collected the unit). The `|| true` masks the inevitable "Unit
280
- # not loaded" return on the happy path.
281
- [ -n "${CHROMIUM_UNIT}" ] && systemctl --user stop "${CHROMIUM_UNIT}" 2>/dev/null || true
282
191
  rm -f "${URL_FILE}" "${LAST_LINE_FILE}"
283
192
  }
284
193
  trap cleanup_oauth EXIT
@@ -287,7 +196,7 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
287
196
  # extracted as it streams. The subshell holds the whole pipeline so
288
197
  # PIPESTATUS[0] (cloudflared's exit code) is reachable later.
289
198
  (
290
- DISPLAY="${BRAND_VNC_DISPLAY}" stdbuf -oL -eL cloudflared \
199
+ stdbuf -oL -eL cloudflared \
291
200
  --origincert "${CFG_DIR}/cert.pem" tunnel login 2>&1 |
292
201
  while IFS= read -r line; do
293
202
  ts="$(stream_log_ts)"
@@ -329,71 +238,17 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
329
238
  fi
330
239
 
331
240
  AUTH_URL="$(cat "${URL_FILE}")"
332
- phase_line setup-tunnel step=browser-drive url_extracted=1
241
+ phase_line setup-tunnel step=oauth-url-extracted url_extracted=1
333
242
 
334
243
  # Emit the URL on stdout in a shape ActionLogPanel's regex captures.
335
- # The button is a redundant re-spawn fallback by the time the chat-side
336
- # ActionLogPanel renders, the page is already on the Pi VNC chromium per
337
- # the spawn below. Button click POSTs to /api/admin/device-browser/navigate
338
- # 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.
339
247
  printf 'OAUTH_URL: %s\n' "${AUTH_URL}"
340
248
 
341
- # Mechanically open the URL on the Pi VNC chromium. Chromium
342
- # is already running on this brand's ${BRAND_VNC_DISPLAY} with CDP enabled
343
- # (vnc.sh start_chrome at boot); invoking the resolved binary <url> against
344
- # a running instance IPCs the URL into it as a new tab. Replaces
345
- # cloudflared's own optimistic xdg-open, which does not reliably target
346
- # the brand's VNC display in this environment.
347
- #
348
- # chromium is launched under a transient systemd-user unit so
349
- # the full process tree (including any standalone chromium that lands
350
- # when no existing instance is running for IPC) lives in its own cgroup.
351
- # On cert.pem arrival the unit is stopped, SIGTERMing the whole cgroup
352
- # atomically. Pre-Task-982 the spawn was `&` fire-and-forget with no
353
- # tracked PID; the resulting orphan chromium on display :101 was the
354
- # symptom in maxy-2 2026-05-12T10:06–10:08Z. `step=browser-close
355
- # result=ok|orphan` records the teardown outcome at cert.pem mv site
356
- # below.
357
- #
358
- # Binary path: SETUP_TUNNEL_CHROMIUM_BIN is read at startup from
359
- # ${MAXY_PLATFORM_ROOT}/config/chromium-binary.path — `/usr/bin/chromium`
360
- # on Pi Bookworm and `/usr/bin/google-chrome-stable` on Ubuntu Noble laptop
361
- # where the system chromium is snap-confined. Hardcoding
362
- # `/usr/bin/chromium` here would re-introduce the AppArmor SingletonLock
363
- # failure on the laptop.
364
- CHROMIUM_UNIT="maxy-oauth-chromium-${BRAND}-$$.service"
365
- CHROMIUM_LAUNCH_DISPLAY="${BRAND_VNC_DISPLAY}"
366
- CHROMIUM_SPAWN_ERR="$(mktemp -t maxy-oauth-chromium-err.XXXXXX)"
367
- if systemd-run --user \
368
- --unit="${CHROMIUM_UNIT}" \
369
- --description="Maxy OAuth chromium for ${BRAND}" \
370
- --collect \
371
- --setenv=DISPLAY="${CHROMIUM_LAUNCH_DISPLAY}" \
372
- "${SETUP_TUNNEL_CHROMIUM_BIN}" "${AUTH_URL}" 2>"${CHROMIUM_SPAWN_ERR}"; then
373
- rm -f "${CHROMIUM_SPAWN_ERR}"
374
- phase_line setup-tunnel step=browser-spawn result=ok \
375
- display="${CHROMIUM_LAUNCH_DISPLAY}" url_extracted=1 unit="${CHROMIUM_UNIT}"
376
- else
377
- SPAWN_RC=$?
378
- SPAWN_STDERR="$(tr '\n' ' ' < "${CHROMIUM_SPAWN_ERR}" | head -c 300 || echo unavailable)"
379
- rm -f "${CHROMIUM_SPAWN_ERR}"
380
- # Loud-fail rather than fire-and-forget fallback: a systemd-run failure
381
- # leaves no teardown handle. Operator should see the bus-not-running
382
- # / linger-not-enabled cause.
383
- phase_line setup-tunnel step=browser-spawn result=error \
384
- reason=systemd-run-failed exit="${SPAWN_RC}" stderr="${SPAWN_STDERR}" \
385
- unit="${CHROMIUM_UNIT}"
386
- echo "ERROR: systemd-run failed to spawn chromium under transient unit (exit=${SPAWN_RC})." >&2
387
- echo " systemd-run stderr: ${SPAWN_STDERR}" >&2
388
- echo " If stderr mentions 'Failed to connect to bus', enable user-scope" >&2
389
- echo " systemd via 'loginctl enable-linger \$(whoami)' and retry." >&2
390
- exit 1
391
- fi
392
- phase_line setup-tunnel step=browser-drive mode=operator-click url="${AUTH_URL}"
393
-
394
249
  # Wait for cert.pem to land — cloudflared writes to ~/.cloudflared/cert.pem
395
250
  # regardless of --origincert, so watch the canonical location. The wait is
396
- # 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.
397
252
  # 180 s default budget; SETUP_TUNNEL_LOGIN_TIMEOUT overrides for testing.
398
253
  # The 2-second heartbeat inside the loop is the observability contract —
399
254
  # no form-spawned script is allowed a silent poll of more than ~2 s.
@@ -417,10 +272,10 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
417
272
  echo "ERROR: Timed out after ${LOGIN_WAIT}s waiting for cert.pem to land." >&2
418
273
  exit 1
419
274
  fi
420
- # Heartbeat every 2 s after the click. t=0 is the browser-drive
421
- # phase line above; the first heartbeat fires at t=2. Without this line
422
- # the tailer sees silence for the full 1-20 s round-trip — the exact
423
- # 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.
424
279
  if [ "${LOGIN_WAIT}" -gt 0 ] && [ $((LOGIN_WAIT % 2)) -eq 0 ]; then
425
280
  phase_line setup-tunnel step=oauth-login result=awaiting-cert \
426
281
  elapsed="${LOGIN_WAIT}s" timeout="${LOGIN_TIMEOUT}s"
@@ -432,45 +287,6 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
432
287
  mv "${HOME}/.cloudflared/cert.pem" "${CFG_DIR}/cert.pem"
433
288
  phase_line setup-tunnel step=oauth-login result=ok \
434
289
  path="${CFG_DIR}/cert.pem" waited="${LOGIN_WAIT}s"
435
-
436
- # SIGTERM the OAuth chromium cgroup now that cert.pem has
437
- # landed. The transient unit was created above at step=browser-spawn; if
438
- # chromium IPCs'd to a running brand-VNC instance and exited cleanly, the
439
- # unit is already auto-collected and `systemctl stop` returns 0 (no-such-
440
- # unit is a benign race, not an orphan). If chromium is still alive (no
441
- # pre-existing brand-VNC instance to IPC into), SIGTERM tears the whole
442
- # cgroup atomically. `result=ok` covers both clean paths; `result=orphan`
443
- # fires only when the stop command itself fails (bus issue, race with
444
- # auto-collect that returned non-zero) — operator-visible signal that an
445
- # orphan chromium MAY still be alive on the VNC display.
446
- CHROMIUM_STOP_ERR="$(mktemp -t maxy-oauth-chromium-stop-err.XXXXXX)"
447
- if systemctl --user stop "${CHROMIUM_UNIT}" 2>"${CHROMIUM_STOP_ERR}"; then
448
- rm -f "${CHROMIUM_STOP_ERR}"
449
- phase_line setup-tunnel step=browser-close result=ok unit="${CHROMIUM_UNIT}"
450
- else
451
- STOP_RC=$?
452
- STOP_STDERR="$(tr '\n' ' ' < "${CHROMIUM_STOP_ERR}" | head -c 300 || echo unavailable)"
453
- rm -f "${CHROMIUM_STOP_ERR}"
454
- # Distinguish benign "unit already auto-collected" from a true teardown
455
- # failure via systemctl's exit-code taxonomy — never via stderr prose
456
- # parsing, which breaks on non-English locales (no-stdout-parsing-for-
457
- # control-flow doctrine). Exit code 5 is systemd's canonical "Unit not
458
- # loaded" return; --collect auto-GCs a terminated unit between the
459
- # chromium-side IPC-and-exit and our stop, producing exactly this code.
460
- # Any other non-zero exit is a real teardown failure (bus down, permission,
461
- # service still alive but stop hung).
462
- if [ "${STOP_RC}" -eq 5 ]; then
463
- phase_line setup-tunnel step=browser-close result=ok \
464
- reason=unit-auto-collected unit="${CHROMIUM_UNIT}"
465
- else
466
- phase_line setup-tunnel step=browser-close result=orphan \
467
- reason=stop-failed exit="${STOP_RC}" stderr="${STOP_STDERR}" \
468
- unit="${CHROMIUM_UNIT}"
469
- echo "WARNING: failed to stop transient chromium unit ${CHROMIUM_UNIT} (exit=${STOP_RC})." >&2
470
- echo " An orphan chromium may remain on display ${CHROMIUM_LAUNCH_DISPLAY}." >&2
471
- echo " systemctl stderr: ${STOP_STDERR}" >&2
472
- fi
473
- fi
474
290
  fi
475
291
 
476
292
  # --------------------------------------------------------------------------