@rubytech/create-maxy 1.0.472 → 1.0.474
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/PLUGIN.md +2 -2
- package/payload/platform/plugins/admin/skills/public-agent-manager/skill.md +3 -3
- package/payload/platform/plugins/docs/references/migration-guide.md +29 -21
- package/payload/platform/plugins/memory/PLUGIN.md +6 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +58 -22
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.d.ts +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js +35 -23
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts +5 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +40 -7
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/scripts/migrate-import.sh +17 -11
- package/payload/platform/scripts/seed-neo4j.sh +15 -61
- package/payload/platform/templates/account.json +1 -1
- package/payload/server/public/assets/ChatInput-BZayQkAS.css +1 -0
- package/payload/server/public/assets/{admin-B1CMS86q.js → admin-CW4uB1GJ.js} +2 -2
- package/payload/server/public/assets/public-DEZaIlO-.js +5 -0
- package/payload/server/public/index.html +3 -3
- package/payload/server/public/public.html +3 -3
- package/payload/server/server.js +46 -102
- package/payload/platform/plugins/admin/hooks/agent-creation-approval.sh +0 -161
- package/payload/platform/plugins/admin/hooks/agent-creation-gate.sh +0 -317
- package/payload/platform/plugins/admin/hooks/agent-creation-post.sh +0 -165
- package/payload/platform/plugins/admin/hooks/session-start.sh +0 -104
- package/payload/platform/plugins/admin/hooks/test-agent-creation-gate.sh +0 -926
- package/payload/server/public/assets/ChatInput-BEwQxFL9.css +0 -1
- package/payload/server/public/assets/public-ojODTkXT.js +0 -5
- /package/payload/server/public/assets/{ChatInput-Dnp1FLis.js → ChatInput-DgjefIvo.js} +0 -0
|
@@ -1,926 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# End-to-end test for the agent creation gate lifecycle.
|
|
3
|
-
#
|
|
4
|
-
# Validates that the hook scripts correctly:
|
|
5
|
-
# - Block writes to agent files without a state file
|
|
6
|
-
# - Block Bash writes to agent files without a state file
|
|
7
|
-
# - Exempt admin agent files
|
|
8
|
-
# - Create state on mcp__admin__plugin-read for public-agent-manager
|
|
9
|
-
# - Do NOT create state for other plugin-read calls
|
|
10
|
-
# - Block writes when gates are pending
|
|
11
|
-
# - Advance gates on user approval (_componentDone via UserPromptSubmit)
|
|
12
|
-
# - Allow writes after gates pass
|
|
13
|
-
# - Allow Bash writes after gates pass
|
|
14
|
-
# - Allow all gated writes when all gates pass
|
|
15
|
-
# - Clean up state immediately when all files exist on disk (PostToolUse)
|
|
16
|
-
# - Recover all-gates-true state on session start when files not yet written
|
|
17
|
-
# - Detect and clean up truly completed state on session start (all gates true + files on disk)
|
|
18
|
-
# - Ignore non-document-editor/form components
|
|
19
|
-
# - Ignore _componentDone for admin agent
|
|
20
|
-
# - Handle gate advancement idempotently
|
|
21
|
-
# - render-component does NOT advance gates (removed in Task 148)
|
|
22
|
-
#
|
|
23
|
-
# Usage: bash test-agent-creation-gate.sh
|
|
24
|
-
# Exit 0 = all tests pass, exit 1 = failure
|
|
25
|
-
#
|
|
26
|
-
# No Claude Code runtime needed — tests the hook scripts directly
|
|
27
|
-
# by piping JSON input and checking exit codes / state files.
|
|
28
|
-
|
|
29
|
-
set -euo pipefail
|
|
30
|
-
|
|
31
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
32
|
-
TMPDIR_ROOT=$(mktemp -d)
|
|
33
|
-
STATE_FILE="$TMPDIR_ROOT/state.json"
|
|
34
|
-
ACCOUNT_DIR="$TMPDIR_ROOT/account"
|
|
35
|
-
GATE_LOG="$TMPDIR_ROOT/gate.log"
|
|
36
|
-
|
|
37
|
-
# Create mock account structure
|
|
38
|
-
mkdir -p "$ACCOUNT_DIR/agents/test-agent"
|
|
39
|
-
mkdir -p "$ACCOUNT_DIR/agents/admin"
|
|
40
|
-
mkdir -p "$ACCOUNT_DIR/.claude"
|
|
41
|
-
|
|
42
|
-
export AGENT_CREATE_STATE_FILE="$STATE_FILE"
|
|
43
|
-
export ACCOUNT_DIR="$ACCOUNT_DIR"
|
|
44
|
-
export GATE_LOG_FILE="$GATE_LOG"
|
|
45
|
-
|
|
46
|
-
PASS=0
|
|
47
|
-
FAIL=0
|
|
48
|
-
|
|
49
|
-
pass() {
|
|
50
|
-
PASS=$((PASS + 1))
|
|
51
|
-
echo " PASS: $1"
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
fail() {
|
|
55
|
-
FAIL=$((FAIL + 1))
|
|
56
|
-
echo " FAIL: $1" >&2
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
cleanup() {
|
|
60
|
-
rm -rf "$TMPDIR_ROOT"
|
|
61
|
-
}
|
|
62
|
-
trap cleanup EXIT
|
|
63
|
-
|
|
64
|
-
echo "=== Agent Creation Gate — Lifecycle Test ==="
|
|
65
|
-
echo ""
|
|
66
|
-
|
|
67
|
-
# ---------------------------------------------------------------------------
|
|
68
|
-
# Test 1: Write blocked without state file
|
|
69
|
-
# ---------------------------------------------------------------------------
|
|
70
|
-
echo "Test 1: Write blocked without state file"
|
|
71
|
-
rm -f "$STATE_FILE"
|
|
72
|
-
|
|
73
|
-
EXIT_CODE=0
|
|
74
|
-
echo '{"tool_name":"Write","tool_input":{"file_path":"'"$ACCOUNT_DIR"'/agents/test-agent/SOUL.md","content":"test"}}' \
|
|
75
|
-
| bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
76
|
-
|
|
77
|
-
if [ "$EXIT_CODE" -eq 2 ]; then
|
|
78
|
-
pass "Write to agents/test-agent/SOUL.md blocked (exit 2)"
|
|
79
|
-
else
|
|
80
|
-
fail "Write to agents/test-agent/SOUL.md should exit 2, got $EXIT_CODE"
|
|
81
|
-
fi
|
|
82
|
-
|
|
83
|
-
# ---------------------------------------------------------------------------
|
|
84
|
-
# Test 2: Bash blocked without state file
|
|
85
|
-
# ---------------------------------------------------------------------------
|
|
86
|
-
echo "Test 2: Bash blocked without state file"
|
|
87
|
-
rm -f "$STATE_FILE"
|
|
88
|
-
|
|
89
|
-
EXIT_CODE=0
|
|
90
|
-
echo '{"tool_name":"Bash","tool_input":{"command":"cat > '"$ACCOUNT_DIR"'/agents/test-agent/SOUL.md << EOF\ntest\nEOF"}}' \
|
|
91
|
-
| bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
92
|
-
|
|
93
|
-
if [ "$EXIT_CODE" -eq 2 ]; then
|
|
94
|
-
pass "Bash write to agents/test-agent/SOUL.md blocked (exit 2)"
|
|
95
|
-
else
|
|
96
|
-
fail "Bash write to agents/test-agent/SOUL.md should exit 2, got $EXIT_CODE"
|
|
97
|
-
fi
|
|
98
|
-
|
|
99
|
-
# ---------------------------------------------------------------------------
|
|
100
|
-
# Test 3: Admin agent exempt
|
|
101
|
-
# ---------------------------------------------------------------------------
|
|
102
|
-
echo "Test 3: Admin agent exempt"
|
|
103
|
-
rm -f "$STATE_FILE"
|
|
104
|
-
|
|
105
|
-
EXIT_CODE=0
|
|
106
|
-
echo '{"tool_name":"Write","tool_input":{"file_path":"'"$ACCOUNT_DIR"'/agents/admin/SOUL.md","content":"test"}}' \
|
|
107
|
-
| bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
108
|
-
|
|
109
|
-
if [ "$EXIT_CODE" -eq 0 ]; then
|
|
110
|
-
pass "Write to agents/admin/SOUL.md allowed (exit 0)"
|
|
111
|
-
else
|
|
112
|
-
fail "Write to agents/admin/SOUL.md should exit 0, got $EXIT_CODE"
|
|
113
|
-
fi
|
|
114
|
-
|
|
115
|
-
# ---------------------------------------------------------------------------
|
|
116
|
-
# Test 4: Non-PAM plugin-read does NOT create state
|
|
117
|
-
# ---------------------------------------------------------------------------
|
|
118
|
-
echo "Test 4: Non-PAM plugin-read does NOT create state"
|
|
119
|
-
rm -f "$STATE_FILE"
|
|
120
|
-
|
|
121
|
-
echo '{"tool_name":"mcp__admin__plugin-read","tool_input":{"pluginName":"admin","file":"skills/skill-builder/SKILL.md"}}' \
|
|
122
|
-
| bash "$SCRIPT_DIR/agent-creation-post.sh" 2>/dev/null || true
|
|
123
|
-
|
|
124
|
-
if [ ! -f "$STATE_FILE" ]; then
|
|
125
|
-
pass "plugin-read for skill-builder did not create state file"
|
|
126
|
-
else
|
|
127
|
-
fail "plugin-read for skill-builder should NOT create state file"
|
|
128
|
-
fi
|
|
129
|
-
|
|
130
|
-
# ---------------------------------------------------------------------------
|
|
131
|
-
# Test 5: State file creation on plugin-read for public-agent-manager
|
|
132
|
-
# ---------------------------------------------------------------------------
|
|
133
|
-
echo "Test 5: State file creation on plugin-read for public-agent-manager"
|
|
134
|
-
rm -f "$STATE_FILE"
|
|
135
|
-
|
|
136
|
-
echo '{"tool_name":"mcp__admin__plugin-read","tool_input":{"pluginName":"admin","file":"skills/public-agent-manager/skill.md"}}' \
|
|
137
|
-
| bash "$SCRIPT_DIR/agent-creation-post.sh" 2>/dev/null || true
|
|
138
|
-
|
|
139
|
-
if [ -f "$STATE_FILE" ]; then
|
|
140
|
-
# Verify all gates are false
|
|
141
|
-
SOUL_GATE=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['gates']['soul'])" 2>/dev/null)
|
|
142
|
-
KNOWLEDGE_GATE=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['gates']['knowledge'])" 2>/dev/null)
|
|
143
|
-
CONFIG_GATE=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['gates']['config'])" 2>/dev/null)
|
|
144
|
-
|
|
145
|
-
if [ "$SOUL_GATE" = "False" ] && [ "$KNOWLEDGE_GATE" = "False" ] && [ "$CONFIG_GATE" = "False" ]; then
|
|
146
|
-
pass "State file created with all gates false"
|
|
147
|
-
else
|
|
148
|
-
fail "State file gates should all be False, got soul=$SOUL_GATE knowledge=$KNOWLEDGE_GATE config=$CONFIG_GATE"
|
|
149
|
-
fi
|
|
150
|
-
else
|
|
151
|
-
fail "State file not created after plugin-read for public-agent-manager"
|
|
152
|
-
fi
|
|
153
|
-
|
|
154
|
-
# ---------------------------------------------------------------------------
|
|
155
|
-
# Test 6: Write blocked with pending gate
|
|
156
|
-
# ---------------------------------------------------------------------------
|
|
157
|
-
echo "Test 6: Write blocked with pending gate (soul=false)"
|
|
158
|
-
|
|
159
|
-
EXIT_CODE=0
|
|
160
|
-
echo '{"tool_name":"Write","tool_input":{"file_path":"'"$ACCOUNT_DIR"'/agents/test-agent/SOUL.md","content":"test"}}' \
|
|
161
|
-
| bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
162
|
-
|
|
163
|
-
if [ "$EXIT_CODE" -eq 2 ]; then
|
|
164
|
-
pass "Write blocked when soul gate is false (exit 2)"
|
|
165
|
-
else
|
|
166
|
-
fail "Write should be blocked when soul gate is false, got exit $EXIT_CODE"
|
|
167
|
-
fi
|
|
168
|
-
|
|
169
|
-
# ---------------------------------------------------------------------------
|
|
170
|
-
# Test 7: Gate advancement — soul (user approval via _componentDone)
|
|
171
|
-
# ---------------------------------------------------------------------------
|
|
172
|
-
echo "Test 7: Gate advancement on user approval (soul)"
|
|
173
|
-
|
|
174
|
-
python3 -c "
|
|
175
|
-
import json
|
|
176
|
-
payload = json.dumps({'action': 'approve', 'content': 'personality content', 'filePath': '$ACCOUNT_DIR/agents/test-agent/SOUL.md'})
|
|
177
|
-
wrapper = json.dumps({'_componentDone': True, 'component': 'document-editor', 'payload': payload})
|
|
178
|
-
print(json.dumps({'prompt': wrapper, 'session_id': 'test', 'hook_event_name': 'UserPromptSubmit'}))
|
|
179
|
-
" | bash "$SCRIPT_DIR/agent-creation-approval.sh" 2>/dev/null || true
|
|
180
|
-
|
|
181
|
-
SOUL_GATE=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['gates']['soul'])" 2>/dev/null)
|
|
182
|
-
if [ "$SOUL_GATE" = "True" ]; then
|
|
183
|
-
pass "Soul gate advanced to true"
|
|
184
|
-
else
|
|
185
|
-
fail "Soul gate should be True after user approval, got $SOUL_GATE"
|
|
186
|
-
fi
|
|
187
|
-
|
|
188
|
-
# ---------------------------------------------------------------------------
|
|
189
|
-
# Test 8: Write allowed after gate passes
|
|
190
|
-
# ---------------------------------------------------------------------------
|
|
191
|
-
echo "Test 8: Write allowed after soul gate passes"
|
|
192
|
-
|
|
193
|
-
EXIT_CODE=0
|
|
194
|
-
echo '{"tool_name":"Write","tool_input":{"file_path":"'"$ACCOUNT_DIR"'/agents/test-agent/SOUL.md","content":"test"}}' \
|
|
195
|
-
| bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
196
|
-
|
|
197
|
-
if [ "$EXIT_CODE" -eq 0 ]; then
|
|
198
|
-
pass "Write to SOUL.md allowed after soul gate passes (exit 0)"
|
|
199
|
-
else
|
|
200
|
-
fail "Write to SOUL.md should be allowed after soul gate passes, got exit $EXIT_CODE"
|
|
201
|
-
fi
|
|
202
|
-
|
|
203
|
-
# ---------------------------------------------------------------------------
|
|
204
|
-
# Test 9: Bash allowed after gate passes
|
|
205
|
-
# ---------------------------------------------------------------------------
|
|
206
|
-
echo "Test 9: Bash write allowed after soul gate passes"
|
|
207
|
-
|
|
208
|
-
EXIT_CODE=0
|
|
209
|
-
echo '{"tool_name":"Bash","tool_input":{"command":"cat > '"$ACCOUNT_DIR"'/agents/test-agent/SOUL.md << EOF\ntest\nEOF"}}' \
|
|
210
|
-
| bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
211
|
-
|
|
212
|
-
if [ "$EXIT_CODE" -eq 0 ]; then
|
|
213
|
-
pass "Bash write to SOUL.md allowed after soul gate passes (exit 0)"
|
|
214
|
-
else
|
|
215
|
-
fail "Bash write to SOUL.md should be allowed after soul gate passes, got exit $EXIT_CODE"
|
|
216
|
-
fi
|
|
217
|
-
|
|
218
|
-
# ---------------------------------------------------------------------------
|
|
219
|
-
# Test 10: All gates advanced — multiple writes succeed
|
|
220
|
-
# ---------------------------------------------------------------------------
|
|
221
|
-
echo "Test 10: All gates advanced, multiple gated writes succeed"
|
|
222
|
-
|
|
223
|
-
# Advance knowledge gate (user approval)
|
|
224
|
-
python3 -c "
|
|
225
|
-
import json
|
|
226
|
-
payload = json.dumps({'action': 'approve', 'content': 'knowledge content', 'filePath': '$ACCOUNT_DIR/agents/test-agent/KNOWLEDGE.md'})
|
|
227
|
-
wrapper = json.dumps({'_componentDone': True, 'component': 'document-editor', 'payload': payload})
|
|
228
|
-
print(json.dumps({'prompt': wrapper, 'session_id': 'test', 'hook_event_name': 'UserPromptSubmit'}))
|
|
229
|
-
" | bash "$SCRIPT_DIR/agent-creation-approval.sh" 2>/dev/null || true
|
|
230
|
-
|
|
231
|
-
# Advance config gate (form submission)
|
|
232
|
-
python3 -c "
|
|
233
|
-
import json
|
|
234
|
-
payload = 'Form submitted: {\"liveMemory\": true, \"model\": \"claude-haiku-4-5-20251001\"}'
|
|
235
|
-
wrapper = json.dumps({'_componentDone': True, 'component': 'form', 'payload': payload})
|
|
236
|
-
print(json.dumps({'prompt': wrapper, 'session_id': 'test', 'hook_event_name': 'UserPromptSubmit'}))
|
|
237
|
-
" | bash "$SCRIPT_DIR/agent-creation-approval.sh" 2>/dev/null || true
|
|
238
|
-
|
|
239
|
-
# All gates should be true — state file persists (6h TTL handles cleanup)
|
|
240
|
-
if [ -f "$STATE_FILE" ]; then
|
|
241
|
-
ALL_TRUE=$(python3 -c "import json; g=json.load(open('$STATE_FILE'))['gates']; print(all(g.values()))" 2>/dev/null)
|
|
242
|
-
if [ "$ALL_TRUE" = "True" ]; then
|
|
243
|
-
# Write all three gated files — each should succeed
|
|
244
|
-
ALL_WRITES_OK=true
|
|
245
|
-
for FILE in SOUL.md KNOWLEDGE.md config.json; do
|
|
246
|
-
EXIT_CODE=0
|
|
247
|
-
echo '{"tool_name":"Write","tool_input":{"file_path":"'"$ACCOUNT_DIR"'/agents/test-agent/'"$FILE"'","content":"test"}}' \
|
|
248
|
-
| bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
249
|
-
if [ "$EXIT_CODE" -ne 0 ]; then
|
|
250
|
-
ALL_WRITES_OK=false
|
|
251
|
-
fail "Write to $FILE blocked after all gates true (exit $EXIT_CODE)"
|
|
252
|
-
fi
|
|
253
|
-
done
|
|
254
|
-
if [ "$ALL_WRITES_OK" = true ]; then
|
|
255
|
-
pass "All three gated files writable when all gates true"
|
|
256
|
-
fi
|
|
257
|
-
else
|
|
258
|
-
fail "All gates should be true after three approvals"
|
|
259
|
-
fi
|
|
260
|
-
else
|
|
261
|
-
fail "State file should still exist after approval hook"
|
|
262
|
-
fi
|
|
263
|
-
|
|
264
|
-
# ---------------------------------------------------------------------------
|
|
265
|
-
# Test 11: Non-agent file writes always allowed
|
|
266
|
-
# ---------------------------------------------------------------------------
|
|
267
|
-
echo "Test 11: Non-agent file writes always allowed"
|
|
268
|
-
rm -f "$STATE_FILE"
|
|
269
|
-
|
|
270
|
-
EXIT_CODE=0
|
|
271
|
-
echo '{"tool_name":"Write","tool_input":{"file_path":"'"$ACCOUNT_DIR"'/some-other-file.md","content":"test"}}' \
|
|
272
|
-
| bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
273
|
-
|
|
274
|
-
if [ "$EXIT_CODE" -eq 0 ]; then
|
|
275
|
-
pass "Write to non-agent file allowed (exit 0)"
|
|
276
|
-
else
|
|
277
|
-
fail "Write to non-agent file should exit 0, got $EXIT_CODE"
|
|
278
|
-
fi
|
|
279
|
-
|
|
280
|
-
# ===========================================================================
|
|
281
|
-
# Hardening Tests
|
|
282
|
-
# ===========================================================================
|
|
283
|
-
|
|
284
|
-
# ---------------------------------------------------------------------------
|
|
285
|
-
# Test 12: Stale state rejected (started > 6 hours ago)
|
|
286
|
-
# ---------------------------------------------------------------------------
|
|
287
|
-
echo "Test 12: Stale state rejected (started > 6 hours ago)"
|
|
288
|
-
rm -f "$STATE_FILE"
|
|
289
|
-
|
|
290
|
-
python3 -c "
|
|
291
|
-
import json
|
|
292
|
-
from datetime import datetime, timezone, timedelta
|
|
293
|
-
state = {
|
|
294
|
-
'slug': 'test-agent',
|
|
295
|
-
'started': (datetime.now(timezone.utc) - timedelta(hours=7)).isoformat(),
|
|
296
|
-
'gates': {'soul': False, 'knowledge': False, 'config': False}
|
|
297
|
-
}
|
|
298
|
-
with open('$STATE_FILE', 'w') as f:
|
|
299
|
-
json.dump(state, f)
|
|
300
|
-
"
|
|
301
|
-
|
|
302
|
-
EXIT_CODE=0
|
|
303
|
-
echo '{"tool_name":"Write","tool_input":{"file_path":"'"$ACCOUNT_DIR"'/agents/test-agent/SOUL.md","content":"test"}}' \
|
|
304
|
-
| bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
305
|
-
|
|
306
|
-
if [ "$EXIT_CODE" -eq 2 ]; then
|
|
307
|
-
if [ ! -f "$STATE_FILE" ]; then
|
|
308
|
-
pass "Stale state rejected (exit 2) and state file deleted"
|
|
309
|
-
else
|
|
310
|
-
fail "Stale state rejected (exit 2) but state file not deleted"
|
|
311
|
-
fi
|
|
312
|
-
else
|
|
313
|
-
fail "Stale state should exit 2, got $EXIT_CODE"
|
|
314
|
-
fi
|
|
315
|
-
|
|
316
|
-
# ---------------------------------------------------------------------------
|
|
317
|
-
# Test 13: Malformed state rejected (missing gate keys)
|
|
318
|
-
# ---------------------------------------------------------------------------
|
|
319
|
-
echo "Test 13: Malformed state rejected (missing gate keys)"
|
|
320
|
-
rm -f "$STATE_FILE"
|
|
321
|
-
|
|
322
|
-
python3 -c "
|
|
323
|
-
import json
|
|
324
|
-
from datetime import datetime, timezone
|
|
325
|
-
state = {
|
|
326
|
-
'slug': 'test-agent',
|
|
327
|
-
'started': datetime.now(timezone.utc).isoformat(),
|
|
328
|
-
'gates': {'soul': True}
|
|
329
|
-
}
|
|
330
|
-
with open('$STATE_FILE', 'w') as f:
|
|
331
|
-
json.dump(state, f)
|
|
332
|
-
"
|
|
333
|
-
|
|
334
|
-
EXIT_CODE=0
|
|
335
|
-
echo '{"tool_name":"Write","tool_input":{"file_path":"'"$ACCOUNT_DIR"'/agents/test-agent/SOUL.md","content":"test"}}' \
|
|
336
|
-
| bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
337
|
-
|
|
338
|
-
if [ "$EXIT_CODE" -eq 2 ]; then
|
|
339
|
-
if [ ! -f "$STATE_FILE" ]; then
|
|
340
|
-
pass "Malformed state rejected (exit 2) and state file deleted"
|
|
341
|
-
else
|
|
342
|
-
fail "Malformed state rejected (exit 2) but state file not deleted"
|
|
343
|
-
fi
|
|
344
|
-
else
|
|
345
|
-
fail "Malformed state should exit 2, got $EXIT_CODE"
|
|
346
|
-
fi
|
|
347
|
-
|
|
348
|
-
# ---------------------------------------------------------------------------
|
|
349
|
-
# Test 14: Legacy state rejected (no 'started' field)
|
|
350
|
-
# ---------------------------------------------------------------------------
|
|
351
|
-
echo "Test 14: Legacy state rejected (no 'started' field)"
|
|
352
|
-
rm -f "$STATE_FILE"
|
|
353
|
-
|
|
354
|
-
echo '{"gates":{"soul":true,"knowledge":true,"config":true}}' > "$STATE_FILE"
|
|
355
|
-
|
|
356
|
-
EXIT_CODE=0
|
|
357
|
-
echo '{"tool_name":"Write","tool_input":{"file_path":"'"$ACCOUNT_DIR"'/agents/test-agent/SOUL.md","content":"test"}}' \
|
|
358
|
-
| bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
359
|
-
|
|
360
|
-
if [ "$EXIT_CODE" -eq 2 ]; then
|
|
361
|
-
if [ ! -f "$STATE_FILE" ]; then
|
|
362
|
-
pass "Legacy state (no started) rejected (exit 2) and state file deleted"
|
|
363
|
-
else
|
|
364
|
-
fail "Legacy state rejected (exit 2) but state file not deleted"
|
|
365
|
-
fi
|
|
366
|
-
else
|
|
367
|
-
fail "Legacy state should exit 2, got $EXIT_CODE"
|
|
368
|
-
fi
|
|
369
|
-
|
|
370
|
-
# ---------------------------------------------------------------------------
|
|
371
|
-
# Test 15: Bash blocked from writing state file
|
|
372
|
-
# ---------------------------------------------------------------------------
|
|
373
|
-
echo "Test 15: Bash blocked from writing state file"
|
|
374
|
-
rm -f "$STATE_FILE"
|
|
375
|
-
|
|
376
|
-
EXIT_CODE=0
|
|
377
|
-
echo '{"tool_name":"Bash","tool_input":{"command":"echo '\''{}'\'' > /tmp/maxy-agent-create-state.json"}}' \
|
|
378
|
-
| bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
379
|
-
|
|
380
|
-
if [ "$EXIT_CODE" -eq 2 ]; then
|
|
381
|
-
pass "Bash write to state file blocked (exit 2)"
|
|
382
|
-
else
|
|
383
|
-
fail "Bash write to state file should exit 2, got $EXIT_CODE"
|
|
384
|
-
fi
|
|
385
|
-
|
|
386
|
-
# ---------------------------------------------------------------------------
|
|
387
|
-
# Test 16: Bash blocked from deleting state file
|
|
388
|
-
# ---------------------------------------------------------------------------
|
|
389
|
-
echo "Test 16: Bash blocked from deleting state file"
|
|
390
|
-
rm -f "$STATE_FILE"
|
|
391
|
-
|
|
392
|
-
EXIT_CODE=0
|
|
393
|
-
echo '{"tool_name":"Bash","tool_input":{"command":"rm /tmp/maxy-agent-create-state.json"}}' \
|
|
394
|
-
| bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
395
|
-
|
|
396
|
-
if [ "$EXIT_CODE" -eq 2 ]; then
|
|
397
|
-
pass "Bash delete of state file blocked (exit 2)"
|
|
398
|
-
else
|
|
399
|
-
fail "Bash delete of state file should exit 2, got $EXIT_CODE"
|
|
400
|
-
fi
|
|
401
|
-
|
|
402
|
-
# ---------------------------------------------------------------------------
|
|
403
|
-
# Test 17: All-gates-true — state file persists, all writes allowed
|
|
404
|
-
# ---------------------------------------------------------------------------
|
|
405
|
-
echo "Test 17: All-gates-true state file persists across multiple writes"
|
|
406
|
-
rm -f "$STATE_FILE"
|
|
407
|
-
|
|
408
|
-
python3 -c "
|
|
409
|
-
import json
|
|
410
|
-
from datetime import datetime, timezone
|
|
411
|
-
state = {
|
|
412
|
-
'slug': 'test-agent',
|
|
413
|
-
'started': datetime.now(timezone.utc).isoformat(),
|
|
414
|
-
'gates': {'soul': True, 'knowledge': True, 'config': True}
|
|
415
|
-
}
|
|
416
|
-
with open('$STATE_FILE', 'w') as f:
|
|
417
|
-
json.dump(state, f)
|
|
418
|
-
"
|
|
419
|
-
|
|
420
|
-
# Write SOUL.md — should succeed and state file should persist
|
|
421
|
-
EXIT_CODE=0
|
|
422
|
-
echo '{"tool_name":"Write","tool_input":{"file_path":"'"$ACCOUNT_DIR"'/agents/test-agent/SOUL.md","content":"test"}}' \
|
|
423
|
-
| bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
424
|
-
|
|
425
|
-
if [ "$EXIT_CODE" -eq 0 ] && [ -f "$STATE_FILE" ]; then
|
|
426
|
-
# Write config.json — should also succeed (regression: previously blocked)
|
|
427
|
-
EXIT_CODE=0
|
|
428
|
-
echo '{"tool_name":"Write","tool_input":{"file_path":"'"$ACCOUNT_DIR"'/agents/test-agent/config.json","content":"test"}}' \
|
|
429
|
-
| bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
430
|
-
if [ "$EXIT_CODE" -eq 0 ]; then
|
|
431
|
-
pass "All-gates-true: multiple writes allowed, state file persists (6h TTL cleanup)"
|
|
432
|
-
else
|
|
433
|
-
fail "All-gates-true: second write blocked (exit $EXIT_CODE) — state file deleted prematurely"
|
|
434
|
-
fi
|
|
435
|
-
else
|
|
436
|
-
fail "All-gates-true: first write should exit 0 with state file persisting, got exit=$EXIT_CODE file_exists=$([ -f '$STATE_FILE' ] && echo yes || echo no)"
|
|
437
|
-
fi
|
|
438
|
-
|
|
439
|
-
# ---------------------------------------------------------------------------
|
|
440
|
-
# Test 18: Session-start staleness check (stale durable not recovered)
|
|
441
|
-
# ---------------------------------------------------------------------------
|
|
442
|
-
echo "Test 18: Session-start staleness check (stale durable not recovered)"
|
|
443
|
-
rm -f "$STATE_FILE"
|
|
444
|
-
DURABLE_FILE="$ACCOUNT_DIR/.claude/agent-create-state.json"
|
|
445
|
-
|
|
446
|
-
python3 -c "
|
|
447
|
-
import json
|
|
448
|
-
from datetime import datetime, timezone, timedelta
|
|
449
|
-
state = {
|
|
450
|
-
'slug': 'test-agent',
|
|
451
|
-
'started': (datetime.now(timezone.utc) - timedelta(hours=7)).isoformat(),
|
|
452
|
-
'gates': {'soul': True, 'knowledge': False, 'config': False}
|
|
453
|
-
}
|
|
454
|
-
with open('$DURABLE_FILE', 'w') as f:
|
|
455
|
-
json.dump(state, f)
|
|
456
|
-
"
|
|
457
|
-
|
|
458
|
-
bash "$SCRIPT_DIR/session-start.sh" "$ACCOUNT_DIR" admin 2>/dev/null || true
|
|
459
|
-
|
|
460
|
-
if [ ! -f "$STATE_FILE" ]; then
|
|
461
|
-
if [ ! -f "$DURABLE_FILE" ]; then
|
|
462
|
-
pass "Stale durable state not recovered, both files deleted"
|
|
463
|
-
else
|
|
464
|
-
fail "Stale durable state not recovered but durable file not deleted"
|
|
465
|
-
fi
|
|
466
|
-
else
|
|
467
|
-
fail "Stale durable state should NOT be recovered to volatile"
|
|
468
|
-
fi
|
|
469
|
-
|
|
470
|
-
# ---------------------------------------------------------------------------
|
|
471
|
-
# Test 19: Write tool blocked from targeting state file
|
|
472
|
-
# ---------------------------------------------------------------------------
|
|
473
|
-
echo "Test 19: Write tool blocked from targeting state file"
|
|
474
|
-
rm -f "$STATE_FILE"
|
|
475
|
-
|
|
476
|
-
EXIT_CODE=0
|
|
477
|
-
echo '{"tool_name":"Write","tool_input":{"file_path":"/tmp/maxy-agent-create-state.json","content":"{\"gates\":{\"soul\":true,\"knowledge\":true,\"config\":true}}"}}' \
|
|
478
|
-
| bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
479
|
-
|
|
480
|
-
if [ "$EXIT_CODE" -eq 2 ]; then
|
|
481
|
-
pass "Write to state file blocked (exit 2)"
|
|
482
|
-
else
|
|
483
|
-
fail "Write to state file should exit 2, got $EXIT_CODE"
|
|
484
|
-
fi
|
|
485
|
-
|
|
486
|
-
# ---------------------------------------------------------------------------
|
|
487
|
-
# Test 20: Edit tool blocked from targeting durable state file
|
|
488
|
-
# ---------------------------------------------------------------------------
|
|
489
|
-
echo "Test 20: Edit tool blocked from targeting durable state file"
|
|
490
|
-
rm -f "$STATE_FILE"
|
|
491
|
-
|
|
492
|
-
EXIT_CODE=0
|
|
493
|
-
echo '{"tool_name":"Edit","tool_input":{"file_path":"'"$ACCOUNT_DIR"'/.claude/agent-create-state.json","old_string":"false","new_string":"true"}}' \
|
|
494
|
-
| bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
495
|
-
|
|
496
|
-
if [ "$EXIT_CODE" -eq 2 ]; then
|
|
497
|
-
pass "Edit to durable state file blocked (exit 2)"
|
|
498
|
-
else
|
|
499
|
-
fail "Edit to durable state file should exit 2, got $EXIT_CODE"
|
|
500
|
-
fi
|
|
501
|
-
|
|
502
|
-
# ===========================================================================
|
|
503
|
-
# Approval hook tests — _componentDone gate advancement edge cases
|
|
504
|
-
# ===========================================================================
|
|
505
|
-
|
|
506
|
-
# ---------------------------------------------------------------------------
|
|
507
|
-
# Test 21: Non-document-editor/form component does NOT advance any gate
|
|
508
|
-
# ---------------------------------------------------------------------------
|
|
509
|
-
echo "Test 21: Non-document-editor/form component does NOT advance any gate"
|
|
510
|
-
rm -f "$STATE_FILE"
|
|
511
|
-
|
|
512
|
-
# Create fresh state file
|
|
513
|
-
echo '{"tool_name":"mcp__admin__plugin-read","tool_input":{"pluginName":"admin","file":"skills/public-agent-manager/skill.md"}}' \
|
|
514
|
-
| bash "$SCRIPT_DIR/agent-creation-post.sh" 2>/dev/null || true
|
|
515
|
-
|
|
516
|
-
# Submit a single-select component
|
|
517
|
-
python3 -c "
|
|
518
|
-
import json
|
|
519
|
-
wrapper = json.dumps({'_componentDone': True, 'component': 'single-select', 'payload': 'option-a'})
|
|
520
|
-
print(json.dumps({'prompt': wrapper, 'session_id': 'test', 'hook_event_name': 'UserPromptSubmit'}))
|
|
521
|
-
" | bash "$SCRIPT_DIR/agent-creation-approval.sh" 2>/dev/null || true
|
|
522
|
-
|
|
523
|
-
CONFIG_GATE=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['gates']['config'])" 2>/dev/null)
|
|
524
|
-
SOUL_GATE=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['gates']['soul'])" 2>/dev/null)
|
|
525
|
-
if [ "$CONFIG_GATE" = "False" ] && [ "$SOUL_GATE" = "False" ]; then
|
|
526
|
-
pass "Non-document-editor/form component did not advance any gate"
|
|
527
|
-
else
|
|
528
|
-
fail "Non-gated component should not advance gates, got soul=$SOUL_GATE config=$CONFIG_GATE"
|
|
529
|
-
fi
|
|
530
|
-
|
|
531
|
-
# ---------------------------------------------------------------------------
|
|
532
|
-
# Test 22: Document-editor for admin slug does NOT advance gate
|
|
533
|
-
# ---------------------------------------------------------------------------
|
|
534
|
-
echo "Test 22: Document-editor approval for admin slug does NOT advance gate"
|
|
535
|
-
|
|
536
|
-
python3 -c "
|
|
537
|
-
import json
|
|
538
|
-
payload = json.dumps({'action': 'approve', 'content': 'admin content', 'filePath': '$ACCOUNT_DIR/agents/admin/SOUL.md'})
|
|
539
|
-
wrapper = json.dumps({'_componentDone': True, 'component': 'document-editor', 'payload': payload})
|
|
540
|
-
print(json.dumps({'prompt': wrapper, 'session_id': 'test', 'hook_event_name': 'UserPromptSubmit'}))
|
|
541
|
-
" | bash "$SCRIPT_DIR/agent-creation-approval.sh" 2>/dev/null || true
|
|
542
|
-
|
|
543
|
-
SOUL_GATE=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['gates']['soul'])" 2>/dev/null)
|
|
544
|
-
if [ "$SOUL_GATE" = "False" ]; then
|
|
545
|
-
pass "Document-editor approval for admin slug did not advance soul gate"
|
|
546
|
-
else
|
|
547
|
-
fail "Document-editor approval for admin slug should not advance soul gate, got $SOUL_GATE"
|
|
548
|
-
fi
|
|
549
|
-
|
|
550
|
-
# ---------------------------------------------------------------------------
|
|
551
|
-
# Test 23: Gate advancement is idempotent
|
|
552
|
-
# ---------------------------------------------------------------------------
|
|
553
|
-
echo "Test 23: Gate advancement is idempotent"
|
|
554
|
-
|
|
555
|
-
# Advance soul gate
|
|
556
|
-
python3 -c "
|
|
557
|
-
import json
|
|
558
|
-
payload = json.dumps({'action': 'approve', 'content': 'personality', 'filePath': '$ACCOUNT_DIR/agents/test-agent/SOUL.md'})
|
|
559
|
-
wrapper = json.dumps({'_componentDone': True, 'component': 'document-editor', 'payload': payload})
|
|
560
|
-
print(json.dumps({'prompt': wrapper, 'session_id': 'test', 'hook_event_name': 'UserPromptSubmit'}))
|
|
561
|
-
" | bash "$SCRIPT_DIR/agent-creation-approval.sh" 2>/dev/null || true
|
|
562
|
-
|
|
563
|
-
# Advance soul gate again (should be idempotent)
|
|
564
|
-
python3 -c "
|
|
565
|
-
import json
|
|
566
|
-
payload = json.dumps({'action': 'approve', 'content': 'updated personality', 'filePath': '$ACCOUNT_DIR/agents/test-agent/SOUL.md'})
|
|
567
|
-
wrapper = json.dumps({'_componentDone': True, 'component': 'document-editor', 'payload': payload})
|
|
568
|
-
print(json.dumps({'prompt': wrapper, 'session_id': 'test', 'hook_event_name': 'UserPromptSubmit'}))
|
|
569
|
-
" | bash "$SCRIPT_DIR/agent-creation-approval.sh" 2>/dev/null || true
|
|
570
|
-
|
|
571
|
-
SOUL_GATE=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['gates']['soul'])" 2>/dev/null)
|
|
572
|
-
if [ "$SOUL_GATE" = "True" ]; then
|
|
573
|
-
pass "Gate advancement is idempotent (soul still true after double approval)"
|
|
574
|
-
else
|
|
575
|
-
fail "Gate advancement should be idempotent, got soul=$SOUL_GATE"
|
|
576
|
-
fi
|
|
577
|
-
|
|
578
|
-
# ---------------------------------------------------------------------------
|
|
579
|
-
# Test 24: Approval without state file is a no-op
|
|
580
|
-
# ---------------------------------------------------------------------------
|
|
581
|
-
echo "Test 24: Approval without state file is a no-op"
|
|
582
|
-
rm -f "$STATE_FILE"
|
|
583
|
-
|
|
584
|
-
python3 -c "
|
|
585
|
-
import json
|
|
586
|
-
payload = json.dumps({'action': 'approve', 'content': 'test', 'filePath': '$ACCOUNT_DIR/agents/test-agent/SOUL.md'})
|
|
587
|
-
wrapper = json.dumps({'_componentDone': True, 'component': 'document-editor', 'payload': payload})
|
|
588
|
-
print(json.dumps({'prompt': wrapper, 'session_id': 'test', 'hook_event_name': 'UserPromptSubmit'}))
|
|
589
|
-
" | bash "$SCRIPT_DIR/agent-creation-approval.sh" 2>/dev/null || true
|
|
590
|
-
|
|
591
|
-
if [ ! -f "$STATE_FILE" ]; then
|
|
592
|
-
pass "Approval without state file did not create state"
|
|
593
|
-
else
|
|
594
|
-
fail "Approval without state file should not create state"
|
|
595
|
-
fi
|
|
596
|
-
|
|
597
|
-
# ---------------------------------------------------------------------------
|
|
598
|
-
# Test 25: render-component does NOT advance gates (removed in Task 148)
|
|
599
|
-
# ---------------------------------------------------------------------------
|
|
600
|
-
echo "Test 25: render-component does NOT advance gates (PostToolUse)"
|
|
601
|
-
rm -f "$STATE_FILE"
|
|
602
|
-
|
|
603
|
-
# Create fresh state
|
|
604
|
-
echo '{"tool_name":"mcp__admin__plugin-read","tool_input":{"pluginName":"admin","file":"skills/public-agent-manager/skill.md"}}' \
|
|
605
|
-
| bash "$SCRIPT_DIR/agent-creation-post.sh" 2>/dev/null || true
|
|
606
|
-
|
|
607
|
-
# Pipe render-component to the post hook — should NOT advance
|
|
608
|
-
echo '{"tool_name":"mcp__admin__render-component","tool_input":{"name":"document-editor","data":{"title":"SOUL.md","content":"test","filePath":"'"$ACCOUNT_DIR"'/agents/test-agent/SOUL.md"}}}' \
|
|
609
|
-
| bash "$SCRIPT_DIR/agent-creation-post.sh" 2>/dev/null || true
|
|
610
|
-
|
|
611
|
-
SOUL_GATE=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['gates']['soul'])" 2>/dev/null)
|
|
612
|
-
if [ "$SOUL_GATE" = "False" ]; then
|
|
613
|
-
pass "render-component to PostToolUse does NOT advance gates"
|
|
614
|
-
else
|
|
615
|
-
fail "render-component should NOT advance gates (Task 148 removed this), got soul=$SOUL_GATE"
|
|
616
|
-
fi
|
|
617
|
-
|
|
618
|
-
# ---------------------------------------------------------------------------
|
|
619
|
-
# Test 26: Gate advancement order independence (config → knowledge → soul)
|
|
620
|
-
# ---------------------------------------------------------------------------
|
|
621
|
-
echo "Test 26: Gate advancement in non-standard order (config → knowledge → soul)"
|
|
622
|
-
|
|
623
|
-
# State file from test 25 still exists with all gates false
|
|
624
|
-
|
|
625
|
-
# Advance config first (form submission)
|
|
626
|
-
python3 -c "
|
|
627
|
-
import json
|
|
628
|
-
wrapper = json.dumps({'_componentDone': True, 'component': 'form', 'payload': 'Form submitted: {\"liveMemory\": true}'})
|
|
629
|
-
print(json.dumps({'prompt': wrapper, 'session_id': 'test', 'hook_event_name': 'UserPromptSubmit'}))
|
|
630
|
-
" | bash "$SCRIPT_DIR/agent-creation-approval.sh" 2>/dev/null || true
|
|
631
|
-
|
|
632
|
-
CONFIG_GATE=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['gates']['config'])" 2>/dev/null)
|
|
633
|
-
if [ "$CONFIG_GATE" != "True" ]; then
|
|
634
|
-
fail "Config gate should be True, got $CONFIG_GATE"
|
|
635
|
-
fi
|
|
636
|
-
|
|
637
|
-
# Advance knowledge second
|
|
638
|
-
python3 -c "
|
|
639
|
-
import json
|
|
640
|
-
payload = json.dumps({'action': 'approve', 'content': 'knowledge', 'filePath': '$ACCOUNT_DIR/agents/test-agent/KNOWLEDGE.md'})
|
|
641
|
-
wrapper = json.dumps({'_componentDone': True, 'component': 'document-editor', 'payload': payload})
|
|
642
|
-
print(json.dumps({'prompt': wrapper, 'session_id': 'test', 'hook_event_name': 'UserPromptSubmit'}))
|
|
643
|
-
" | bash "$SCRIPT_DIR/agent-creation-approval.sh" 2>/dev/null || true
|
|
644
|
-
|
|
645
|
-
KNOWLEDGE_GATE=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['gates']['knowledge'])" 2>/dev/null)
|
|
646
|
-
if [ "$KNOWLEDGE_GATE" != "True" ]; then
|
|
647
|
-
fail "Knowledge gate should be True, got $KNOWLEDGE_GATE"
|
|
648
|
-
fi
|
|
649
|
-
|
|
650
|
-
# Advance soul last
|
|
651
|
-
python3 -c "
|
|
652
|
-
import json
|
|
653
|
-
payload = json.dumps({'action': 'approve', 'content': 'personality', 'filePath': '$ACCOUNT_DIR/agents/test-agent/SOUL.md'})
|
|
654
|
-
wrapper = json.dumps({'_componentDone': True, 'component': 'document-editor', 'payload': payload})
|
|
655
|
-
print(json.dumps({'prompt': wrapper, 'session_id': 'test', 'hook_event_name': 'UserPromptSubmit'}))
|
|
656
|
-
" | bash "$SCRIPT_DIR/agent-creation-approval.sh" 2>/dev/null || true
|
|
657
|
-
|
|
658
|
-
# All gates true — state file persists (6h TTL handles cleanup)
|
|
659
|
-
if [ -f "$STATE_FILE" ]; then
|
|
660
|
-
ALL_TRUE=$(python3 -c "import json; g=json.load(open('$STATE_FILE'))['gates']; print(all(g.values()))" 2>/dev/null)
|
|
661
|
-
if [ "$ALL_TRUE" = "True" ]; then
|
|
662
|
-
pass "Gates advanced in non-standard order, all true"
|
|
663
|
-
else
|
|
664
|
-
fail "All gates should be true after three approvals in any order"
|
|
665
|
-
fi
|
|
666
|
-
else
|
|
667
|
-
fail "State file should still exist (cleanup is PreToolUse's job, not approval hook)"
|
|
668
|
-
fi
|
|
669
|
-
|
|
670
|
-
# ---------------------------------------------------------------------------
|
|
671
|
-
# Test 27: Diagnostics log file is written
|
|
672
|
-
# ---------------------------------------------------------------------------
|
|
673
|
-
echo "Test 27: Diagnostics log file is written"
|
|
674
|
-
|
|
675
|
-
if [ -f "$GATE_LOG" ] && [ -s "$GATE_LOG" ]; then
|
|
676
|
-
pass "Gate log file exists and contains entries"
|
|
677
|
-
else
|
|
678
|
-
fail "Gate log file should exist and contain diagnostic entries"
|
|
679
|
-
fi
|
|
680
|
-
|
|
681
|
-
# ---------------------------------------------------------------------------
|
|
682
|
-
# Test 28: Non-JSON user prompt is a no-op
|
|
683
|
-
# ---------------------------------------------------------------------------
|
|
684
|
-
echo "Test 28: Non-JSON user prompt is a no-op"
|
|
685
|
-
rm -f "$STATE_FILE"
|
|
686
|
-
|
|
687
|
-
# Create state
|
|
688
|
-
echo '{"tool_name":"mcp__admin__plugin-read","tool_input":{"pluginName":"admin","file":"skills/public-agent-manager/skill.md"}}' \
|
|
689
|
-
| bash "$SCRIPT_DIR/agent-creation-post.sh" 2>/dev/null || true
|
|
690
|
-
|
|
691
|
-
# Send a plain text prompt (no _componentDone wrapper)
|
|
692
|
-
python3 -c "
|
|
693
|
-
import json
|
|
694
|
-
print(json.dumps({'prompt': 'I approve the SOUL.md file', 'session_id': 'test', 'hook_event_name': 'UserPromptSubmit'}))
|
|
695
|
-
" | bash "$SCRIPT_DIR/agent-creation-approval.sh" 2>/dev/null || true
|
|
696
|
-
|
|
697
|
-
SOUL_GATE=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['gates']['soul'])" 2>/dev/null)
|
|
698
|
-
if [ "$SOUL_GATE" = "False" ]; then
|
|
699
|
-
pass "Plain text prompt did not advance any gate"
|
|
700
|
-
else
|
|
701
|
-
fail "Plain text prompt should not advance gates, got soul=$SOUL_GATE"
|
|
702
|
-
fi
|
|
703
|
-
|
|
704
|
-
# ---------------------------------------------------------------------------
|
|
705
|
-
# Test 29: _componentDone with missing component key is a no-op
|
|
706
|
-
# ---------------------------------------------------------------------------
|
|
707
|
-
echo "Test 29: _componentDone with missing component key is a no-op"
|
|
708
|
-
|
|
709
|
-
python3 -c "
|
|
710
|
-
import json
|
|
711
|
-
wrapper = json.dumps({'_componentDone': True, 'payload': 'some content'})
|
|
712
|
-
print(json.dumps({'prompt': wrapper, 'session_id': 'test', 'hook_event_name': 'UserPromptSubmit'}))
|
|
713
|
-
" | bash "$SCRIPT_DIR/agent-creation-approval.sh" 2>/dev/null || true
|
|
714
|
-
|
|
715
|
-
SOUL_GATE=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['gates']['soul'])" 2>/dev/null)
|
|
716
|
-
if [ "$SOUL_GATE" = "False" ]; then
|
|
717
|
-
pass "_componentDone without component key did not advance any gate"
|
|
718
|
-
else
|
|
719
|
-
fail "_componentDone without component key should not advance gates"
|
|
720
|
-
fi
|
|
721
|
-
|
|
722
|
-
rm -f "$STATE_FILE"
|
|
723
|
-
|
|
724
|
-
# ---------------------------------------------------------------------------
|
|
725
|
-
# Test 30: Session-start RECOVERS all-gates-true state when files are missing
|
|
726
|
-
# ---------------------------------------------------------------------------
|
|
727
|
-
echo "Test 30: Session-start recovers all-gates-true state when files are missing"
|
|
728
|
-
rm -f "$STATE_FILE"
|
|
729
|
-
rm -f "$ACCOUNT_DIR/agents/test-agent/SOUL.md" "$ACCOUNT_DIR/agents/test-agent/KNOWLEDGE.md" "$ACCOUNT_DIR/agents/test-agent/config.json" 2>/dev/null || true
|
|
730
|
-
DURABLE_FILE="$ACCOUNT_DIR/.claude/agent-create-state.json"
|
|
731
|
-
|
|
732
|
-
python3 -c "
|
|
733
|
-
import json
|
|
734
|
-
from datetime import datetime, timezone
|
|
735
|
-
state = {
|
|
736
|
-
'slug': 'test-agent',
|
|
737
|
-
'started': datetime.now(timezone.utc).isoformat(),
|
|
738
|
-
'gates': {'soul': True, 'knowledge': True, 'config': True}
|
|
739
|
-
}
|
|
740
|
-
with open('$DURABLE_FILE', 'w') as f:
|
|
741
|
-
json.dump(state, f)
|
|
742
|
-
"
|
|
743
|
-
|
|
744
|
-
bash "$SCRIPT_DIR/session-start.sh" "$ACCOUNT_DIR" admin 2>/dev/null || true
|
|
745
|
-
|
|
746
|
-
if [ -f "$STATE_FILE" ] && [ -f "$DURABLE_FILE" ]; then
|
|
747
|
-
pass "All-gates-true state recovered (files not yet written — writes still pending)"
|
|
748
|
-
else
|
|
749
|
-
fail "All-gates-true state should be recovered when gated files are missing from disk"
|
|
750
|
-
fi
|
|
751
|
-
|
|
752
|
-
rm -f "$STATE_FILE"
|
|
753
|
-
|
|
754
|
-
# ---------------------------------------------------------------------------
|
|
755
|
-
# Test 30a: Session-start does NOT recover truly completed state (all gates true + all files on disk)
|
|
756
|
-
# ---------------------------------------------------------------------------
|
|
757
|
-
echo "Test 30a: Session-start does NOT recover truly completed state"
|
|
758
|
-
|
|
759
|
-
# Create all gated files on disk
|
|
760
|
-
touch "$ACCOUNT_DIR/agents/test-agent/SOUL.md"
|
|
761
|
-
touch "$ACCOUNT_DIR/agents/test-agent/KNOWLEDGE.md"
|
|
762
|
-
touch "$ACCOUNT_DIR/agents/test-agent/config.json"
|
|
763
|
-
|
|
764
|
-
python3 -c "
|
|
765
|
-
import json
|
|
766
|
-
from datetime import datetime, timezone
|
|
767
|
-
state = {
|
|
768
|
-
'slug': 'test-agent',
|
|
769
|
-
'started': datetime.now(timezone.utc).isoformat(),
|
|
770
|
-
'gates': {'soul': True, 'knowledge': True, 'config': True}
|
|
771
|
-
}
|
|
772
|
-
with open('$DURABLE_FILE', 'w') as f:
|
|
773
|
-
json.dump(state, f)
|
|
774
|
-
"
|
|
775
|
-
|
|
776
|
-
bash "$SCRIPT_DIR/session-start.sh" "$ACCOUNT_DIR" admin 2>/dev/null || true
|
|
777
|
-
|
|
778
|
-
if [ ! -f "$STATE_FILE" ] && [ ! -f "$DURABLE_FILE" ]; then
|
|
779
|
-
pass "Truly completed state (all gates true + all files on disk) deleted"
|
|
780
|
-
else
|
|
781
|
-
fail "Truly completed state should be deleted — both gates passed and files written"
|
|
782
|
-
fi
|
|
783
|
-
|
|
784
|
-
# Clean up gated files for subsequent tests
|
|
785
|
-
rm -f "$ACCOUNT_DIR/agents/test-agent/SOUL.md" "$ACCOUNT_DIR/agents/test-agent/KNOWLEDGE.md" "$ACCOUNT_DIR/agents/test-agent/config.json"
|
|
786
|
-
|
|
787
|
-
# ---------------------------------------------------------------------------
|
|
788
|
-
# Test 31: PostToolUse cleans up state after last gated file is written
|
|
789
|
-
# ---------------------------------------------------------------------------
|
|
790
|
-
echo "Test 31: PostToolUse cleans up state after last gated file is written"
|
|
791
|
-
rm -f "$STATE_FILE"
|
|
792
|
-
DURABLE_FILE="$ACCOUNT_DIR/.claude/agent-create-state.json"
|
|
793
|
-
|
|
794
|
-
# Create state with all gates true
|
|
795
|
-
python3 -c "
|
|
796
|
-
import json
|
|
797
|
-
from datetime import datetime, timezone
|
|
798
|
-
state = {
|
|
799
|
-
'slug': 'test-agent',
|
|
800
|
-
'started': datetime.now(timezone.utc).isoformat(),
|
|
801
|
-
'gates': {'soul': True, 'knowledge': True, 'config': True}
|
|
802
|
-
}
|
|
803
|
-
with open('$STATE_FILE', 'w') as f:
|
|
804
|
-
json.dump(state, f)
|
|
805
|
-
with open('$DURABLE_FILE', 'w') as f:
|
|
806
|
-
json.dump(state, f)
|
|
807
|
-
"
|
|
808
|
-
|
|
809
|
-
# Create all three gated files on disk
|
|
810
|
-
touch "$ACCOUNT_DIR/agents/test-agent/SOUL.md"
|
|
811
|
-
touch "$ACCOUNT_DIR/agents/test-agent/KNOWLEDGE.md"
|
|
812
|
-
touch "$ACCOUNT_DIR/agents/test-agent/config.json"
|
|
813
|
-
|
|
814
|
-
# Simulate a Write PostToolUse for config.json (the last file)
|
|
815
|
-
echo '{"tool_name":"Write","tool_input":{"file_path":"'"$ACCOUNT_DIR"'/agents/test-agent/config.json","content":"{}"}}' \
|
|
816
|
-
| bash "$SCRIPT_DIR/agent-creation-post.sh" 2>/dev/null || true
|
|
817
|
-
|
|
818
|
-
if [ ! -f "$STATE_FILE" ] && [ ! -f "$DURABLE_FILE" ]; then
|
|
819
|
-
pass "State files cleaned up immediately after all gated files exist"
|
|
820
|
-
else
|
|
821
|
-
fail "State files should be deleted when all gates true and all files exist (volatile=$([ -f '$STATE_FILE' ] && echo yes || echo no) durable=$([ -f '$DURABLE_FILE' ] && echo yes || echo no))"
|
|
822
|
-
fi
|
|
823
|
-
|
|
824
|
-
# ---------------------------------------------------------------------------
|
|
825
|
-
# Test 32: PostToolUse does NOT clean up when a gated file is missing
|
|
826
|
-
# ---------------------------------------------------------------------------
|
|
827
|
-
echo "Test 32: PostToolUse does NOT clean up when a gated file is missing"
|
|
828
|
-
rm -f "$STATE_FILE"
|
|
829
|
-
rm -f "$DURABLE_FILE"
|
|
830
|
-
|
|
831
|
-
# Create state with all gates true
|
|
832
|
-
python3 -c "
|
|
833
|
-
import json
|
|
834
|
-
from datetime import datetime, timezone
|
|
835
|
-
state = {
|
|
836
|
-
'slug': 'test-agent',
|
|
837
|
-
'started': datetime.now(timezone.utc).isoformat(),
|
|
838
|
-
'gates': {'soul': True, 'knowledge': True, 'config': True}
|
|
839
|
-
}
|
|
840
|
-
with open('$STATE_FILE', 'w') as f:
|
|
841
|
-
json.dump(state, f)
|
|
842
|
-
with open('$DURABLE_FILE', 'w') as f:
|
|
843
|
-
json.dump(state, f)
|
|
844
|
-
"
|
|
845
|
-
|
|
846
|
-
# Only two files on disk — KNOWLEDGE.md missing
|
|
847
|
-
touch "$ACCOUNT_DIR/agents/test-agent/SOUL.md"
|
|
848
|
-
rm -f "$ACCOUNT_DIR/agents/test-agent/KNOWLEDGE.md"
|
|
849
|
-
touch "$ACCOUNT_DIR/agents/test-agent/config.json"
|
|
850
|
-
|
|
851
|
-
# Simulate a Write PostToolUse for config.json
|
|
852
|
-
echo '{"tool_name":"Write","tool_input":{"file_path":"'"$ACCOUNT_DIR"'/agents/test-agent/config.json","content":"{}"}}' \
|
|
853
|
-
| bash "$SCRIPT_DIR/agent-creation-post.sh" 2>/dev/null || true
|
|
854
|
-
|
|
855
|
-
if [ -f "$STATE_FILE" ] && [ -f "$DURABLE_FILE" ]; then
|
|
856
|
-
pass "State files preserved when KNOWLEDGE.md is missing on disk"
|
|
857
|
-
else
|
|
858
|
-
fail "State files should NOT be deleted when a gated file is missing"
|
|
859
|
-
fi
|
|
860
|
-
|
|
861
|
-
# ---------------------------------------------------------------------------
|
|
862
|
-
# Test 33: PostToolUse does NOT clean up when gates are not all true
|
|
863
|
-
# ---------------------------------------------------------------------------
|
|
864
|
-
echo "Test 33: PostToolUse does NOT clean up when gates are not all true"
|
|
865
|
-
rm -f "$STATE_FILE"
|
|
866
|
-
rm -f "$DURABLE_FILE"
|
|
867
|
-
|
|
868
|
-
# Create state with one gate still false
|
|
869
|
-
python3 -c "
|
|
870
|
-
import json
|
|
871
|
-
from datetime import datetime, timezone
|
|
872
|
-
state = {
|
|
873
|
-
'slug': 'test-agent',
|
|
874
|
-
'started': datetime.now(timezone.utc).isoformat(),
|
|
875
|
-
'gates': {'soul': True, 'knowledge': False, 'config': True}
|
|
876
|
-
}
|
|
877
|
-
with open('$STATE_FILE', 'w') as f:
|
|
878
|
-
json.dump(state, f)
|
|
879
|
-
with open('$DURABLE_FILE', 'w') as f:
|
|
880
|
-
json.dump(state, f)
|
|
881
|
-
"
|
|
882
|
-
|
|
883
|
-
# All three files on disk
|
|
884
|
-
touch "$ACCOUNT_DIR/agents/test-agent/SOUL.md"
|
|
885
|
-
touch "$ACCOUNT_DIR/agents/test-agent/KNOWLEDGE.md"
|
|
886
|
-
touch "$ACCOUNT_DIR/agents/test-agent/config.json"
|
|
887
|
-
|
|
888
|
-
# Simulate a Write PostToolUse for SOUL.md
|
|
889
|
-
echo '{"tool_name":"Write","tool_input":{"file_path":"'"$ACCOUNT_DIR"'/agents/test-agent/SOUL.md","content":"personality"}}' \
|
|
890
|
-
| bash "$SCRIPT_DIR/agent-creation-post.sh" 2>/dev/null || true
|
|
891
|
-
|
|
892
|
-
if [ -f "$STATE_FILE" ] && [ -f "$DURABLE_FILE" ]; then
|
|
893
|
-
pass "State files preserved when knowledge gate is still false"
|
|
894
|
-
else
|
|
895
|
-
fail "State files should NOT be deleted when not all gates are true"
|
|
896
|
-
fi
|
|
897
|
-
|
|
898
|
-
# Clean up agent files for other tests
|
|
899
|
-
rm -f "$ACCOUNT_DIR/agents/test-agent/SOUL.md" "$ACCOUNT_DIR/agents/test-agent/KNOWLEDGE.md" "$ACCOUNT_DIR/agents/test-agent/config.json"
|
|
900
|
-
rm -f "$STATE_FILE" "$DURABLE_FILE"
|
|
901
|
-
|
|
902
|
-
# ---------------------------------------------------------------------------
|
|
903
|
-
# Test 27: Gate hook fails explicitly when ACCOUNT_DIR is unset
|
|
904
|
-
# ---------------------------------------------------------------------------
|
|
905
|
-
echo "Test 27: Gate hook fails explicitly when ACCOUNT_DIR is unset"
|
|
906
|
-
|
|
907
|
-
EXIT_CODE=0
|
|
908
|
-
echo '{"tool_name":"Write","tool_input":{"file_path":"/some/path/agents/test-agent/SOUL.md","content":"test"}}' \
|
|
909
|
-
| env -u ACCOUNT_DIR -u GATE_LOG_FILE bash "$SCRIPT_DIR/agent-creation-gate.sh" 2>/dev/null || EXIT_CODE=$?
|
|
910
|
-
|
|
911
|
-
if [ "$EXIT_CODE" -eq 2 ]; then
|
|
912
|
-
pass "Gate hook exits 2 when ACCOUNT_DIR is unset (no silent /tmp fallback)"
|
|
913
|
-
else
|
|
914
|
-
fail "Gate hook should exit 2 when ACCOUNT_DIR is unset, got $EXIT_CODE"
|
|
915
|
-
fi
|
|
916
|
-
|
|
917
|
-
# ---------------------------------------------------------------------------
|
|
918
|
-
# Summary
|
|
919
|
-
# ---------------------------------------------------------------------------
|
|
920
|
-
echo ""
|
|
921
|
-
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
922
|
-
|
|
923
|
-
if [ "$FAIL" -gt 0 ]; then
|
|
924
|
-
exit 1
|
|
925
|
-
fi
|
|
926
|
-
exit 0
|