@rubytech/create-maxy 1.0.473 → 1.0.475
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/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 +2 -55
- package/payload/server/public/assets/ChatInput-sDYraTun.css +1 -0
- package/payload/server/public/assets/{admin-BEbxw46k.js → admin-DpmnCxNk.js} +60 -60
- package/payload/server/public/assets/public-BBxDqQvQ.js +5 -0
- package/payload/server/public/index.html +3 -3
- package/payload/server/public/public.html +3 -3
- package/payload/server/server.js +108 -104
- 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-OdyNuhVE.js +0 -5
- /package/payload/server/public/assets/{ChatInput-Dnp1FLis.js → ChatInput-DZ0j0Gdp.js} +0 -0
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# UserPromptSubmit hook — agent creation gate advancement.
|
|
3
|
-
#
|
|
4
|
-
# Advances gates when the user approves a component during agent creation.
|
|
5
|
-
# The UI wraps all component completions in a _componentDone envelope:
|
|
6
|
-
#
|
|
7
|
-
# {"_componentDone": true, "component": "<name>", "payload": "<original msg>"}
|
|
8
|
-
#
|
|
9
|
-
# Gate advancement rules:
|
|
10
|
-
# _componentDone + component="document-editor" + payload.filePath matching
|
|
11
|
-
# agents/{non-admin}/SOUL.md → gates.soul = true
|
|
12
|
-
# _componentDone + component="document-editor" + payload.filePath matching
|
|
13
|
-
# agents/{non-admin}/KNOWLEDGE.md → gates.knowledge = true
|
|
14
|
-
# _componentDone + component="form" (during active creation) → gates.config = true
|
|
15
|
-
#
|
|
16
|
-
# This hook fires on the user's response (after they click Approve or Submit),
|
|
17
|
-
# not when the agent renders a component. Gate enforcement (blocking writes)
|
|
18
|
-
# is handled by agent-creation-gate.sh (PreToolUse).
|
|
19
|
-
#
|
|
20
|
-
# Exit 0 always — UserPromptSubmit hooks must never block user messages
|
|
21
|
-
# for gate advancement purposes.
|
|
22
|
-
# Env: AGENT_CREATE_STATE_FILE (override volatile path, for testing)
|
|
23
|
-
# ACCOUNT_DIR (account directory for durable state)
|
|
24
|
-
# GATE_LOG_FILE (override log path, for testing)
|
|
25
|
-
|
|
26
|
-
if [ -n "$GATE_LOG_FILE" ]; then
|
|
27
|
-
GATE_LOG="$GATE_LOG_FILE"
|
|
28
|
-
elif [ -n "$ACCOUNT_DIR" ]; then
|
|
29
|
-
mkdir -p "$ACCOUNT_DIR/logs"
|
|
30
|
-
GATE_LOG="$ACCOUNT_DIR/logs/maxy-gate.log"
|
|
31
|
-
else
|
|
32
|
-
# UserPromptSubmit hooks must never block — exit 0 on misconfiguration
|
|
33
|
-
exit 0
|
|
34
|
-
fi
|
|
35
|
-
STATE_FILE="${AGENT_CREATE_STATE_FILE:-/tmp/maxy-agent-create-state.json}"
|
|
36
|
-
ACCOUNT_DIR="${ACCOUNT_DIR:-}"
|
|
37
|
-
DURABLE_STATE="${ACCOUNT_DIR:+$ACCOUNT_DIR/.claude/agent-create-state.json}"
|
|
38
|
-
|
|
39
|
-
# Fast path: no state file → no agent creation in progress, skip stdin read entirely
|
|
40
|
-
[[ -f "$STATE_FILE" ]] || exit 0
|
|
41
|
-
|
|
42
|
-
# Read stdin (user prompt JSON from Claude Code hook protocol)
|
|
43
|
-
INPUT=$(cat)
|
|
44
|
-
|
|
45
|
-
export _HOOK_INPUT="$INPUT"
|
|
46
|
-
export _STATE_FILE="$STATE_FILE"
|
|
47
|
-
export _DURABLE_STATE="${DURABLE_STATE:-}"
|
|
48
|
-
export _GATE_LOG="$GATE_LOG"
|
|
49
|
-
|
|
50
|
-
python3 -c '
|
|
51
|
-
import os, sys, json, re
|
|
52
|
-
|
|
53
|
-
hook_input_raw = os.environ.get("_HOOK_INPUT", "")
|
|
54
|
-
state_file = os.environ.get("_STATE_FILE", "/tmp/maxy-agent-create-state.json")
|
|
55
|
-
durable_file = os.environ.get("_DURABLE_STATE", "")
|
|
56
|
-
log_file = os.environ.get("_GATE_LOG", "/tmp/maxy-gate.log")
|
|
57
|
-
|
|
58
|
-
def log(msg):
|
|
59
|
-
try:
|
|
60
|
-
with open(log_file, "a") as lf:
|
|
61
|
-
lf.write("agent-creation-approval: %s\n" % msg)
|
|
62
|
-
except OSError:
|
|
63
|
-
pass
|
|
64
|
-
|
|
65
|
-
# Parse hook input — UserPromptSubmit provides {"prompt": "..."} on stdin
|
|
66
|
-
try:
|
|
67
|
-
hook_input = json.loads(hook_input_raw)
|
|
68
|
-
except Exception as e:
|
|
69
|
-
log("ERROR — failed to parse UserPromptSubmit input: %s" % e)
|
|
70
|
-
sys.exit(0)
|
|
71
|
-
|
|
72
|
-
if not isinstance(hook_input, dict):
|
|
73
|
-
sys.exit(0)
|
|
74
|
-
|
|
75
|
-
prompt = hook_input.get("prompt", "")
|
|
76
|
-
if not isinstance(prompt, str) or not prompt:
|
|
77
|
-
sys.exit(0)
|
|
78
|
-
|
|
79
|
-
# --- Detect _componentDone wrapper ---
|
|
80
|
-
try:
|
|
81
|
-
msg = json.loads(prompt)
|
|
82
|
-
except (json.JSONDecodeError, TypeError):
|
|
83
|
-
# Not JSON — not a wrapped component message, nothing to advance
|
|
84
|
-
sys.exit(0)
|
|
85
|
-
|
|
86
|
-
if not isinstance(msg, dict) or not msg.get("_componentDone"):
|
|
87
|
-
sys.exit(0)
|
|
88
|
-
|
|
89
|
-
component = msg.get("component", "")
|
|
90
|
-
payload_raw = msg.get("payload", "")
|
|
91
|
-
|
|
92
|
-
# --- Determine which gate to advance ---
|
|
93
|
-
gate_flag = ""
|
|
94
|
-
agent_slug = ""
|
|
95
|
-
|
|
96
|
-
if component == "document-editor":
|
|
97
|
-
# Parse payload for filePath
|
|
98
|
-
try:
|
|
99
|
-
payload = json.loads(payload_raw) if isinstance(payload_raw, str) else payload_raw
|
|
100
|
-
except (json.JSONDecodeError, TypeError):
|
|
101
|
-
payload = {}
|
|
102
|
-
if isinstance(payload, dict):
|
|
103
|
-
file_path = payload.get("filePath", "")
|
|
104
|
-
if isinstance(file_path, str):
|
|
105
|
-
m = re.search(r"agents/([^/]+)/(SOUL\.md|KNOWLEDGE\.md)$", file_path)
|
|
106
|
-
if m and m.group(1) != "admin":
|
|
107
|
-
agent_slug = m.group(1)
|
|
108
|
-
gate_flag = "soul" if m.group(2) == "SOUL.md" else "knowledge"
|
|
109
|
-
|
|
110
|
-
elif component == "form":
|
|
111
|
-
# Any form submission during active creation advances the config gate.
|
|
112
|
-
# The state file existing is sufficient context — the workflow presents
|
|
113
|
-
# exactly one form (the agent config form) during creation.
|
|
114
|
-
gate_flag = "config"
|
|
115
|
-
|
|
116
|
-
if not gate_flag:
|
|
117
|
-
sys.exit(0)
|
|
118
|
-
|
|
119
|
-
# --- Read and advance the gate ---
|
|
120
|
-
try:
|
|
121
|
-
with open(state_file) as f:
|
|
122
|
-
state = json.load(f)
|
|
123
|
-
except Exception as e:
|
|
124
|
-
log("ERROR — failed to read state file for advancement: %s" % e)
|
|
125
|
-
sys.exit(0)
|
|
126
|
-
|
|
127
|
-
if not isinstance(state, dict) or not isinstance(state.get("gates"), dict):
|
|
128
|
-
log("WARNING — state file has unexpected structure, skipping advancement")
|
|
129
|
-
sys.exit(0)
|
|
130
|
-
|
|
131
|
-
# Populate slug from document-editor filePath if empty
|
|
132
|
-
if not state.get("slug") and agent_slug:
|
|
133
|
-
state["slug"] = agent_slug
|
|
134
|
-
|
|
135
|
-
# Already advanced — idempotent
|
|
136
|
-
if state["gates"].get(gate_flag) is True:
|
|
137
|
-
sys.exit(0)
|
|
138
|
-
|
|
139
|
-
state["gates"][gate_flag] = True
|
|
140
|
-
|
|
141
|
-
# Write updated state
|
|
142
|
-
try:
|
|
143
|
-
with open(state_file, "w") as f:
|
|
144
|
-
json.dump(state, f)
|
|
145
|
-
except OSError as e:
|
|
146
|
-
log("ERROR — failed to write volatile state after advancement: %s" % e)
|
|
147
|
-
|
|
148
|
-
if durable_file:
|
|
149
|
-
try:
|
|
150
|
-
durable_dir = os.path.dirname(durable_file)
|
|
151
|
-
os.makedirs(durable_dir, exist_ok=True)
|
|
152
|
-
with open(durable_file, "w") as f:
|
|
153
|
-
json.dump(state, f)
|
|
154
|
-
except OSError as e:
|
|
155
|
-
log("WARNING — failed to write durable state after advancement: %s" % e)
|
|
156
|
-
|
|
157
|
-
pending = [k for k, v in state.get("gates", {}).items() if not v]
|
|
158
|
-
log("%s gate advanced (user approval detected). Pending: %s" % (gate_flag, ", ".join(pending) if pending else "none"))
|
|
159
|
-
' 2>>"$GATE_LOG" || true
|
|
160
|
-
|
|
161
|
-
exit 0
|
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# PreToolUse hook — agent creation approval gate.
|
|
3
|
-
#
|
|
4
|
-
# Blocks Write, Edit, and Bash writes to public agent files (SOUL.md,
|
|
5
|
-
# KNOWLEDGE.md, config.json) unless the agent creation workflow has been
|
|
6
|
-
# started AND the corresponding approval gate has been passed.
|
|
7
|
-
#
|
|
8
|
-
# Decision tree:
|
|
9
|
-
#
|
|
10
|
-
# Bash command targeting the state file itself?
|
|
11
|
-
# │
|
|
12
|
-
# ├─ Yes → exit 2: "State file is managed by hooks only."
|
|
13
|
-
# │
|
|
14
|
-
# └─ No → (continue)
|
|
15
|
-
#
|
|
16
|
-
# Write or Edit to agents/{slug}/* where slug ≠ admin?
|
|
17
|
-
# │
|
|
18
|
-
# ├─ No → exit 0 (not an agent file, irrelevant)
|
|
19
|
-
# │
|
|
20
|
-
# └─ Yes → (gate check below)
|
|
21
|
-
#
|
|
22
|
-
# Bash command writing to agents/{slug}/* where slug ≠ admin?
|
|
23
|
-
# │ (write patterns: >, >>, cat.*>, tee, cp, mv targeting gated files)
|
|
24
|
-
# │
|
|
25
|
-
# ├─ No → exit 0 (not a write to an agent file)
|
|
26
|
-
# │
|
|
27
|
-
# └─ Yes → (gate check below)
|
|
28
|
-
#
|
|
29
|
-
# Gate check:
|
|
30
|
-
# state file exists?
|
|
31
|
-
# │
|
|
32
|
-
# ├─ No → exit 2: "Load public-agent-manager skill first."
|
|
33
|
-
# │
|
|
34
|
-
# └─ Yes → has 'started' ISO timestamp?
|
|
35
|
-
# │
|
|
36
|
-
# ├─ No → delete state file, exit 2 (invalid)
|
|
37
|
-
# │
|
|
38
|
-
# └─ Yes → age > 6 hours?
|
|
39
|
-
# │
|
|
40
|
-
# ├─ Yes → delete state file, exit 2 (stale)
|
|
41
|
-
# │
|
|
42
|
-
# └─ No → structure valid (3 boolean gate keys)?
|
|
43
|
-
# │
|
|
44
|
-
# ├─ No → delete state file, exit 2 (malformed)
|
|
45
|
-
# │
|
|
46
|
-
# └─ Yes → gate for this file true?
|
|
47
|
-
# │
|
|
48
|
-
# ├─ Yes → exit 0 (approved)
|
|
49
|
-
# │
|
|
50
|
-
# └─ No → exit 2 (gate message)
|
|
51
|
-
#
|
|
52
|
-
# Python parse errors = fail open (exit 0) — never block on an infra issue.
|
|
53
|
-
# Structural validation failures = fail closed (exit 2) — forged/stale state.
|
|
54
|
-
# Admin agent files (agents/admin/*) are exempt.
|
|
55
|
-
#
|
|
56
|
-
# The Bash gate is defense-in-depth — it catches common fallback patterns
|
|
57
|
-
# (cat >, tee, cp, mv) but cannot catch all possible write vectors
|
|
58
|
-
# (e.g. python3 -c "open(...).write(...)"). The primary enforcement is
|
|
59
|
-
# the Write/Edit gate. The Bash gate also protects the state file itself
|
|
60
|
-
# from being written or deleted by the agent.
|
|
61
|
-
#
|
|
62
|
-
# Exit 0: allow
|
|
63
|
-
# Exit 2: block (stderr shown to agent)
|
|
64
|
-
# Env: AGENT_CREATE_STATE_FILE (override volatile path, for testing)
|
|
65
|
-
# ACCOUNT_DIR (account directory for durable state cleanup)
|
|
66
|
-
# GATE_LOG_FILE (override log path, for testing)
|
|
67
|
-
|
|
68
|
-
if [ -n "$GATE_LOG_FILE" ]; then
|
|
69
|
-
GATE_LOG="$GATE_LOG_FILE"
|
|
70
|
-
elif [ -n "$ACCOUNT_DIR" ]; then
|
|
71
|
-
mkdir -p "$ACCOUNT_DIR/logs"
|
|
72
|
-
GATE_LOG="$ACCOUNT_DIR/logs/maxy-gate.log"
|
|
73
|
-
else
|
|
74
|
-
echo "ACCOUNT_DIR is not set. Cannot determine gate log path." >&2
|
|
75
|
-
exit 2
|
|
76
|
-
fi
|
|
77
|
-
STATE_FILE="${AGENT_CREATE_STATE_FILE:-/tmp/maxy-agent-create-state.json}"
|
|
78
|
-
ACCOUNT_DIR="${ACCOUNT_DIR:-}"
|
|
79
|
-
DURABLE_STATE="${ACCOUNT_DIR:+$ACCOUNT_DIR/.claude/agent-create-state.json}"
|
|
80
|
-
|
|
81
|
-
# Read stdin (tool call JSON from Claude Code hook protocol)
|
|
82
|
-
INPUT=$(cat)
|
|
83
|
-
|
|
84
|
-
TOOL_NAME=$(echo "$INPUT" | python3 -c \
|
|
85
|
-
"import sys,json; d=json.load(sys.stdin); print(d.get('tool_name',''))" \
|
|
86
|
-
2>>"$GATE_LOG" || echo "")
|
|
87
|
-
|
|
88
|
-
AGENT_SLUG=""
|
|
89
|
-
AGENT_FILE=""
|
|
90
|
-
GATE_FLAG=""
|
|
91
|
-
|
|
92
|
-
case "$TOOL_NAME" in
|
|
93
|
-
Write|Edit)
|
|
94
|
-
# Extract file_path from tool input
|
|
95
|
-
FILE_PATH=$(echo "$INPUT" | python3 -c \
|
|
96
|
-
"import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path',''))" \
|
|
97
|
-
2>>"$GATE_LOG" || echo "")
|
|
98
|
-
|
|
99
|
-
[[ -z "$FILE_PATH" ]] && exit 0
|
|
100
|
-
|
|
101
|
-
# Block Write/Edit targeting the state file itself (volatile or durable).
|
|
102
|
-
# Same protection as the Bash handler — the agent must not forge or
|
|
103
|
-
# destroy gate state via any tool.
|
|
104
|
-
if [[ "$FILE_PATH" == *maxy-agent-create-state* ]] || [[ "$FILE_PATH" == *agent-create-state.json ]]; then
|
|
105
|
-
echo "Agent creation gate: the gate state file is managed by hooks only. Do not write to or delete it directly." >&2
|
|
106
|
-
exit 2
|
|
107
|
-
fi
|
|
108
|
-
|
|
109
|
-
# Check if this is an agent file: path contains agents/<slug>/<file>
|
|
110
|
-
AGENT_INFO=$(python3 -c "
|
|
111
|
-
import re
|
|
112
|
-
path = '''$FILE_PATH'''
|
|
113
|
-
m = re.search(r'agents/([^/]+)/([^/]+)$', path)
|
|
114
|
-
if m:
|
|
115
|
-
print(m.group(1) + ' ' + m.group(2))
|
|
116
|
-
else:
|
|
117
|
-
print('')
|
|
118
|
-
" 2>>"$GATE_LOG" || echo "")
|
|
119
|
-
|
|
120
|
-
[[ -z "$AGENT_INFO" ]] && exit 0
|
|
121
|
-
|
|
122
|
-
AGENT_SLUG="${AGENT_INFO%% *}"
|
|
123
|
-
AGENT_FILE="${AGENT_INFO##* }"
|
|
124
|
-
;;
|
|
125
|
-
|
|
126
|
-
Bash)
|
|
127
|
-
# --- Bash state-file protection (defense-in-depth) ---
|
|
128
|
-
# Block commands that write to or delete the state file itself.
|
|
129
|
-
# The state file is managed exclusively by hooks — the agent must not
|
|
130
|
-
# forge or destroy gate state via shell commands.
|
|
131
|
-
STATE_FILE_TARGETED=$(echo "$INPUT" | python3 -c "
|
|
132
|
-
import sys, json, re
|
|
133
|
-
d = json.load(sys.stdin)
|
|
134
|
-
cmd = d.get('tool_input', {}).get('command', '')
|
|
135
|
-
if re.search(r'(>|>>|cat\s.*>|tee\s|echo\s.*>|cp\s|mv\s|rm\s).*maxy-agent-create-state', cmd):
|
|
136
|
-
print('yes')
|
|
137
|
-
else:
|
|
138
|
-
print('no')
|
|
139
|
-
" 2>>"$GATE_LOG" || echo "no")
|
|
140
|
-
|
|
141
|
-
if [[ "$STATE_FILE_TARGETED" == "yes" ]]; then
|
|
142
|
-
echo "Agent creation gate: the gate state file is managed by hooks only. Do not write to or delete it directly." >&2
|
|
143
|
-
exit 2
|
|
144
|
-
fi
|
|
145
|
-
|
|
146
|
-
# Extract command and check for write patterns targeting agent directories.
|
|
147
|
-
# Matches: >, >>, cat.*>, tee, cp, mv followed by agents/{non-admin}/ path
|
|
148
|
-
# containing a gated filename (SOUL.md, KNOWLEDGE.md, config.json).
|
|
149
|
-
AGENT_INFO=$(echo "$INPUT" | python3 -c "
|
|
150
|
-
import sys, json, re
|
|
151
|
-
d = json.load(sys.stdin)
|
|
152
|
-
cmd = d.get('tool_input', {}).get('command', '')
|
|
153
|
-
# Match write patterns followed by a path containing agents/{slug}/{file}
|
|
154
|
-
# where file is one of the gated files
|
|
155
|
-
m = re.search(r'agents/([^/\s]+)/(SOUL\.md|KNOWLEDGE\.md|config\.json)', cmd)
|
|
156
|
-
if not m:
|
|
157
|
-
print('')
|
|
158
|
-
sys.exit(0)
|
|
159
|
-
slug = m.group(1)
|
|
160
|
-
filename = m.group(2)
|
|
161
|
-
# Check the command contains a write pattern before the path
|
|
162
|
-
write_patterns = r'(>|>>|cat\s.*>|tee\s|cp\s|mv\s)'
|
|
163
|
-
if re.search(write_patterns, cmd):
|
|
164
|
-
print(slug + ' ' + filename)
|
|
165
|
-
else:
|
|
166
|
-
print('')
|
|
167
|
-
" 2>>"$GATE_LOG" || echo "")
|
|
168
|
-
|
|
169
|
-
[[ -z "$AGENT_INFO" ]] && exit 0
|
|
170
|
-
|
|
171
|
-
AGENT_SLUG="${AGENT_INFO%% *}"
|
|
172
|
-
AGENT_FILE="${AGENT_INFO##* }"
|
|
173
|
-
;;
|
|
174
|
-
|
|
175
|
-
*)
|
|
176
|
-
exit 0
|
|
177
|
-
;;
|
|
178
|
-
esac
|
|
179
|
-
|
|
180
|
-
# Admin agent is exempt — never gated
|
|
181
|
-
[[ "$AGENT_SLUG" == "admin" ]] && exit 0
|
|
182
|
-
|
|
183
|
-
# Determine which gate flag to check
|
|
184
|
-
case "$AGENT_FILE" in
|
|
185
|
-
SOUL.md) GATE_FLAG="soul" ;;
|
|
186
|
-
KNOWLEDGE.md) GATE_FLAG="knowledge" ;;
|
|
187
|
-
config.json) GATE_FLAG="config" ;;
|
|
188
|
-
*) exit 0 ;; # Other agent files are not gated
|
|
189
|
-
esac
|
|
190
|
-
|
|
191
|
-
# --- This is a gated agent file. State file must exist. ---
|
|
192
|
-
|
|
193
|
-
if [[ ! -f "$STATE_FILE" ]]; then
|
|
194
|
-
echo "Agent creation gate: invoke the public-agent-manager skill before writing agent files. The skill workflow ensures the user reviews and approves each file." >&2
|
|
195
|
-
exit 2
|
|
196
|
-
fi
|
|
197
|
-
|
|
198
|
-
# --- State file exists. Validate before trusting. ---
|
|
199
|
-
#
|
|
200
|
-
# Two-stage validation:
|
|
201
|
-
# 1. Staleness — reject files older than 6 hours or missing 'started'
|
|
202
|
-
# 2. Structure — require exactly {started, gates: {soul, knowledge, config}}
|
|
203
|
-
#
|
|
204
|
-
# Python parse errors (can't read JSON at all) → fail open (exit 0).
|
|
205
|
-
# Structural validation failures (wrong shape) → fail closed (exit 2) + delete.
|
|
206
|
-
# These are distinct: infra issues don't block work; forged state does.
|
|
207
|
-
|
|
208
|
-
GATE_RESULT=$(python3 -c "
|
|
209
|
-
import json, os, sys
|
|
210
|
-
from datetime import datetime, timezone
|
|
211
|
-
|
|
212
|
-
state_file = '$STATE_FILE'
|
|
213
|
-
durable_file = '${DURABLE_STATE:-}'
|
|
214
|
-
gate_flag = '$GATE_FLAG'
|
|
215
|
-
|
|
216
|
-
def delete_state():
|
|
217
|
-
for f in [state_file, durable_file]:
|
|
218
|
-
if f:
|
|
219
|
-
try:
|
|
220
|
-
os.remove(f)
|
|
221
|
-
except OSError:
|
|
222
|
-
pass
|
|
223
|
-
|
|
224
|
-
try:
|
|
225
|
-
with open(state_file) as f:
|
|
226
|
-
state = json.load(f)
|
|
227
|
-
except Exception:
|
|
228
|
-
# Cannot parse JSON — fail open (infra issue, not forged state)
|
|
229
|
-
print('allow')
|
|
230
|
-
sys.exit(0)
|
|
231
|
-
|
|
232
|
-
# Non-dict JSON (e.g. array, string, number) is structurally invalid
|
|
233
|
-
if not isinstance(state, dict):
|
|
234
|
-
delete_state()
|
|
235
|
-
print('reject_malformed')
|
|
236
|
-
sys.exit(0)
|
|
237
|
-
|
|
238
|
-
# --- Staleness check ---
|
|
239
|
-
started = state.get('started')
|
|
240
|
-
if not isinstance(started, str) or not started:
|
|
241
|
-
delete_state()
|
|
242
|
-
print('reject_invalid')
|
|
243
|
-
sys.exit(0)
|
|
244
|
-
|
|
245
|
-
try:
|
|
246
|
-
started_dt = datetime.fromisoformat(started.replace('Z', '+00:00'))
|
|
247
|
-
age_hours = (datetime.now(timezone.utc) - started_dt).total_seconds() / 3600
|
|
248
|
-
except (ValueError, TypeError):
|
|
249
|
-
delete_state()
|
|
250
|
-
print('reject_invalid')
|
|
251
|
-
sys.exit(0)
|
|
252
|
-
|
|
253
|
-
if age_hours > 6 or age_hours < 0:
|
|
254
|
-
delete_state()
|
|
255
|
-
print('reject_stale')
|
|
256
|
-
sys.exit(0)
|
|
257
|
-
|
|
258
|
-
# --- Structure validation ---
|
|
259
|
-
gates = state.get('gates')
|
|
260
|
-
if not isinstance(gates, dict):
|
|
261
|
-
delete_state()
|
|
262
|
-
print('reject_malformed')
|
|
263
|
-
sys.exit(0)
|
|
264
|
-
|
|
265
|
-
expected_keys = {'soul', 'knowledge', 'config'}
|
|
266
|
-
if set(gates.keys()) != expected_keys:
|
|
267
|
-
delete_state()
|
|
268
|
-
print('reject_malformed')
|
|
269
|
-
sys.exit(0)
|
|
270
|
-
|
|
271
|
-
if not all(isinstance(v, bool) for v in gates.values()):
|
|
272
|
-
delete_state()
|
|
273
|
-
print('reject_malformed')
|
|
274
|
-
sys.exit(0)
|
|
275
|
-
|
|
276
|
-
# --- Per-gate check ---
|
|
277
|
-
if gates.get(gate_flag, False):
|
|
278
|
-
print('allow')
|
|
279
|
-
else:
|
|
280
|
-
print('reject_gate')
|
|
281
|
-
" 2>>"$GATE_LOG" || echo "allow")
|
|
282
|
-
|
|
283
|
-
case "$GATE_RESULT" in
|
|
284
|
-
allow)
|
|
285
|
-
exit 0
|
|
286
|
-
;;
|
|
287
|
-
reject_invalid)
|
|
288
|
-
echo "Agent creation gate: state file is invalid (missing or unparseable timestamp). Invoke the public-agent-manager skill to start a new agent creation workflow." >&2
|
|
289
|
-
exit 2
|
|
290
|
-
;;
|
|
291
|
-
reject_stale)
|
|
292
|
-
echo "Agent creation gate: state file is stale (older than 6 hours). Invoke the public-agent-manager skill to start a new agent creation workflow." >&2
|
|
293
|
-
exit 2
|
|
294
|
-
;;
|
|
295
|
-
reject_malformed)
|
|
296
|
-
echo "Agent creation gate: state file has invalid structure. Invoke the public-agent-manager skill to start a new agent creation workflow." >&2
|
|
297
|
-
exit 2
|
|
298
|
-
;;
|
|
299
|
-
reject_gate)
|
|
300
|
-
# Gate not passed — block with descriptive error
|
|
301
|
-
case "$GATE_FLAG" in
|
|
302
|
-
soul)
|
|
303
|
-
echo "Agent creation gate: present SOUL.md to the user via a document-editor component with the correct filePath, then wait for the user to approve. The gate advances when the user clicks Approve." >&2
|
|
304
|
-
;;
|
|
305
|
-
knowledge)
|
|
306
|
-
echo "Agent creation gate: present KNOWLEDGE.md to the user via a document-editor component with the correct filePath, then wait for the user to approve. The gate advances when the user clicks Approve." >&2
|
|
307
|
-
;;
|
|
308
|
-
config)
|
|
309
|
-
echo "Agent creation gate: present the agent configuration to the user via a form component, then wait for the user to submit. The gate advances when the user submits the form." >&2
|
|
310
|
-
;;
|
|
311
|
-
esac
|
|
312
|
-
exit 2
|
|
313
|
-
;;
|
|
314
|
-
esac
|
|
315
|
-
|
|
316
|
-
# Fallback — should not reach here, but fail open for safety
|
|
317
|
-
exit 0
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# PostToolUse hook — agent creation state creation and completion cleanup.
|
|
3
|
-
#
|
|
4
|
-
# Two responsibilities:
|
|
5
|
-
#
|
|
6
|
-
# 1. STATE CREATION: When mcp__admin__plugin-read loads the public-agent-manager
|
|
7
|
-
# skill, creates (or resets) the gate state file with all gates false. Always
|
|
8
|
-
# overwrites any existing state — loading the skill is the intent to start a
|
|
9
|
-
# fresh workflow. This is the only entry point that unlocks agent file writes.
|
|
10
|
-
#
|
|
11
|
-
# 2. STATE CLEANUP: After a successful Write/Edit to a gated agent file, checks
|
|
12
|
-
# whether all gates are true AND all three gated files (SOUL.md, KNOWLEDGE.md,
|
|
13
|
-
# config.json) exist on disk. If so, creation is complete — deletes both
|
|
14
|
-
# volatile and durable state files immediately.
|
|
15
|
-
#
|
|
16
|
-
# Gate advancement (advancing gates from false to true) is handled by
|
|
17
|
-
# agent-creation-approval.sh (UserPromptSubmit), which fires when the user
|
|
18
|
-
# approves a component. Gate enforcement (blocking writes until gates pass)
|
|
19
|
-
# is handled by agent-creation-gate.sh (PreToolUse).
|
|
20
|
-
#
|
|
21
|
-
# Exit 0 always — PostToolUse hooks must never block tool execution.
|
|
22
|
-
# Env: AGENT_CREATE_STATE_FILE (override volatile path, for testing)
|
|
23
|
-
# ACCOUNT_DIR (account directory for durable state)
|
|
24
|
-
# GATE_LOG_FILE (override log path, for testing)
|
|
25
|
-
|
|
26
|
-
if [ -n "$GATE_LOG_FILE" ]; then
|
|
27
|
-
GATE_LOG="$GATE_LOG_FILE"
|
|
28
|
-
elif [ -n "$ACCOUNT_DIR" ]; then
|
|
29
|
-
mkdir -p "$ACCOUNT_DIR/logs"
|
|
30
|
-
GATE_LOG="$ACCOUNT_DIR/logs/maxy-gate.log"
|
|
31
|
-
else
|
|
32
|
-
# PostToolUse hooks must never block — exit 0 on misconfiguration
|
|
33
|
-
exit 0
|
|
34
|
-
fi
|
|
35
|
-
STATE_FILE="${AGENT_CREATE_STATE_FILE:-/tmp/maxy-agent-create-state.json}"
|
|
36
|
-
ACCOUNT_DIR="${ACCOUNT_DIR:-}"
|
|
37
|
-
DURABLE_STATE="${ACCOUNT_DIR:+$ACCOUNT_DIR/.claude/agent-create-state.json}"
|
|
38
|
-
|
|
39
|
-
# Read stdin (tool call JSON from Claude Code hook protocol)
|
|
40
|
-
INPUT=$(cat)
|
|
41
|
-
|
|
42
|
-
TOOL_NAME=$(echo "$INPUT" | python3 -c \
|
|
43
|
-
"import sys,json; d=json.load(sys.stdin); print(d.get('tool_name',''))" \
|
|
44
|
-
2>>"$GATE_LOG" || echo "")
|
|
45
|
-
|
|
46
|
-
# ── STATE CREATION: plugin-read for public-agent-manager ──
|
|
47
|
-
|
|
48
|
-
if [[ "$TOOL_NAME" == "mcp__admin__plugin-read" ]]; then
|
|
49
|
-
MATCHES_PAM=$(echo "$INPUT" | python3 -c "
|
|
50
|
-
import sys, json
|
|
51
|
-
d = json.load(sys.stdin)
|
|
52
|
-
ti = d.get('tool_input', {})
|
|
53
|
-
plugin = ti.get('pluginName', '')
|
|
54
|
-
file = ti.get('file', '')
|
|
55
|
-
# plugin-read uses pluginName + file, not path
|
|
56
|
-
print('yes' if plugin == 'admin' and file.startswith('skills/public-agent-manager/') else 'no')
|
|
57
|
-
" 2>>"$GATE_LOG" || echo "no")
|
|
58
|
-
|
|
59
|
-
if [[ "$MATCHES_PAM" == "yes" ]]; then
|
|
60
|
-
python3 -c "
|
|
61
|
-
import json, datetime, os, sys
|
|
62
|
-
|
|
63
|
-
state_file = '$STATE_FILE'
|
|
64
|
-
durable_file = '${DURABLE_STATE:-}'
|
|
65
|
-
log_file = '$GATE_LOG'
|
|
66
|
-
|
|
67
|
-
state = {
|
|
68
|
-
'slug': '',
|
|
69
|
-
'started': datetime.datetime.utcnow().isoformat() + 'Z',
|
|
70
|
-
'gates': {'soul': False, 'knowledge': False, 'config': False}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
try:
|
|
74
|
-
with open(state_file, 'w') as f:
|
|
75
|
-
json.dump(state, f)
|
|
76
|
-
except OSError as e:
|
|
77
|
-
with open(log_file, 'a') as lf:
|
|
78
|
-
lf.write('agent-creation-gate: ERROR — failed to create volatile state file: %s\n' % e)
|
|
79
|
-
sys.exit(0)
|
|
80
|
-
|
|
81
|
-
if durable_file:
|
|
82
|
-
try:
|
|
83
|
-
durable_dir = os.path.dirname(durable_file)
|
|
84
|
-
os.makedirs(durable_dir, exist_ok=True)
|
|
85
|
-
with open(durable_file, 'w') as f:
|
|
86
|
-
json.dump(state, f)
|
|
87
|
-
except OSError as e:
|
|
88
|
-
with open(log_file, 'a') as lf:
|
|
89
|
-
lf.write('agent-creation-gate: WARNING — failed to create durable state file: %s\n' % e)
|
|
90
|
-
|
|
91
|
-
with open(log_file, 'a') as lf:
|
|
92
|
-
lf.write('agent-creation-gate: workflow started. All gates pending: soul, knowledge, config\n')
|
|
93
|
-
" 2>>"$GATE_LOG" || true
|
|
94
|
-
fi
|
|
95
|
-
exit 0
|
|
96
|
-
fi
|
|
97
|
-
|
|
98
|
-
# ── STATE CLEANUP: delete state when all gated files exist on disk ──
|
|
99
|
-
# Fast path: no state file → nothing to clean up
|
|
100
|
-
[[ -f "$STATE_FILE" ]] || exit 0
|
|
101
|
-
|
|
102
|
-
if [[ "$TOOL_NAME" == "Write" || "$TOOL_NAME" == "Edit" ]]; then
|
|
103
|
-
export _HOOK_INPUT="$INPUT"
|
|
104
|
-
export _STATE_FILE="$STATE_FILE"
|
|
105
|
-
export _DURABLE_STATE="${DURABLE_STATE:-}"
|
|
106
|
-
export _GATE_LOG="$GATE_LOG"
|
|
107
|
-
|
|
108
|
-
python3 -c '
|
|
109
|
-
import json, os, re, sys
|
|
110
|
-
|
|
111
|
-
state_file = os.environ.get("_STATE_FILE", "/tmp/maxy-agent-create-state.json")
|
|
112
|
-
durable_file = os.environ.get("_DURABLE_STATE", "")
|
|
113
|
-
log_file = os.environ.get("_GATE_LOG", "/tmp/maxy-gate.log")
|
|
114
|
-
input_raw = os.environ.get("_HOOK_INPUT", "")
|
|
115
|
-
|
|
116
|
-
def log(msg):
|
|
117
|
-
try:
|
|
118
|
-
with open(log_file, "a") as lf:
|
|
119
|
-
lf.write("agent-creation-post: %s\n" % msg)
|
|
120
|
-
except OSError:
|
|
121
|
-
pass
|
|
122
|
-
|
|
123
|
-
# Parse tool input to get file_path
|
|
124
|
-
try:
|
|
125
|
-
tool_data = json.loads(input_raw)
|
|
126
|
-
file_path = tool_data.get("tool_input", {}).get("file_path", "")
|
|
127
|
-
except Exception:
|
|
128
|
-
sys.exit(0)
|
|
129
|
-
|
|
130
|
-
# Is this a write to a gated agent file?
|
|
131
|
-
m = re.search(r"agents/([^/]+)/(SOUL\.md|KNOWLEDGE\.md|config\.json)$", file_path)
|
|
132
|
-
if not m or m.group(1) == "admin":
|
|
133
|
-
sys.exit(0)
|
|
134
|
-
|
|
135
|
-
# Read state — are all gates true?
|
|
136
|
-
try:
|
|
137
|
-
with open(state_file) as f:
|
|
138
|
-
state = json.load(f)
|
|
139
|
-
except Exception:
|
|
140
|
-
sys.exit(0)
|
|
141
|
-
|
|
142
|
-
gates = state.get("gates", {})
|
|
143
|
-
if not (isinstance(gates, dict) and all(v is True for v in gates.values())):
|
|
144
|
-
sys.exit(0)
|
|
145
|
-
|
|
146
|
-
# All gates true. Check if all three gated files exist on disk.
|
|
147
|
-
agent_dir = os.path.dirname(file_path)
|
|
148
|
-
gated_files = ["SOUL.md", "KNOWLEDGE.md", "config.json"]
|
|
149
|
-
if not all(os.path.isfile(os.path.join(agent_dir, f)) for f in gated_files):
|
|
150
|
-
sys.exit(0)
|
|
151
|
-
|
|
152
|
-
# All files exist — creation is complete. Clean up state.
|
|
153
|
-
for f in [state_file, durable_file]:
|
|
154
|
-
if f:
|
|
155
|
-
try:
|
|
156
|
-
os.remove(f)
|
|
157
|
-
except OSError:
|
|
158
|
-
pass
|
|
159
|
-
|
|
160
|
-
slug = state.get("slug", "")
|
|
161
|
-
log("creation complete for \"%s\" — state files cleaned up" % slug)
|
|
162
|
-
' 2>>"$GATE_LOG" || true
|
|
163
|
-
fi
|
|
164
|
-
|
|
165
|
-
exit 0
|