@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.
- 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 -197
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +12 -1
- 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/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-CvpZ5VUN.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-CBeNa5rc.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-BMD0nCx5.js +1 -0
- package/payload/server/public/assets/graph-labels-CSqIl6zN.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-CQUvdg_a.js +1 -0
- package/payload/server/public/assets/{page-Ba0BAfpu.js → page-DVEcL-Uw.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/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,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
|
-
#
|
|
103
|
-
#
|
|
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
|
|
178
|
-
#
|
|
179
|
-
#
|
|
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.
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
#
|
|
193
|
-
#
|
|
194
|
-
#
|
|
195
|
-
#
|
|
196
|
-
#
|
|
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"
|
|
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
|
-
|
|
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=
|
|
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
|
|
316
|
-
#
|
|
317
|
-
# the
|
|
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
|
|
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
|
|
401
|
-
#
|
|
402
|
-
#
|
|
403
|
-
#
|
|
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
|
|
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
|
|
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
|
|