@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.
- package/package.json +1 -1
- package/payload/platform/config/brand.json +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-memory-write-passthrough.test.sh +118 -0
- package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +0 -102
- package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -1
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +33 -217
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +9 -7
- package/payload/platform/plugins/docs/references/admin-session.md +1 -1
- package/payload/platform/plugins/docs/references/cloudflare.md +2 -2
- package/payload/platform/plugins/docs/references/deployment.md +1 -1
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +2 -2
- package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/config.js +1 -8
- package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +15 -15
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/server/public/assets/{brand-C_iHnsbT.css → ChatInput-CktRrxx1.css} +1 -1
- package/payload/server/public/assets/{brand-HqTMr1bV.js → ChatInput-D62NmYfJ.js} +5 -1
- package/payload/server/public/assets/Checkbox-D2XqbR29.js +1 -0
- package/payload/server/public/assets/admin-BK1F2LMD.js +217 -0
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-DQMCKDU2.js → architectureDiagram-Q4EWVU46-9F6y6ESU.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-CqJNtiQY.js → blockDiagram-DXYQGD6D-LjGbzg0M.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-BVAa1YL3.js → c4Diagram-AHTNJAMY-Bxpn5T5R.js} +1 -1
- package/payload/server/public/assets/channel-CY_QocHj.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-XIsdqKyD.js → chunk-336JU56O-Cc2kxWVN.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-Dw4mcMYK.js → chunk-426QAEUC-DRzEhHvW.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-BvrlG2FS.js → chunk-4TB4RGXK-C8zRzH9g.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-3-UZ9191.js → chunk-5FUZZQ4R-s8V7nzOd.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-DOeY60Lp.js → chunk-5PVQY5BW-KNShBlaM.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-BSbGSOxN.js → chunk-EDXVE4YY-C6GvGxn3.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-CszgAcWZ.js → chunk-ENJZ2VHE-XvXhU8ZS.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-rgw1I6pt.js → chunk-ICPOFSXX-OSDYUb0H.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-DL4v2Nx4.js → chunk-OYMX7WX6-km_4dA-E.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-CwMCwyfq.js → chunk-U2HBQHQK-BGHqDvzJ.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-CvKfDVvq.js → chunk-X2U36JSP-BKKSYdFO.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-D7vju2_U.js → chunk-YZCP3GAM-DT_yhfJM.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-sANOwJKn.js → chunk-ZZ45TVLE-CP2JuBPA.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-BtwH3Rsu.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-tCRstAZ-.js +1 -0
- package/payload/server/public/assets/clone-Bc-lmwcQ.js +1 -0
- package/payload/server/public/assets/{dagre-CplqWwSs.js → dagre-CUhFHAG8.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-DNbl5g9o.js → dagre-KV5264BT-Dk7osC4w.js} +1 -1
- package/payload/server/public/assets/data-B3WPMQdR.js +1 -0
- package/payload/server/public/assets/{diagram-5BDNPKRD-BCdMaYnY.js → diagram-5BDNPKRD-CdOEJbjg.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-8XhDBU0o.js → diagram-G4DWMVQ6-Dyx0MQk6.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-M-RMQgtx.js → diagram-MMDJMWI5-Dw6sxLh8.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-BBHPIBvQ.js → diagram-TYMM5635-Bm3GRH-1.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-D6jOL-9s.js → erDiagram-SMLLAGMA-CAJ3Z9uD.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CiyTYRlU.js → flowDiagram-DWJPFMVM-NrVUcQ2d.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-DHSIpjfS.js → ganttDiagram-T4ZO3ILL-28YyrNXj.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CEneVF3o.js → gitGraphDiagram-UUTBAWPF-D73FSDE8.js} +1 -1
- package/payload/server/public/assets/graph-B6c5nlBL.js +1 -0
- package/payload/server/public/assets/graph-labels-BMqBNCYc.js +1 -0
- package/payload/server/public/assets/{graphlib-CuF7rLfE.js → graphlib-n_bUAhub.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-wGl-JoP7.js → infoDiagram-42DDH7IO-rFap2NoE.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-B3TxFNi_.js → ishikawaDiagram-UXIWVN3A-KKAj626l.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-B5n8No7p.js → journeyDiagram-VCZTEJTY-Do6hVThi.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-ByGF39_h.js → kanban-definition-6JOO6SKY-C38qlZwy.js} +1 -1
- package/payload/server/public/assets/lib-D-jcbjd4.js +29 -0
- package/payload/server/public/assets/{line-Br0FyyFW.js → line-DsDSEpXU.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-BUcTjodx.js → mermaid-parser.core-Cvzc7ePL.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-BKzdtZgG.js → mermaid.core-BSZExB1C.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CobGMF9l.js → mindmap-definition-QFDTVHPH-CrS3b5u5.js} +1 -1
- package/payload/server/public/assets/page-C438DjPa.js +1 -0
- package/payload/server/public/assets/{page-Ba0BAfpu.js → page-fdrpcuv5.js} +2 -2
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-DXFmGcNR.js → pieDiagram-DEJITSTG-Dk-6uOEj.js} +1 -1
- package/payload/server/public/assets/public-BK3eOHJs.js +8 -0
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-BnChStlO.js → quadrantDiagram-34T5L4WZ-CEMK5xQ0.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-DdiBKsyO.js → requirementDiagram-MS252O5E-vTyRxQOY.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-16-np_56.js → sankeyDiagram-XADWPNL6-s2z8dMpM.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-D67SCRpA.js → sequenceDiagram-FGHM5R23-locoOgnm.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-CvNJX0YC.js → stateDiagram-FHFEXIEX-m1d4aD0M.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-B7eWHz7x.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BJE5YoBa.js → timeline-definition-GMOUNBTQ-HraDHuOp.js} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-DF17ddXt.js → vennDiagram-DHZGUBPP-DkyF2rFo.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-xn_2XUOo.js → wardleyDiagram-NUSXRM2D-BGScmzP0.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DVF8zf3f.js → xychartDiagram-5P7HB3ND-CPJ1JLj2.js} +1 -1
- package/payload/server/public/brand-constants.json +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +8 -8
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +2 -2
- package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-memory-write-revoke.test.sh +0 -248
- package/payload/platform/plugins/cloudflare/scripts/__tests__/setup-tunnel-display-precedence.test.sh +0 -194
- package/payload/server/public/assets/Checkbox-B34vplI7.js +0 -1
- package/payload/server/public/assets/admin-BkTlPuP0.js +0 -217
- package/payload/server/public/assets/channel-CZG4mVkt.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-BoqQ3pHg.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-FoKuBK7n.js +0 -1
- package/payload/server/public/assets/clone-CT8AM-bU.js +0 -1
- package/payload/server/public/assets/data-BIDM6J5V.js +0 -1
- package/payload/server/public/assets/device-url-actions-nxh9PmIH.js +0 -33
- package/payload/server/public/assets/graph-Cm-iWFqy.js +0 -1
- package/payload/server/public/assets/graph-labels-CFX6KkCz.js +0 -1
- package/payload/server/public/assets/page-B5NRnW0t.js +0 -1
- package/payload/server/public/assets/public-DWfBzf83.js +0 -8
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CFw2U4iL.js +0 -1
package/package.json
CHANGED
package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-memory-write-passthrough.test.sh
ADDED
|
@@ -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,
|
|
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
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
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
|
-
#
|
|
103
|
-
#
|
|
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
|
|
183
|
-
#
|
|
184
|
-
#
|
|
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.
|
|
194
|
-
#
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
#
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
|
336
|
-
#
|
|
337
|
-
# the
|
|
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
|
|
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
|
|
421
|
-
#
|
|
422
|
-
#
|
|
423
|
-
#
|
|
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
|
# --------------------------------------------------------------------------
|