@rubytech/create-maxy 1.0.799 → 1.0.801
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/dist/index.js +7 -1
- package/package.json +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/archive-ingest-surface-gate.test.sh +191 -0
- package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +207 -0
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +12 -0
- package/payload/platform/plugins/cloudflare/scripts/_cdp-authorize-matcher.mjs +74 -0
- package/payload/platform/plugins/cloudflare/scripts/_cdp-authorize.mjs +60 -50
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +118 -22
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +4 -0
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/whatsapp-import/PLUGIN.md +2 -2
- package/payload/platform/plugins/whatsapp-import/bin/ingest.mjs +732 -0
- package/payload/platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh +102 -0
- package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/SKILL.md +49 -97
- package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/references/export-parse.md +1 -1
- package/payload/platform/scripts/seed-neo4j.sh +24 -15
- package/payload/platform/templates/specialists/agents/database-operator.md +13 -3
- package/payload/server/public/assets/{admin-C0lKk6WM.js → admin-Sa301b8q.js} +6 -6
- package/payload/server/public/index.html +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/archive-ingest-gate.test.sh +0 -166
- package/payload/platform/plugins/admin/hooks/archive-ingest-gate.sh +0 -147
- package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/references/conversation-and-messages.md +0 -99
- package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/references/insight-extraction.md +0 -121
package/dist/index.js
CHANGED
|
@@ -1765,11 +1765,17 @@ function installTunnelScripts() {
|
|
|
1765
1765
|
// $HOME symlink — it's a helper, not a top-level operator command. We do
|
|
1766
1766
|
// chmod +x defensively so `ls -l` and any ad-hoc `~/setup-tunnel.sh` copy
|
|
1767
1767
|
// flow sees it as executable (Task 588).
|
|
1768
|
+
//
|
|
1769
|
+
// _cdp-authorize-matcher.mjs is imported by _cdp-authorize.mjs (Task 855)
|
|
1770
|
+
// — the tri-state matcher's MATCH_EXPR + findMatch live in this side
|
|
1771
|
+
// module so JSDOM tests run identical logic to the live page. chmod +x
|
|
1772
|
+
// is defensive symmetry; the file is read by Node, not exec'd.
|
|
1768
1773
|
const cdpAuthorizeSrc = join(INSTALL_DIR, "platform/plugins/cloudflare/scripts/_cdp-authorize.mjs");
|
|
1774
|
+
const cdpMatcherSrc = join(INSTALL_DIR, "platform/plugins/cloudflare/scripts/_cdp-authorize-matcher.mjs");
|
|
1769
1775
|
const setupLink = resolve(process.env.HOME ?? "/root", "setup-tunnel.sh");
|
|
1770
1776
|
const resetLink = resolve(process.env.HOME ?? "/root", "reset-tunnel.sh");
|
|
1771
1777
|
const listLink = resolve(process.env.HOME ?? "/root", "list-cf-domains.sh");
|
|
1772
|
-
for (const src of [setupSrc, resetSrc, listSrc, cdpAuthorizeSrc]) {
|
|
1778
|
+
for (const src of [setupSrc, resetSrc, listSrc, cdpAuthorizeSrc, cdpMatcherSrc]) {
|
|
1773
1779
|
try {
|
|
1774
1780
|
chmodSync(src, 0o755);
|
|
1775
1781
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Regression test for archive-ingest-surface-gate.sh (Task 855).
|
|
3
|
+
#
|
|
4
|
+
# Covers:
|
|
5
|
+
# Preserved-from-Task-846:
|
|
6
|
+
# 1. Edit on /platform/plugins/<x>/lib/* → BLOCKED
|
|
7
|
+
# 2. Edit on benign path → ALLOWED
|
|
8
|
+
# 3. Bash with `npx vitest`/`bun test`/`npm test` → BLOCKED
|
|
9
|
+
# 4. PostToolUse on whatsapp-export-parse with isError:true sets flag
|
|
10
|
+
# 5. Subsequent PreToolUse on ANY tool → BLOCKED
|
|
11
|
+
# 6. UserPromptSubmit clears flag → normal allow resumes
|
|
12
|
+
# 7. PostToolUse with isError:false → flag absent
|
|
13
|
+
# 8. Stale flag (>600s) auto-clears
|
|
14
|
+
# New (Task 855):
|
|
15
|
+
# A. PreToolUse mcp__memory__whatsapp-export-parse → BLOCKED
|
|
16
|
+
# B. PreToolUse mcp__memory__whatsapp-export-insight-write → BLOCKED
|
|
17
|
+
# C. PreToolUse mcp__memory__memory-archive-write w/ archiveType=whatsapp-export → BLOCKED
|
|
18
|
+
# D. PreToolUse mcp__memory__memory-archive-write w/ archiveType=linkedin-connections → ALLOWED
|
|
19
|
+
# E. PreToolUse Bash invoking whatsapp-ingest.sh → ALLOWED
|
|
20
|
+
# F. Default-allow emits a [archive-ingest-gate] decision=allow log line
|
|
21
|
+
|
|
22
|
+
set -u
|
|
23
|
+
|
|
24
|
+
HOOK="$(cd "$(dirname "$0")/.." && pwd)/archive-ingest-surface-gate.sh"
|
|
25
|
+
if [[ ! -x "$HOOK" ]]; then
|
|
26
|
+
echo "FAIL: $HOOK not executable" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
STATE_DIR=$(mktemp -d)
|
|
31
|
+
export ARCHIVE_INGEST_GATE_STATE_DIR="$STATE_DIR"
|
|
32
|
+
FLAG_FILE="$STATE_DIR/archive-ingest-parse-error.flag"
|
|
33
|
+
|
|
34
|
+
cleanup() { rm -rf "$STATE_DIR"; }
|
|
35
|
+
trap cleanup EXIT
|
|
36
|
+
|
|
37
|
+
PASS=0
|
|
38
|
+
FAIL=0
|
|
39
|
+
|
|
40
|
+
run_case() {
|
|
41
|
+
local name="$1" stdin="$2" expected_exit="$3"
|
|
42
|
+
local actual_exit
|
|
43
|
+
printf '%s' "$stdin" | bash "$HOOK" >/dev/null 2>/dev/null
|
|
44
|
+
actual_exit=$?
|
|
45
|
+
if [[ "$actual_exit" -eq "$expected_exit" ]]; then
|
|
46
|
+
echo "PASS: $name (exit=$actual_exit)"
|
|
47
|
+
PASS=$((PASS + 1))
|
|
48
|
+
else
|
|
49
|
+
echo "FAIL: $name (expected exit=$expected_exit, got=$actual_exit)" >&2
|
|
50
|
+
FAIL=$((FAIL + 1))
|
|
51
|
+
fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Preserved cases ---------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
run_case "Edit on platform/plugins/whatsapp-import/lib/src/parse-export.ts → BLOCKED" \
|
|
57
|
+
'{"hook_event_name":"PreToolUse","tool_name":"Edit","tool_input":{"file_path":"/Users/x/repo/platform/plugins/whatsapp-import/lib/src/parse-export.ts","old_string":"a","new_string":"b"}}' \
|
|
58
|
+
2
|
|
59
|
+
|
|
60
|
+
run_case "Edit on README.md → ALLOWED" \
|
|
61
|
+
'{"hook_event_name":"PreToolUse","tool_name":"Edit","tool_input":{"file_path":"/Users/x/repo/README.md","old_string":"a","new_string":"b"}}' \
|
|
62
|
+
0
|
|
63
|
+
|
|
64
|
+
run_case "Bash 'npx vitest run' → BLOCKED" \
|
|
65
|
+
'{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npx vitest run parse-export.test.ts"}}' \
|
|
66
|
+
2
|
|
67
|
+
|
|
68
|
+
run_case "Bash 'ls -la' → ALLOWED" \
|
|
69
|
+
'{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"ls -la"}}' \
|
|
70
|
+
0
|
|
71
|
+
|
|
72
|
+
run_case "Bash 'bun test' → BLOCKED" \
|
|
73
|
+
'{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"bun test"}}' \
|
|
74
|
+
2
|
|
75
|
+
|
|
76
|
+
run_case "Bash 'npm test' → BLOCKED" \
|
|
77
|
+
'{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npm test"}}' \
|
|
78
|
+
2
|
|
79
|
+
|
|
80
|
+
# New (Task 855) cases ----------------------------------------------------
|
|
81
|
+
|
|
82
|
+
run_case "PreToolUse mcp__memory__whatsapp-export-parse → BLOCKED" \
|
|
83
|
+
'{"hook_event_name":"PreToolUse","tool_name":"mcp__memory__whatsapp-export-parse","tool_input":{"filePath":"/tmp/_chat.txt","accountId":"acct1","timezone":"Europe/London"}}' \
|
|
84
|
+
2
|
|
85
|
+
|
|
86
|
+
run_case "PreToolUse mcp__memory__whatsapp-export-insight-write → BLOCKED" \
|
|
87
|
+
'{"hook_event_name":"PreToolUse","tool_name":"mcp__memory__whatsapp-export-insight-write","tool_input":{"kind":"MENTIONS","name":"Joel"}}' \
|
|
88
|
+
2
|
|
89
|
+
|
|
90
|
+
run_case "PreToolUse memory-archive-write w/ archiveType=whatsapp-export → BLOCKED" \
|
|
91
|
+
'{"hook_event_name":"PreToolUse","tool_name":"mcp__memory__memory-archive-write","tool_input":{"archiveType":"whatsapp-export","ownerNodeId":"x","accountId":"a","rows":[]}}' \
|
|
92
|
+
2
|
|
93
|
+
|
|
94
|
+
run_case "PreToolUse memory-archive-write w/ archiveType=linkedin-connections → ALLOWED" \
|
|
95
|
+
'{"hook_event_name":"PreToolUse","tool_name":"mcp__memory__memory-archive-write","tool_input":{"archiveType":"linkedin-connections","ownerNodeId":"x","accountId":"a","rows":[]}}' \
|
|
96
|
+
0
|
|
97
|
+
|
|
98
|
+
# Bypass attempts (Task 855 code-review C1): nested archiveType in rows[0]
|
|
99
|
+
# or conversation must NOT defeat the block. The gate must read the
|
|
100
|
+
# top-level tool_input.archiveType, not the first textual occurrence.
|
|
101
|
+
run_case "BYPASS: nested rows[0].archiveType=linkedin + top-level archiveType=whatsapp-export → BLOCKED" \
|
|
102
|
+
'{"hook_event_name":"PreToolUse","tool_name":"mcp__memory__memory-archive-write","tool_input":{"rows":[{"archiveType":"linkedin-connections"}],"archiveType":"whatsapp-export","ownerNodeId":"x","accountId":"a"}}' \
|
|
103
|
+
2
|
|
104
|
+
|
|
105
|
+
run_case "BYPASS: conversation.archiveType=linkedin + top-level archiveType=whatsapp-export → BLOCKED" \
|
|
106
|
+
'{"hook_event_name":"PreToolUse","tool_name":"mcp__memory__memory-archive-write","tool_input":{"conversation":{"archiveType":"linkedin-connections","conversationId":"x"},"archiveType":"whatsapp-export","ownerNodeId":"x","accountId":"a","rows":[]}}' \
|
|
107
|
+
2
|
|
108
|
+
|
|
109
|
+
# Plugin-source-edit path block must read tool_input.file_path top-level,
|
|
110
|
+
# not a nested file_path in old_string/new_string.
|
|
111
|
+
run_case "BYPASS: nested file_path in old_string + top-level file_path in lib/* → BLOCKED" \
|
|
112
|
+
'{"hook_event_name":"PreToolUse","tool_name":"Edit","tool_input":{"file_path":"/repo/platform/plugins/whatsapp-import/lib/src/parse-export.ts","old_string":"file_path:/safe/path","new_string":"x"}}' \
|
|
113
|
+
2
|
|
114
|
+
|
|
115
|
+
run_case "PreToolUse Bash invoking whatsapp-ingest.sh → ALLOWED" \
|
|
116
|
+
'{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"bash platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh /tmp/chat.zip --owner-element-id 4:abc:1 --scope admin"}}' \
|
|
117
|
+
0
|
|
118
|
+
|
|
119
|
+
# Default-allow log-line check
|
|
120
|
+
LOG=$(printf '%s' '{"hook_event_name":"PreToolUse","tool_name":"Read","tool_input":{"file_path":"/tmp/foo"}}' | bash "$HOOK" 2>&1 1>/dev/null)
|
|
121
|
+
if printf '%s' "$LOG" | grep -q '\[archive-ingest-gate\] decision=allow tool=Read reason=default'; then
|
|
122
|
+
echo "PASS: default-allow emits decision-log line"
|
|
123
|
+
PASS=$((PASS + 1))
|
|
124
|
+
else
|
|
125
|
+
echo "FAIL: default-allow missing decision-log line. Got: $LOG" >&2
|
|
126
|
+
FAIL=$((FAIL + 1))
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# Block-log line includes command field for Bash
|
|
130
|
+
LOG=$(printf '%s' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npx vitest run"}}' | bash "$HOOK" 2>&1 1>/dev/null)
|
|
131
|
+
if printf '%s' "$LOG" | grep -q '\[archive-ingest-gate\] decision=block tool=Bash reason=test-runner'; then
|
|
132
|
+
echo "PASS: block emits decision-log line with reason"
|
|
133
|
+
PASS=$((PASS + 1))
|
|
134
|
+
else
|
|
135
|
+
echo "FAIL: block missing decision-log line. Got: $LOG" >&2
|
|
136
|
+
FAIL=$((FAIL + 1))
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# Parse-error flag lifecycle (preserved)
|
|
140
|
+
rm -f "$FLAG_FILE"
|
|
141
|
+
run_case "PostToolUse parse-error sets flag (exit 0)" \
|
|
142
|
+
'{"hook_event_name":"PostToolUse","tool_name":"mcp__memory__whatsapp-export-parse","tool_input":{"filePath":"_chat.txt"},"tool_response":{"isError":true,"content":[{"type":"text","text":"parse-error file=_chat.txt line=1 reason=not-a-_chat.txt"}]}}' \
|
|
143
|
+
0
|
|
144
|
+
[[ -f "$FLAG_FILE" ]] && { echo "PASS: parse-error flag created"; PASS=$((PASS+1)); } \
|
|
145
|
+
|| { echo "FAIL: parse-error flag NOT created" >&2; FAIL=$((FAIL+1)); }
|
|
146
|
+
|
|
147
|
+
run_case "PreToolUse Read after parse-error → BLOCKED" \
|
|
148
|
+
'{"hook_event_name":"PreToolUse","tool_name":"Read","tool_input":{"file_path":"/tmp/foo"}}' \
|
|
149
|
+
2
|
|
150
|
+
|
|
151
|
+
run_case "UserPromptSubmit clears flag" \
|
|
152
|
+
'{"hook_event_name":"UserPromptSubmit","prompt":"retry"}' \
|
|
153
|
+
0
|
|
154
|
+
[[ ! -f "$FLAG_FILE" ]] && { echo "PASS: UserPromptSubmit cleared flag"; PASS=$((PASS+1)); } \
|
|
155
|
+
|| { echo "FAIL: UserPromptSubmit did NOT clear flag" >&2; FAIL=$((FAIL+1)); }
|
|
156
|
+
|
|
157
|
+
run_case "PreToolUse Read after clearance → ALLOWED" \
|
|
158
|
+
'{"hook_event_name":"PreToolUse","tool_name":"Read","tool_input":{"file_path":"/tmp/foo"}}' \
|
|
159
|
+
0
|
|
160
|
+
|
|
161
|
+
# Stale flag auto-clears
|
|
162
|
+
PAST=$(( $(date -u +%s) - 700 ))
|
|
163
|
+
echo "$PAST" > "$FLAG_FILE"
|
|
164
|
+
run_case "Stale flag auto-clears, PreToolUse Read → ALLOWED" \
|
|
165
|
+
'{"hook_event_name":"PreToolUse","tool_name":"Read","tool_input":{"file_path":"/tmp/foo"}}' \
|
|
166
|
+
0
|
|
167
|
+
|
|
168
|
+
# PostToolUse parse-success leaves flag absent
|
|
169
|
+
rm -f "$FLAG_FILE"
|
|
170
|
+
run_case "PostToolUse parse-success (isError:false) does NOT set flag" \
|
|
171
|
+
'{"hook_event_name":"PostToolUse","tool_name":"mcp__memory__whatsapp-export-parse","tool_input":{"filePath":"_chat.txt"},"tool_response":{"isError":false,"content":[{"type":"text","text":"{\"parsedLines\":[]}"}]}}' \
|
|
172
|
+
0
|
|
173
|
+
[[ ! -f "$FLAG_FILE" ]] && { echo "PASS: parse-success leaves flag absent"; PASS=$((PASS+1)); } \
|
|
174
|
+
|| { echo "FAIL: parse-success incorrectly created flag" >&2; FAIL=$((FAIL+1)); }
|
|
175
|
+
|
|
176
|
+
# Fail-closed terminal check
|
|
177
|
+
if grep -q '\[ -t 0 \]' "$HOOK"; then
|
|
178
|
+
echo "PASS: fail-closed terminal check is present"
|
|
179
|
+
PASS=$((PASS + 1))
|
|
180
|
+
else
|
|
181
|
+
echo "FAIL: fail-closed terminal check missing" >&2
|
|
182
|
+
FAIL=$((FAIL + 1))
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
echo
|
|
186
|
+
echo "──────── archive-ingest-surface-gate test summary ────────"
|
|
187
|
+
echo "PASS: $PASS"
|
|
188
|
+
echo "FAIL: $FAIL"
|
|
189
|
+
|
|
190
|
+
[[ "$FAIL" -gt 0 ]] && exit 1
|
|
191
|
+
exit 0
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Archive-ingest surface gate (Task 855; supersedes Task 846).
|
|
3
|
+
#
|
|
4
|
+
# Five enforcements, one script — phase decided by `hook_event_name` on stdin.
|
|
5
|
+
# Task 855 narrows the database-operator subagent's effective surface during
|
|
6
|
+
# WhatsApp archive ingestion to exactly one Bash entry
|
|
7
|
+
# (`whatsapp-import/bin/whatsapp-ingest.sh`) plus read-only neighbours, by
|
|
8
|
+
# blocking the legacy MCP deviation tools mechanically.
|
|
9
|
+
#
|
|
10
|
+
# 1. PreToolUse on the three legacy WhatsApp MCP tools — BLOCK unconditionally.
|
|
11
|
+
# The single deterministic Bash entry (Task 855) is the only supported path
|
|
12
|
+
# for `archiveType=whatsapp-export`. The legacy MCP tools' source remains
|
|
13
|
+
# until cleanup; the gate stops them being invoked.
|
|
14
|
+
# mcp__memory__whatsapp-export-parse
|
|
15
|
+
# mcp__memory__whatsapp-export-insight-write
|
|
16
|
+
# mcp__memory__memory-archive-write (only when `archiveType` is
|
|
17
|
+
# `whatsapp-export`; LinkedIn
|
|
18
|
+
# and other archiveTypes pass
|
|
19
|
+
# through unchanged.)
|
|
20
|
+
#
|
|
21
|
+
# 2. PreToolUse Edit/Write/NotebookEdit: deny writes under
|
|
22
|
+
# `*platform/plugins/*/lib/*` (parser/CSV-shape source for any *-import or
|
|
23
|
+
# *-export plugin). Preserved from Task 846 — defence in depth even though
|
|
24
|
+
# the database-operator template no longer lists Edit/Write tools.
|
|
25
|
+
#
|
|
26
|
+
# 3. PreToolUse Bash: deny commands invoking JavaScript test runners
|
|
27
|
+
# (vitest|bun test|npm test|npx jest|node .*vitest). Preserved from Task 846.
|
|
28
|
+
#
|
|
29
|
+
# 4. Parse-error gate: PostToolUse on any `mcp__*__*-export-parse` /
|
|
30
|
+
# `mcp__*__*-import-parse` tool whose `tool_response.isError == true`
|
|
31
|
+
# writes a flag file. Subsequent PreToolUse on ANY tool blocks until
|
|
32
|
+
# UserPromptSubmit clears the flag. A 600s TTL is the cross-session safety
|
|
33
|
+
# net. Preserved from Task 846 because LinkedIn and future per-source archive
|
|
34
|
+
# parsers still use the legacy MCP path until they migrate to their own
|
|
35
|
+
# deterministic Bash entries.
|
|
36
|
+
#
|
|
37
|
+
# 5. Logging: every PreToolUse decision emits one line in the format
|
|
38
|
+
# [archive-ingest-gate] decision=<allow|block> tool=<name> reason=<r> ...
|
|
39
|
+
# so the operator can grep the full decision trail for one ingest from
|
|
40
|
+
# server.log alongside the [whatsapp-ingest] script lines.
|
|
41
|
+
#
|
|
42
|
+
# Exit codes follow Claude Code hook protocol: 0 = allow, 2 = block (stderr
|
|
43
|
+
# message shown to the agent). Fail-closed on terminal stdin to match
|
|
44
|
+
# pre-tool-use.sh.
|
|
45
|
+
|
|
46
|
+
set -uo pipefail
|
|
47
|
+
|
|
48
|
+
# Read stdin — fail closed if attached to a terminal (no JSON envelope coming).
|
|
49
|
+
if [ -t 0 ]; then
|
|
50
|
+
echo "Blocked: archive-ingest-surface-gate received no stdin (cannot inspect tool call). Failing closed." >&2
|
|
51
|
+
exit 2
|
|
52
|
+
fi
|
|
53
|
+
INPUT=$(cat)
|
|
54
|
+
|
|
55
|
+
# ----- Resolve account dir for state file ----------------------------------
|
|
56
|
+
HOOK_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
|
|
57
|
+
PLATFORM_ROOT_RESOLVED="${HOOK_DIR}/../../.."
|
|
58
|
+
ACCOUNTS_DIR="${PLATFORM_ROOT_RESOLVED}/../data/accounts"
|
|
59
|
+
ACCOUNT_DIR=""
|
|
60
|
+
if [ -d "$ACCOUNTS_DIR" ]; then
|
|
61
|
+
ACCOUNT_DIR=$(find "$ACCOUNTS_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | head -1)
|
|
62
|
+
fi
|
|
63
|
+
STATE_DIR="${ARCHIVE_INGEST_GATE_STATE_DIR:-${ACCOUNT_DIR}/state}"
|
|
64
|
+
FLAG_FILE="${STATE_DIR}/archive-ingest-parse-error.flag"
|
|
65
|
+
TTL_SECONDS=600
|
|
66
|
+
|
|
67
|
+
# ----- Parse fields from stdin ---------------------------------------------
|
|
68
|
+
HOOK_EVENT=$(printf '%s' "$INPUT" | grep -o '"hook_event_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
|
|
69
|
+
TOOL_NAME=$(printf '%s' "$INPUT" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
|
|
70
|
+
|
|
71
|
+
# ============================================================================
|
|
72
|
+
# UserPromptSubmit — clear the parse-error flag.
|
|
73
|
+
# ============================================================================
|
|
74
|
+
if [ "$HOOK_EVENT" = "UserPromptSubmit" ]; then
|
|
75
|
+
if [ -f "$FLAG_FILE" ]; then
|
|
76
|
+
rm -f "$FLAG_FILE" 2>/dev/null
|
|
77
|
+
echo "[archive-ingest-gate] cleared reason=user-prompt-submit" >&2
|
|
78
|
+
fi
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# ============================================================================
|
|
83
|
+
# PostToolUse — record parse-error from any *-export-parse / *-import-parse.
|
|
84
|
+
# ============================================================================
|
|
85
|
+
if [ "$HOOK_EVENT" = "PostToolUse" ]; then
|
|
86
|
+
case "$TOOL_NAME" in
|
|
87
|
+
mcp__*__*-export-parse|mcp__*__*-import-parse)
|
|
88
|
+
if printf '%s' "$INPUT" | grep -Eq '"isError"[[:space:]]*:[[:space:]]*true'; then
|
|
89
|
+
mkdir -p "$STATE_DIR" 2>/dev/null
|
|
90
|
+
date -u +%s > "$FLAG_FILE" 2>/dev/null
|
|
91
|
+
echo "[archive-ingest-gate] surfaced reason=parse-error tool=${TOOL_NAME}" >&2
|
|
92
|
+
fi
|
|
93
|
+
;;
|
|
94
|
+
esac
|
|
95
|
+
exit 0
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# ============================================================================
|
|
99
|
+
# PreToolUse — five independent blocks.
|
|
100
|
+
# ============================================================================
|
|
101
|
+
if [ "$HOOK_EVENT" != "PreToolUse" ]; then
|
|
102
|
+
exit 0
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Helper: emit a single decision-log line + remediation message, then exit.
|
|
106
|
+
# Args: $1=decision (allow|block) $2=reason $3=remediation message
|
|
107
|
+
emit_decision() {
|
|
108
|
+
local decision="$1" reason="$2" remediation="$3" cmd="${4:-}"
|
|
109
|
+
local cmd_field=""
|
|
110
|
+
if [ -n "$cmd" ]; then
|
|
111
|
+
# Sanitise: strip control chars, take first 60 chars.
|
|
112
|
+
cmd_field=" command=\"$(printf '%s' "$cmd" | tr -d '\000-\037' | cut -c1-60)\""
|
|
113
|
+
fi
|
|
114
|
+
echo "[archive-ingest-gate] decision=${decision} tool=${TOOL_NAME} reason=${reason}${cmd_field}" >&2
|
|
115
|
+
if [ "$decision" = "block" ]; then
|
|
116
|
+
echo "$remediation" >&2
|
|
117
|
+
exit 2
|
|
118
|
+
fi
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# --- Block 1: post-parse-error gate (applies to ALL tools) -----------------
|
|
122
|
+
if [ -f "$FLAG_FILE" ]; then
|
|
123
|
+
FLAG_TS=$(cat "$FLAG_FILE" 2>/dev/null | head -1)
|
|
124
|
+
NOW=$(date -u +%s)
|
|
125
|
+
if [ -n "$FLAG_TS" ] && [ "$FLAG_TS" -gt 0 ] 2>/dev/null; then
|
|
126
|
+
AGE=$(( NOW - FLAG_TS ))
|
|
127
|
+
if [ "$AGE" -lt "$TTL_SECONDS" ]; then
|
|
128
|
+
emit_decision "block" "post-parse-error age_s=${AGE}" \
|
|
129
|
+
"Blocked: an archive-parser MCP tool returned isError=true earlier in this turn. The subagent's next action must be a user-facing message naming the parse-error and yielding back to the operator. Further tool calls are blocked until the operator submits a new prompt."
|
|
130
|
+
fi
|
|
131
|
+
rm -f "$FLAG_FILE" 2>/dev/null
|
|
132
|
+
else
|
|
133
|
+
rm -f "$FLAG_FILE" 2>/dev/null
|
|
134
|
+
fi
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# --- Block 2: legacy WhatsApp MCP tools — denied unconditionally ----------
|
|
138
|
+
case "$TOOL_NAME" in
|
|
139
|
+
mcp__memory__whatsapp-export-parse|mcp__memory__whatsapp-export-insight-write)
|
|
140
|
+
emit_decision "block" "denied-mcp-legacy" \
|
|
141
|
+
"Blocked: ${TOOL_NAME} is the Task 804 legacy path. Task 855 ships the deterministic Bash entry — invoke 'bash platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh <archive> --owner-element-id <id> --scope <admin|public>' once. Parse, archive-write, and insight all run in-process; do not call legacy MCP tools."
|
|
142
|
+
;;
|
|
143
|
+
esac
|
|
144
|
+
|
|
145
|
+
# Helper: extract a top-level field from `tool_input` via python3 — never via
|
|
146
|
+
# grep+sed against the raw JSON, which would pick the first textual occurrence
|
|
147
|
+
# including nested-object matches (`rows[0].archiveType`,
|
|
148
|
+
# `conversation.archiveType`, etc.) and let a malicious payload bypass the
|
|
149
|
+
# block. python3 is the project standard for JSON-aware hook parsing
|
|
150
|
+
# (mirrors lane-gate.sh:31-46). On parse failure return empty string —
|
|
151
|
+
# downstream block conditions skip cleanly.
|
|
152
|
+
extract_tool_input_field() {
|
|
153
|
+
local field="$1"
|
|
154
|
+
printf '%s' "$INPUT" | python3 -c "
|
|
155
|
+
import sys, json
|
|
156
|
+
try:
|
|
157
|
+
d = json.load(sys.stdin)
|
|
158
|
+
print(d.get('tool_input', {}).get('$field', ''))
|
|
159
|
+
except Exception:
|
|
160
|
+
print('')
|
|
161
|
+
" 2>/dev/null || echo ""
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# --- Block 3: memory-archive-write conditional on whatsapp-export -----------
|
|
165
|
+
# LinkedIn and future archiveTypes flow unchanged through memory-archive-write.
|
|
166
|
+
# Only `archiveType=whatsapp-export` is now restricted to the script path.
|
|
167
|
+
if [ "$TOOL_NAME" = "mcp__memory__memory-archive-write" ]; then
|
|
168
|
+
ARCHIVE_TYPE=$(extract_tool_input_field archiveType)
|
|
169
|
+
if [ "$ARCHIVE_TYPE" = "whatsapp-export" ]; then
|
|
170
|
+
emit_decision "block" "denied-mcp-legacy archiveType=whatsapp-export" \
|
|
171
|
+
"Blocked: memory-archive-write with archiveType='whatsapp-export' is the Task 804 legacy path. Task 855 ships the deterministic Bash entry — invoke 'bash platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh <archive> --owner-element-id <id> --scope <admin|public>' once. Other archiveTypes (linkedin-connections, …) flow through memory-archive-write unchanged."
|
|
172
|
+
fi
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# --- Block 4: plugin-source path block (Edit/Write/NotebookEdit) -----------
|
|
176
|
+
case "$TOOL_NAME" in
|
|
177
|
+
Edit|Write|NotebookEdit)
|
|
178
|
+
FILE_PATH=$(extract_tool_input_field file_path)
|
|
179
|
+
case "$FILE_PATH" in
|
|
180
|
+
*/platform/plugins/*/lib/*|platform/plugins/*/lib/*)
|
|
181
|
+
emit_decision "block" "plugin-source-edit path=${FILE_PATH}" \
|
|
182
|
+
"Blocked: ${TOOL_NAME} on ${FILE_PATH} is a platform plugin lib/ path. The database-operator subagent does not own plugin source; if a parser is broken, surface the parse-error to the operator and let them dispatch a code-edit task instead."
|
|
183
|
+
;;
|
|
184
|
+
esac
|
|
185
|
+
;;
|
|
186
|
+
esac
|
|
187
|
+
|
|
188
|
+
# --- Block 5: shell test-runner block (Bash) -------------------------------
|
|
189
|
+
COMMAND=""
|
|
190
|
+
if [ "$TOOL_NAME" = "Bash" ]; then
|
|
191
|
+
COMMAND=$(extract_tool_input_field command)
|
|
192
|
+
if printf '%s' "$COMMAND" | grep -Eq '(^|[[:space:]/])vitest($|[[:space:]])|(^|[[:space:]])bun[[:space:]]+test($|[[:space:]])|(^|[[:space:]])npm[[:space:]]+test($|[[:space:]])|(^|[[:space:]])npx[[:space:]]+jest($|[[:space:]])|node[[:space:]].*vitest'; then
|
|
193
|
+
emit_decision "block" "test-runner" \
|
|
194
|
+
"Blocked: Bash command invokes a JavaScript test runner (vitest/bun test/npm test/npx jest). The database-operator subagent does not run plugin tests; surface the parse-error to the operator." \
|
|
195
|
+
"$COMMAND"
|
|
196
|
+
fi
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
# Default — allow. Emit a single-line decision log so successful invocations
|
|
200
|
+
# leave a grep-able trail too. (Bash gets command field; other tools omit.)
|
|
201
|
+
if [ "$TOOL_NAME" = "Bash" ]; then
|
|
202
|
+
emit_decision "allow" "default" "" "$COMMAND"
|
|
203
|
+
else
|
|
204
|
+
emit_decision "allow" "default" ""
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
exit 0
|
|
@@ -131,6 +131,18 @@ ls /tmp/.X11-unix/
|
|
|
131
131
|
|
|
132
132
|
You should see `X99`. If not, start VNC before retrying Step 1.
|
|
133
133
|
|
|
134
|
+
### Three states the consent page can render
|
|
135
|
+
|
|
136
|
+
The dashboard at `dash.cloudflare.com/argotunnel?...` shows one of three states. The automation script's CDP helper (`_cdp-authorize.mjs`) detects each one; the same triage applies when you walk the flow manually.
|
|
137
|
+
|
|
138
|
+
1. **Pre-authorize — the Authorize button is visible.** This is the normal first run. Click the zone you want, then click **Authorize**. Cloudflare's callback fires and `~/.cloudflared/cert.pem` lands a second or two later. Continue with the `mv` above.
|
|
139
|
+
|
|
140
|
+
2. **Post-success — "Cloudflared has installed a certificate" modal.** The bound account already has a cert authorised for the zones the dashboard knows about. The OAuth callback is idempotent — navigating to a fresh `cloudflared tunnel login` URL still fires the callback, so `~/.cloudflared/cert.pem` will land. Wait a second or two, then run the `mv`.
|
|
141
|
+
|
|
142
|
+
If the cert *also* exists at `${CFG_DIR}/cert.pem` from a prior partial run, Step 1 is already complete — skip to Step 2.
|
|
143
|
+
|
|
144
|
+
3. **Blank or button-less — neither the Authorize button nor the success modal is on the page.** The browser may be mid-load, signed out of Cloudflare, blocking on captcha, or showing some other state. Sign in to Cloudflare in the same browser, navigate back to the URL `cloudflared tunnel login` printed, and complete the click manually. If the URL has expired, kill `cloudflared` (`Ctrl+C`) and re-run Step 1 to get a fresh URL.
|
|
145
|
+
|
|
134
146
|
---
|
|
135
147
|
|
|
136
148
|
## Step 2 — List existing tunnels
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Tri-state DOM matcher for the Cloudflare argotunnel consent page (Task 855).
|
|
2
|
+
//
|
|
3
|
+
// `dash.cloudflare.com/argotunnel?...` legitimately renders three observable
|
|
4
|
+
// states for our flow:
|
|
5
|
+
//
|
|
6
|
+
// 1. Pre-authorize — a <button> or <input type="submit"> whose trimmed text
|
|
7
|
+
// matches /^(authorize|connect)$/i (and is not disabled). Click it; the
|
|
8
|
+
// callback fires, cloudflared writes ~/.cloudflared/cert.pem.
|
|
9
|
+
//
|
|
10
|
+
// 2. Post-success — the dashboard renders a Success modal containing the
|
|
11
|
+
// stable substring "Cloudflared has installed a certificate". This shape
|
|
12
|
+
// is reached when the user already authorised this account before (or
|
|
13
|
+
// did so manually in VNC between cloudflared spawn and helper poll).
|
|
14
|
+
// The OAuth callback is idempotent on the cert side: when the helper
|
|
15
|
+
// sees this state, the running cloudflared subprocess will still write
|
|
16
|
+
// ~/.cloudflared/cert.pem because the callback URL is exercised by the
|
|
17
|
+
// navigation. The wrapper drops into the existing cert-poll afterwards.
|
|
18
|
+
//
|
|
19
|
+
// 3. Neither — the page is mid-load, blank, or in some other state. The
|
|
20
|
+
// caller polls again until BUTTON_POLL_TIMEOUT_MS, then exits 1.
|
|
21
|
+
//
|
|
22
|
+
// The matcher returns a discriminated union from a single DOM read:
|
|
23
|
+
//
|
|
24
|
+
// { kind: 'button', descriptor: {tag,text,disabled} } | { kind: 'success' } | null
|
|
25
|
+
//
|
|
26
|
+
// PRIORITY: button > success modal. Page transitions can briefly render both
|
|
27
|
+
// (e.g. during the success animation). If a clickable Authorize button exists,
|
|
28
|
+
// click it — the click produces a fresh callback regardless of any leftover
|
|
29
|
+
// modal text. Only when no button is present do we trust the modal as the
|
|
30
|
+
// terminal state.
|
|
31
|
+
//
|
|
32
|
+
// EXPORTED SHAPES:
|
|
33
|
+
//
|
|
34
|
+
// * findMatch(doc) — JS function form. Used by JSDOM-based unit tests
|
|
35
|
+
// (platform/ui/scripts/__tests__/cdp-authorize-matcher.test.ts) so future
|
|
36
|
+
// matcher edits replay against captured DOM fixtures offline (Success
|
|
37
|
+
// criterion 8 of Task 855).
|
|
38
|
+
//
|
|
39
|
+
// * MATCH_EXPR — string form, evaluated by Chrome DevTools Protocol's
|
|
40
|
+
// Runtime.evaluate in _cdp-authorize.mjs's polling loop. Built from
|
|
41
|
+
// findMatch.toString() so the live page and the tests run identical
|
|
42
|
+
// logic — single source of truth.
|
|
43
|
+
//
|
|
44
|
+
// CONTRACT — DO NOT loosen the success-modal anchor. Tighter anchors regress
|
|
45
|
+
// on Cloudflare copy edits; "Cloudflared has installed a certificate" is the
|
|
46
|
+
// load-bearing claim of the modal. Wider anchors (e.g. matching just
|
|
47
|
+
// "certificate") would false-positive on the pre-authorize page.
|
|
48
|
+
|
|
49
|
+
export function findMatch(doc) {
|
|
50
|
+
const candidates = Array.from(doc.querySelectorAll('button, input[type="submit"]'));
|
|
51
|
+
const match = candidates.find((el) => {
|
|
52
|
+
const text = (el.textContent ?? el.value ?? '').trim();
|
|
53
|
+
return /^(authorize|connect)$/i.test(text) && !el.disabled;
|
|
54
|
+
});
|
|
55
|
+
if (match) {
|
|
56
|
+
const descriptor = {
|
|
57
|
+
tag: match.tagName.toLowerCase(),
|
|
58
|
+
text: (match.textContent ?? match.value ?? '').trim().slice(0, 40),
|
|
59
|
+
disabled: Boolean(match.disabled),
|
|
60
|
+
};
|
|
61
|
+
match.click();
|
|
62
|
+
return { kind: 'button', descriptor };
|
|
63
|
+
}
|
|
64
|
+
// textContent over innerText: jsdom's innerText is partial; the dashboard's
|
|
65
|
+
// success-modal text is plain DOM content (not visibility-gated by CSS for
|
|
66
|
+
// anyone reading the page) so textContent finds it on both runtimes.
|
|
67
|
+
const bodyText = doc.body ? doc.body.textContent || '' : '';
|
|
68
|
+
if (bodyText.includes('Cloudflared has installed a certificate')) {
|
|
69
|
+
return { kind: 'success' };
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const MATCH_EXPR = `(${findMatch.toString()})(document)`;
|