@rubytech/create-maxy 1.0.800 → 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/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/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/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/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
|
|
@@ -40,7 +40,7 @@ These are enabled during onboarding and can be added or removed at any time. Som
|
|
|
40
40
|
| `waitlist` | Waitlist lifecycle — extract sign-ups from conversations, review | — |
|
|
41
41
|
| `replicate` | Image generation — three models for photorealistic, design, and fast draft images | Content producer, Research assistant |
|
|
42
42
|
| `linkedin-import` | Import a LinkedIn Basic Data Export — Profile and Connections today, more CSVs as references land | Database operator |
|
|
43
|
-
| `whatsapp-import` | Import a WhatsApp `_chat.txt` export as a Conversation + chronologically-chained Messages
|
|
43
|
+
| `whatsapp-import` | Import a WhatsApp `_chat.txt` export as a Conversation + chronologically-chained Messages plus `:Observation` nodes for typed insights (mentions, tasks, preferences, observed relationships). Single Bash entry — `bash platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh <archive> --owner-element-id <id> --scope <admin\|public>` runs parse → archive-write → Haiku insight in one process. Distinct from the live `whatsapp` plugin which is a Baileys QR-pairing channel. | Database operator |
|
|
44
44
|
|
|
45
45
|
### Claude Official (marketplace)
|
|
46
46
|
|
|
@@ -14,7 +14,7 @@ Ingests a WhatsApp "Export Chat" archive (the `_chat.txt` file plus media attach
|
|
|
14
14
|
|
|
15
15
|
## When this applies
|
|
16
16
|
|
|
17
|
-
The admin agent delegates to `database-operator` when the operator drops a `_chat.txt` (or its containing folder) into chat. The specialist runs the skill's archive-owner
|
|
17
|
+
The admin agent delegates to `database-operator` when the operator drops a `_chat.txt` (or its containing folder) into chat. The specialist runs the skill's archive-owner confirmation flow before any line is written, then invokes the deterministic Bash entry (`bin/whatsapp-ingest.sh`) once: parse, archive-write (via `memoryArchiveWrite` in-process), and Haiku insight all run in one Node process — no MCP envelope between steps (Task 855).
|
|
18
18
|
|
|
19
19
|
## Accepted export shapes
|
|
20
20
|
|
|
@@ -28,6 +28,6 @@ WhatsApp's "Export Chat" emits `[DD/MM/YYYY, HH:MM:SS]` prefixes by default in m
|
|
|
28
28
|
|
|
29
29
|
## Relationship to other plugins
|
|
30
30
|
|
|
31
|
-
- **memory** — the underlying write surface
|
|
31
|
+
- **memory** — the underlying write surface imported in-process by `bin/ingest.mjs` (`memoryArchiveWrite` for bulk Conversation+Messages; direct Cypher `:Observation` writes for the insight pass). All writes carry `source='whatsapp'` + `createdByAgent='whatsapp-import'` provenance. The legacy `mcp__memory__whatsapp-export-parse` / `whatsapp-export-insight-write` MCP tools and the direct `memory-archive-write` MCP path with `archiveType=whatsapp-export` are blocked at the harness — the Bash entry is the only supported invocation surface (Task 855).
|
|
32
32
|
- **database-operator specialist** — owns execution. See [admin/IDENTITY.md](../../../platform/templates/agents/admin/IDENTITY.md) delegation clause and [database-operator.md](../../../platform/templates/specialists/agents/database-operator.md) per-source archive list.
|
|
33
33
|
- **linkedin-import** — sister plugin under the same pattern (LinkedIn Basic Data Export). Reading [linkedin-import/PLUGIN.md](../linkedin-import/PLUGIN.md) is the fastest way to understand the shape this plugin follows.
|