@rubytech/create-maxy-code 0.1.214 → 0.1.216
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/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +4 -1
- package/payload/platform/plugins/admin/hooks/__tests__/post-tool-use-agent.test.sh +97 -1
- package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-admin-tool-gate.test.sh +53 -12
- package/payload/platform/plugins/admin/hooks/lib/hook-emit.sh +30 -0
- package/payload/platform/plugins/admin/hooks/post-tool-use-agent.sh +81 -29
- package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +34 -17
- package/payload/platform/plugins/admin/mcp/dist/__tests__/capabilities-here.test.d.ts +2 -0
- package/payload/platform/plugins/admin/mcp/dist/__tests__/capabilities-here.test.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/__tests__/capabilities-here.test.js +130 -0
- package/payload/platform/plugins/admin/mcp/dist/__tests__/capabilities-here.test.js.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js +63 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/lib/catalogue-census.d.ts +14 -0
- package/payload/platform/plugins/admin/mcp/dist/lib/catalogue-census.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/lib/catalogue-census.js +50 -0
- package/payload/platform/plugins/admin/mcp/dist/lib/catalogue-census.js.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/capabilities-here.d.ts +28 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/capabilities-here.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/capabilities-here.js +68 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/capabilities-here.js.map +1 -0
- package/payload/platform/plugins/admin/skills/capabilities-here/SKILL.md +31 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +30 -14
- package/payload/platform/plugins/docs/references/platform.md +19 -9
- package/payload/platform/plugins/docs/references/plugins-guide.md +2 -0
- package/payload/platform/scripts/check-architecture-skill-no-drift.mjs +89 -42
- package/payload/platform/scripts/check-roles-doc-completeness.mjs +96 -0
- package/payload/platform/scripts/check-specialist-tool-surface.mjs +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +77 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +30 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts +18 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js +63 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +5 -0
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +29 -0
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-script-spawn.d.ts +80 -0
- package/payload/platform/services/claude-session-manager/dist/rc-script-spawn.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/rc-script-spawn.js +306 -0
- package/payload/platform/services/claude-session-manager/dist/rc-script-spawn.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/specialist-drift.d.ts +9 -0
- package/payload/platform/services/claude-session-manager/dist/specialist-drift.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/specialist-drift.js +6 -2
- package/payload/platform/services/claude-session-manager/dist/specialist-drift.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +3 -1
- package/payload/platform/templates/specialists/agents/content-producer.md +1 -1
- package/payload/server/public/assets/AdminShell-CPWbJ_I8.js +1 -0
- package/payload/server/public/assets/{admin-DMo5DWKH.js → admin-DjGGOx2K.js} +1 -1
- package/payload/server/public/assets/{data-D6IQdm0R.js → data-WBEvtRV-.js} +1 -1
- package/payload/server/public/assets/{graph-PkZKVkWD.js → graph-DsOLsIPf.js} +1 -1
- package/payload/server/public/data.html +2 -2
- package/payload/server/public/graph.html +2 -2
- package/payload/server/public/index.html +2 -2
- package/payload/server/server.js +6 -0
- package/payload/server/public/assets/AdminShell-BcxEZnpu.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "admin",
|
|
3
|
-
"description": "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit), and session-retrospective-mark-complete (the deterministic sentinel the session-end Stop hook reads via JSONL grep to release the retrospective gate; admin calls it once per session after running the three-pass retrospective) for managing the Maxy platform.",
|
|
3
|
+
"description": "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, capabilities-here (deterministic install-introspection — returns enabledPlugins, tier, models, installed platform + premium specialists, registered Claude Code agent types, and the brand block as a single JSON object; the canonical tool for grounding any claim about what is enabled on the current account), account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit), and session-retrospective-mark-complete (the deterministic sentinel the session-end Stop hook reads via JSONL grep to release the retrospective gate; admin calls it once per session after running the three-pass retrospective) for managing the Maxy platform.",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Rubytech LLC"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: admin
|
|
3
3
|
surface: platform
|
|
4
|
-
description: "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit), and session-retrospective-mark-complete (the deterministic sentinel the session-end Stop hook reads via JSONL grep to release the retrospective gate; admin calls it once per session after running the three-pass retrospective) for managing the Maxy platform."
|
|
4
|
+
description: "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, capabilities-here (deterministic install-introspection — returns enabledPlugins, tier, models, installed platform + premium specialists, registered Claude Code agent types, and the brand block as a single JSON object; the canonical tool for grounding any claim about what is enabled on the current account), account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit), and session-retrospective-mark-complete (the deterministic sentinel the session-end Stop hook reads via JSONL grep to release the retrospective gate; admin calls it once per session after running the three-pass retrospective) for managing the Maxy platform."
|
|
5
5
|
tools:
|
|
6
6
|
- name: system-status
|
|
7
7
|
publicAllowlist: false
|
|
@@ -18,6 +18,9 @@ tools:
|
|
|
18
18
|
- name: account-manage
|
|
19
19
|
publicAllowlist: false
|
|
20
20
|
adminAllowlist: false
|
|
21
|
+
- name: capabilities-here
|
|
22
|
+
publicAllowlist: false
|
|
23
|
+
adminAllowlist: false
|
|
21
24
|
- name: account-update
|
|
22
25
|
publicAllowlist: false
|
|
23
26
|
adminAllowlist: false
|
|
@@ -72,6 +72,102 @@ echo "$CENSUS_LOG" | grep -qE 'attachmentsEmitted=2' \
|
|
|
72
72
|
&& pass "census M equals N when stdout open" \
|
|
73
73
|
|| fail "census M wrong: $CENSUS_LOG"
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
# Task 563 — concurrency hardening.
|
|
76
|
+
|
|
77
|
+
# Concurrent emitter + drainer on the same buffer must not drop records.
|
|
78
|
+
# Spawn an emitter that writes 50 records in a tight loop while two
|
|
79
|
+
# drainer fires race against it. Union of attachments must equal 50.
|
|
80
|
+
LIB="$(cd "$(dirname "$0")/.." && pwd)/lib/hook-emit.sh"
|
|
81
|
+
[[ -f "$LIB" ]] || { fail "lib missing"; }
|
|
82
|
+
CONCUR_TMP=$(mktemp -d)
|
|
83
|
+
export MAXY_HOOK_BUFFER_DIR="$CONCUR_TMP/buffers"
|
|
84
|
+
export MAXY_HOOK_SERVER_LOG="$CONCUR_TMP/server.log"
|
|
85
|
+
mkdir -p "$MAXY_HOOK_BUFFER_DIR"
|
|
86
|
+
PARENT_C="parent-CONC"
|
|
87
|
+
SUB_C="sub-CONC"
|
|
88
|
+
INPUT_C=$(python3 -c '
|
|
89
|
+
import json, sys
|
|
90
|
+
print(json.dumps({"hook_event_name":"PostToolUse","tool_name":"Agent",
|
|
91
|
+
"session_id":sys.argv[1]}, separators=(",",":")))
|
|
92
|
+
' "$PARENT_C")
|
|
93
|
+
|
|
94
|
+
# Emitter: 50 records as fast as possible.
|
|
95
|
+
EMIT_OUT="$CONCUR_TMP/emit.log"
|
|
96
|
+
(
|
|
97
|
+
# shellcheck disable=SC1090
|
|
98
|
+
source "$LIB"
|
|
99
|
+
for i in $(seq 1 50); do
|
|
100
|
+
hook_emit_decision "$SUB_C" "h" "Bash" "block" "r$i" "x" 2 1
|
|
101
|
+
done
|
|
102
|
+
echo "done" >"$EMIT_OUT"
|
|
103
|
+
) &
|
|
104
|
+
EMIT_PID=$!
|
|
105
|
+
|
|
106
|
+
# Two drainer fires racing the emitter.
|
|
107
|
+
DRAIN1="$CONCUR_TMP/drain1.out"
|
|
108
|
+
DRAIN2="$CONCUR_TMP/drain2.out"
|
|
109
|
+
(printf '%s' "$INPUT_C" | bash "$HOOK" >"$DRAIN1" 2>/dev/null) &
|
|
110
|
+
D1=$!
|
|
111
|
+
(printf '%s' "$INPUT_C" | bash "$HOOK" >"$DRAIN2" 2>/dev/null) &
|
|
112
|
+
D2=$!
|
|
113
|
+
wait "$EMIT_PID" "$D1" "$D2"
|
|
114
|
+
|
|
115
|
+
# Drain any leftovers after emitter+drainers settled.
|
|
116
|
+
DRAIN3="$CONCUR_TMP/drain3.out"
|
|
117
|
+
printf '%s' "$INPUT_C" | bash "$HOOK" >"$DRAIN3" 2>/dev/null
|
|
118
|
+
# A second sweep catches buffer files created after drainer 3 started.
|
|
119
|
+
DRAIN4="$CONCUR_TMP/drain4.out"
|
|
120
|
+
printf '%s' "$INPUT_C" | bash "$HOOK" >"$DRAIN4" 2>/dev/null
|
|
121
|
+
|
|
122
|
+
TOTAL_ATTACHED=$(cat "$DRAIN1" "$DRAIN2" "$DRAIN3" "$DRAIN4" | grep -c '^\[hook-propagate\] {' || true)
|
|
123
|
+
UNIQUE_R=$(cat "$DRAIN1" "$DRAIN2" "$DRAIN3" "$DRAIN4" | grep '^\[hook-propagate\] {' | \
|
|
124
|
+
python3 -c 'import json,sys
|
|
125
|
+
seen=set()
|
|
126
|
+
for line in sys.stdin:
|
|
127
|
+
rec=line.split(" ",1)[1]
|
|
128
|
+
d=json.loads(rec)
|
|
129
|
+
seen.add(d["reason"])
|
|
130
|
+
print(len(seen))')
|
|
131
|
+
[[ "$TOTAL_ATTACHED" -eq 50 ]] && pass "concurrent: 50 records attached, no duplicates" \
|
|
132
|
+
|| fail "concurrent: expected 50 attached, got $TOTAL_ATTACHED"
|
|
133
|
+
[[ "$UNIQUE_R" -eq 50 ]] && pass "concurrent: 50 unique records, no drops" \
|
|
134
|
+
|| fail "concurrent: expected 50 unique reasons, got $UNIQUE_R"
|
|
135
|
+
|
|
136
|
+
# No cursor file should exist (cursor mechanism removed).
|
|
137
|
+
[[ -z "$(ls "$MAXY_HOOK_BUFFER_DIR"/.*.cursor 2>/dev/null)" ]] \
|
|
138
|
+
&& pass "cursor mechanism removed (no .cursor files)" \
|
|
139
|
+
|| fail "cursor file unexpectedly present: $(ls "$MAXY_HOOK_BUFFER_DIR"/.*.cursor 2>/dev/null)"
|
|
140
|
+
|
|
141
|
+
rm -rf "$CONCUR_TMP"
|
|
142
|
+
|
|
143
|
+
# Unknown-parent fallback emits observability line and does not corrupt
|
|
144
|
+
# state across concurrent fires.
|
|
145
|
+
UNK_TMP=$(mktemp -d)
|
|
146
|
+
export MAXY_HOOK_BUFFER_DIR="$UNK_TMP/buffers"
|
|
147
|
+
export MAXY_HOOK_SERVER_LOG="$UNK_TMP/server.log"
|
|
148
|
+
mkdir -p "$MAXY_HOOK_BUFFER_DIR"
|
|
149
|
+
# Stdin with no session_id at all.
|
|
150
|
+
BAD_INPUT='{"hook_event_name":"PostToolUse","tool_name":"Agent"}'
|
|
151
|
+
UNK1="$UNK_TMP/unk1.out"
|
|
152
|
+
UNK2="$UNK_TMP/unk2.out"
|
|
153
|
+
(printf '%s' "$BAD_INPUT" | bash "$HOOK" >"$UNK1" 2>/dev/null) &
|
|
154
|
+
U1=$!
|
|
155
|
+
(printf '%s' "$BAD_INPUT" | bash "$HOOK" >"$UNK2" 2>/dev/null) &
|
|
156
|
+
U2=$!
|
|
157
|
+
wait "$U1" "$U2"
|
|
158
|
+
|
|
159
|
+
grep -q 'parentSession=unknown reason=session-id-missing' "$UNK1" "$UNK2" \
|
|
160
|
+
&& pass "unknown parent emits observability line" \
|
|
161
|
+
|| fail "unknown parent missing observability line: $(cat "$UNK1" "$UNK2")"
|
|
162
|
+
grep -q 'parentSession=unknown reason=session-id-missing' "$MAXY_HOOK_SERVER_LOG" \
|
|
163
|
+
&& pass "unknown parent line logged to server.log" \
|
|
164
|
+
|| fail "server.log missing unknown-parent line"
|
|
165
|
+
# No .unknown.cursor file — cursor mechanism is gone.
|
|
166
|
+
[[ ! -e "$MAXY_HOOK_BUFFER_DIR/.unknown.cursor" ]] \
|
|
167
|
+
&& pass "no shared .unknown.cursor file" \
|
|
168
|
+
|| fail ".unknown.cursor unexpectedly present"
|
|
169
|
+
|
|
170
|
+
rm -rf "$UNK_TMP"
|
|
171
|
+
|
|
76
172
|
echo "----"; echo "$PASS pass, $FAIL fail"
|
|
77
173
|
[[ "$FAIL" -eq 0 ]]
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Task 529 — admin-tool-gate enforces orchestration-only at the PreToolUse
|
|
3
3
|
# hook. Admin-direct invocations of Write/Edit/MultiEdit/Bash/WebFetch/
|
|
4
|
-
# WebSearch are refused with the dispatch hint;
|
|
5
|
-
# pass through.
|
|
4
|
+
# WebSearch are refused with the dispatch hint; subagents pass through.
|
|
6
5
|
#
|
|
7
|
-
# Task 559 —
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
6
|
+
# Task 559 — discriminator is `parent_tool_use_id` from the Claude Code
|
|
7
|
+
# hook input. Non-empty → in-window Task subagent (allow).
|
|
8
|
+
#
|
|
9
|
+
# Task 566 — discriminator extended to `$MAXY_SPECIALIST` for maxy MCP
|
|
10
|
+
# `Agent`-spawned specialists (separate top-level `claude rc` processes
|
|
11
|
+
# with no `parent_tool_use_id` but `MAXY_SPECIALIST=<name>` on env, set
|
|
12
|
+
# by pty-spawner.ts:1601). Calls with neither marker AND no
|
|
13
|
+
# `MAXY_SESSION_ROLE=admin` fail closed with `reason=missing-marker` so a
|
|
14
|
+
# future spawn shape cannot silently regress.
|
|
12
15
|
|
|
13
16
|
set -u
|
|
14
17
|
|
|
@@ -35,8 +38,15 @@ fail() { FAIL=$((FAIL+1)); echo "FAIL: $1" >&2; }
|
|
|
35
38
|
SESSION_TRANSCRIPT="/tmp/sess/44283ae1-2b82-4bd7-b8af-99a11141ed4b.jsonl"
|
|
36
39
|
SUBAGENT_PARENT_ID="toolu_01QFC4W32wFUyHy6WJMNjHf6"
|
|
37
40
|
|
|
41
|
+
# run <tool> <parent_id> <expected_rc> <expected_pattern> <label>
|
|
42
|
+
# Env knobs (default mirror production admin spawn):
|
|
43
|
+
# MAXY_SESSION_ROLE_OVERRIDE — defaults to "admin"; set empty to drop the role
|
|
44
|
+
# MAXY_SPECIALIST_OVERRIDE — defaults to empty; set to a name to simulate
|
|
45
|
+
# an MCP-Agent-spawned specialist
|
|
38
46
|
run() {
|
|
39
47
|
local tool="$1" parent_id="$2" expected_rc="$3" expected_pattern="$4" label="$5"
|
|
48
|
+
local role="${MAXY_SESSION_ROLE_OVERRIDE-admin}"
|
|
49
|
+
local specialist="${MAXY_SPECIALIST_OVERRIDE-}"
|
|
40
50
|
local input_json
|
|
41
51
|
input_json=$(python3 -c '
|
|
42
52
|
import json, sys
|
|
@@ -53,7 +63,10 @@ print(json.dumps(payload, separators=(",", ":")))
|
|
|
53
63
|
' "$tool" "$SESSION_TRANSCRIPT" "$parent_id")
|
|
54
64
|
local stdout_file; stdout_file=$(mktemp); TMPFILES+=("$stdout_file")
|
|
55
65
|
local stderr_file; stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
|
|
56
|
-
printf '%s' "$input_json" |
|
|
66
|
+
printf '%s' "$input_json" | \
|
|
67
|
+
env -u MAXY_SESSION_ROLE -u MAXY_SPECIALIST \
|
|
68
|
+
MAXY_SESSION_ROLE="$role" MAXY_SPECIALIST="$specialist" \
|
|
69
|
+
bash "$HOOK" admin >"$stdout_file" 2>"$stderr_file"
|
|
57
70
|
local rc=$?
|
|
58
71
|
if [[ "$rc" -ne "$expected_rc" ]]; then
|
|
59
72
|
fail "$label: expected exit $expected_rc, got $rc. Stderr: $(cat "$stderr_file")"
|
|
@@ -66,8 +79,8 @@ print(json.dumps(payload, separators=(",", ":")))
|
|
|
66
79
|
pass "$label"
|
|
67
80
|
}
|
|
68
81
|
|
|
69
|
-
# ── Admin-direct refusals (no parent_tool_use_id)
|
|
70
|
-
run Write "" 2 '\[admin-tool-gate\] role=admin parent_tool_use_id=- specialist=- tool=Write decision=block.*owner=content-producer' \
|
|
82
|
+
# ── Admin-direct refusals (no parent_tool_use_id, no MAXY_SPECIALIST, role=admin) ───
|
|
83
|
+
run Write "" 2 '\[admin-tool-gate\] role=admin parent_tool_use_id=- specialist=- session_role=admin tool=Write decision=block.*owner=content-producer' \
|
|
71
84
|
"Test 1: admin-direct Write rejected with content-producer dispatch hint"
|
|
72
85
|
run Edit "" 2 '\[admin-tool-gate\] role=admin .*tool=Edit decision=block.*owner=content-producer' \
|
|
73
86
|
"Test 2: admin-direct Edit rejected"
|
|
@@ -97,7 +110,7 @@ run Read "" 0 '' \
|
|
|
97
110
|
"Test 12: admin-direct Read passes (Read is admin-keep-list)"
|
|
98
111
|
|
|
99
112
|
# ── Task 560: admin-direct block emits propagation record ──────────────
|
|
100
|
-
#
|
|
113
|
+
# Admin-direct path: MAXY_SESSION_ROLE=admin, no MAXY_SPECIALIST, no parent.
|
|
101
114
|
PROP_TMPDIR=$(mktemp -d); TMPFILES+=("$PROP_TMPDIR")
|
|
102
115
|
export MAXY_HOOK_BUFFER_DIR="$PROP_TMPDIR/buffers"
|
|
103
116
|
export MAXY_HOOK_SERVER_LOG="$PROP_TMPDIR/server.log"
|
|
@@ -108,7 +121,9 @@ print(json.dumps({"hook_event_name":"PreToolUse","tool_name":"Bash",
|
|
|
108
121
|
"tool_input":{"command":"ls"},"transcript_path":sys.argv[1],
|
|
109
122
|
"session_id":sys.argv[2]}, separators=(",",":")))
|
|
110
123
|
' "$SESSION_TRANSCRIPT" "$SESSION")
|
|
111
|
-
printf '%s' "$PROP_INPUT" |
|
|
124
|
+
printf '%s' "$PROP_INPUT" | \
|
|
125
|
+
env -u MAXY_SPECIALIST MAXY_SESSION_ROLE=admin \
|
|
126
|
+
bash "$HOOK" admin >/dev/null 2>/dev/null
|
|
112
127
|
BUF="$MAXY_HOOK_BUFFER_DIR/$SESSION.jsonl"
|
|
113
128
|
if [[ -f "$BUF" ]] && grep -q '"hookName":"admin-tool-gate"' "$BUF" \
|
|
114
129
|
&& grep -q '"decision":"block"' "$BUF" \
|
|
@@ -119,6 +134,32 @@ else
|
|
|
119
134
|
fi
|
|
120
135
|
unset MAXY_HOOK_BUFFER_DIR MAXY_HOOK_SERVER_LOG
|
|
121
136
|
|
|
137
|
+
# ── Task 566: maxy-MCP-Agent-spawned specialist passthrough ────────────
|
|
138
|
+
# MCP-Agent specialists are separate top-level claude rc processes with no
|
|
139
|
+
# parent_tool_use_id; pty-spawner stamps MAXY_SPECIALIST=<name> on the env.
|
|
140
|
+
MAXY_SPECIALIST_OVERRIDE=content-producer \
|
|
141
|
+
run Edit "" 0 '\[admin-tool-gate\] role=subagent .*specialist=content-producer .*tool=Edit decision=allow source=mcp-specialist' \
|
|
142
|
+
"Test 14: mcp-specialist Edit passes through (Task 566)"
|
|
143
|
+
|
|
144
|
+
MAXY_SPECIALIST_OVERRIDE=coding-assistant \
|
|
145
|
+
run Bash "" 0 '\[admin-tool-gate\] role=subagent .*specialist=coding-assistant .*tool=Bash decision=allow source=mcp-specialist' \
|
|
146
|
+
"Test 15: mcp-specialist Bash passes through (Task 559 regression class)"
|
|
147
|
+
|
|
148
|
+
MAXY_SPECIALIST_OVERRIDE=research-assistant \
|
|
149
|
+
run WebFetch "" 0 '\[admin-tool-gate\] role=subagent .*specialist=research-assistant .*tool=WebFetch decision=allow source=mcp-specialist' \
|
|
150
|
+
"Test 16: mcp-specialist WebFetch passes through"
|
|
151
|
+
|
|
152
|
+
# ── Task 566: missing-marker fail-closed ───────────────────────────────
|
|
153
|
+
# No parent_tool_use_id, no MAXY_SPECIALIST, MAXY_SESSION_ROLE not 'admin'.
|
|
154
|
+
MAXY_SESSION_ROLE_OVERRIDE="" MAXY_SPECIALIST_OVERRIDE="" \
|
|
155
|
+
run Edit "" 2 '\[admin-tool-gate\] role=unknown .*tool=Edit decision=block reason=missing-marker' \
|
|
156
|
+
"Test 17: missing-marker fail-closed (no positive role marker)"
|
|
157
|
+
|
|
158
|
+
# ── Task 566: combined parent_tool_use_id + MAXY_SPECIALIST is also subagent ──
|
|
159
|
+
MAXY_SPECIALIST_OVERRIDE=database-operator \
|
|
160
|
+
run Bash "$SUBAGENT_PARENT_ID" 0 '\[admin-tool-gate\] role=subagent .*specialist=database-operator .*tool=Bash decision=allow source=task-subagent\+mcp-specialist' \
|
|
161
|
+
"Test 18: combined parent + specialist resolves as task-subagent+mcp-specialist"
|
|
162
|
+
|
|
122
163
|
echo
|
|
123
164
|
echo "──────── pre-tool-use admin-tool-gate test summary ────────"
|
|
124
165
|
echo "PASS: $PASS"
|
|
@@ -9,6 +9,27 @@
|
|
|
9
9
|
: "${MAXY_HOOK_BUFFER_DIR:=$HOME/.maxy-code/logs/hook-decisions}"
|
|
10
10
|
: "${MAXY_HOOK_SERVER_LOG:=$HOME/.maxy-code/logs/server.log}"
|
|
11
11
|
: "${MAXY_HOOK_STDERR_CAP:=4096}"
|
|
12
|
+
# Task 563 — mkdir-based mutex serialises emitter append vs drainer rename.
|
|
13
|
+
# 100 attempts × 10ms = ~1s ceiling. Portable across Linux (Pi) and macOS
|
|
14
|
+
# (tests) without flock(1).
|
|
15
|
+
: "${MAXY_HOOK_LOCK_ATTEMPTS:=100}"
|
|
16
|
+
: "${MAXY_HOOK_LOCK_SLEEP:=0.01}"
|
|
17
|
+
|
|
18
|
+
# _hook_acquire_lock <lockdir> — returns 0 on success, 1 on timeout.
|
|
19
|
+
_hook_acquire_lock() {
|
|
20
|
+
local lockdir="$1" i=0
|
|
21
|
+
while ! mkdir "$lockdir" 2>/dev/null; do
|
|
22
|
+
i=$((i+1))
|
|
23
|
+
[[ "$i" -ge "$MAXY_HOOK_LOCK_ATTEMPTS" ]] && return 1
|
|
24
|
+
sleep "$MAXY_HOOK_LOCK_SLEEP" 2>/dev/null || true
|
|
25
|
+
done
|
|
26
|
+
return 0
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# _hook_release_lock <lockdir>
|
|
30
|
+
_hook_release_lock() {
|
|
31
|
+
rmdir "$1" 2>/dev/null || true
|
|
32
|
+
}
|
|
12
33
|
|
|
13
34
|
# hook_emit_decision <agentId> <hookName> <toolName> <decision> <reason>
|
|
14
35
|
# <stderrPayload> <exitCode> <durationMs>
|
|
@@ -52,12 +73,21 @@ PY
|
|
|
52
73
|
|
|
53
74
|
mkdir -p "$MAXY_HOOK_BUFFER_DIR" 2>/dev/null
|
|
54
75
|
local buf="$MAXY_HOOK_BUFFER_DIR/$agentId.jsonl"
|
|
76
|
+
local lock="${buf}.lock"
|
|
77
|
+
if ! _hook_acquire_lock "$lock"; then
|
|
78
|
+
mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
|
|
79
|
+
printf '[hook-propagate-error] reason=lock-timeout agentId=%s hook=%s\n' \
|
|
80
|
+
"$agentId" "$hookName" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null
|
|
81
|
+
return 0
|
|
82
|
+
fi
|
|
55
83
|
if ! printf '%s\n' "$rec" >>"$buf" 2>/dev/null; then
|
|
84
|
+
_hook_release_lock "$lock"
|
|
56
85
|
mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
|
|
57
86
|
printf '[hook-propagate-error] reason=buffer-write agentId=%s hook=%s\n' \
|
|
58
87
|
"$agentId" "$hookName" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null
|
|
59
88
|
return 0
|
|
60
89
|
fi
|
|
90
|
+
_hook_release_lock "$lock"
|
|
61
91
|
|
|
62
92
|
local bytesIn="${#stderrPayload}"
|
|
63
93
|
local bytesOut="${#rec}"
|
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Task 560 — PostToolUse hook scoped to tool_name=Agent in the admin role.
|
|
3
|
-
# Drains
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
3
|
+
# Drains every subagent hook-decision buffer in the dir, prints each record
|
|
4
|
+
# as a [hook-propagate] line to stdout (Claude Code attaches the stdout to
|
|
5
|
+
# the parent JSONL as a hook_success attachment), and emits one
|
|
6
|
+
# [hook-propagate-census] line. N != M in the census is the propagation
|
|
7
|
+
# regression signal.
|
|
8
|
+
#
|
|
9
|
+
# Task 563 — concurrency hardening:
|
|
10
|
+
# - Atomic claim before read. Each buffer is renamed under a mkdir mutex
|
|
11
|
+
# to a per-fire `claimed/<basename>-<epoch>-<pid>.jsonl` path. The lock
|
|
12
|
+
# is held only for the rename syscall; reading happens against the
|
|
13
|
+
# renamed inode, which no emitter can address. The race window between
|
|
14
|
+
# the drainer's read and the buffer rotation is closed.
|
|
15
|
+
# - No cursor. The cursor file was load-bearing only because consumed
|
|
16
|
+
# buffers could be re-seen; with atomic claim the file's presence in
|
|
17
|
+
# the directory is the unambiguous signal. Dropping the cursor closes
|
|
18
|
+
# two further races (concurrent-drainer cursor write, shared
|
|
19
|
+
# `.unknown.cursor` across broken-stdin fires).
|
|
20
|
+
# - Unknown-parent observability. When stdin's `session_id` is missing
|
|
21
|
+
# or unparseable, an explicit `[hook-propagate] parentSession=unknown
|
|
22
|
+
# reason=session-id-missing` line is emitted to both stdout and
|
|
23
|
+
# server.log so the broken-stdin path is no longer silent.
|
|
8
24
|
|
|
9
25
|
set -uo pipefail
|
|
10
26
|
|
|
11
27
|
: "${MAXY_HOOK_BUFFER_DIR:=$HOME/.maxy-code/logs/hook-decisions}"
|
|
12
28
|
: "${MAXY_HOOK_SERVER_LOG:=$HOME/.maxy-code/logs/server.log}"
|
|
29
|
+
: "${MAXY_HOOK_LOCK_ATTEMPTS:=100}"
|
|
30
|
+
: "${MAXY_HOOK_LOCK_SLEEP:=0.01}"
|
|
13
31
|
|
|
14
32
|
if [ -t 0 ]; then
|
|
15
33
|
# No stdin — no correlation possible. Fail-open.
|
|
@@ -19,61 +37,95 @@ INPUT=$(cat)
|
|
|
19
37
|
PARENT_SESSION=$(printf '%s' "$INPUT" | python3 -c '
|
|
20
38
|
import sys, json
|
|
21
39
|
try:
|
|
22
|
-
|
|
40
|
+
sid = json.load(sys.stdin).get("session_id") or ""
|
|
41
|
+
print(sid if sid else "unknown")
|
|
23
42
|
except Exception:
|
|
24
43
|
print("unknown")
|
|
25
44
|
' 2>/dev/null || echo "unknown")
|
|
26
45
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
if [[ "$PARENT_SESSION" == "unknown" ]]; then
|
|
47
|
+
# Conservative play (Task 563 brief option C): emit observability and
|
|
48
|
+
# exit without draining. A broken-stdin fire has no business processing
|
|
49
|
+
# buffers — the next valid parent fire will pick them up.
|
|
50
|
+
mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
|
|
51
|
+
printf '[hook-propagate] parentSession=unknown reason=session-id-missing\n'
|
|
52
|
+
printf '[hook-propagate] parentSession=unknown reason=session-id-missing\n' \
|
|
53
|
+
>>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true
|
|
54
|
+
exit 0
|
|
34
55
|
fi
|
|
35
|
-
NOW_EPOCH=$(date +%s)
|
|
36
56
|
|
|
57
|
+
mkdir -p "$MAXY_HOOK_BUFFER_DIR/claimed" "$MAXY_HOOK_BUFFER_DIR/consumed" 2>/dev/null
|
|
58
|
+
|
|
59
|
+
# Sweep orphaned `.<session>.cursor` files left behind by an older
|
|
60
|
+
# version of this script. The cursor mechanism is gone; without this
|
|
61
|
+
# they would persist forever as dir noise.
|
|
62
|
+
rm -f "$MAXY_HOOK_BUFFER_DIR"/.*.cursor 2>/dev/null || true
|
|
63
|
+
|
|
64
|
+
NOW_EPOCH=$(date +%s)
|
|
37
65
|
N_OBSERVED=0
|
|
38
66
|
N_EMITTED=0
|
|
67
|
+
|
|
68
|
+
_drain_acquire_lock() {
|
|
69
|
+
local lockdir="$1" i=0
|
|
70
|
+
while ! mkdir "$lockdir" 2>/dev/null; do
|
|
71
|
+
i=$((i+1))
|
|
72
|
+
[[ "$i" -ge "$MAXY_HOOK_LOCK_ATTEMPTS" ]] && return 1
|
|
73
|
+
sleep "$MAXY_HOOK_LOCK_SLEEP" 2>/dev/null || true
|
|
74
|
+
done
|
|
75
|
+
return 0
|
|
76
|
+
}
|
|
77
|
+
|
|
39
78
|
shopt -s nullglob
|
|
40
79
|
for buf in "$MAXY_HOOK_BUFFER_DIR"/*.jsonl; do
|
|
41
80
|
[[ -f "$buf" ]] || continue
|
|
42
81
|
case "$(basename "$buf")" in
|
|
43
82
|
.*) continue;;
|
|
44
83
|
esac
|
|
45
|
-
|
|
46
|
-
if
|
|
84
|
+
lock="${buf}.lock"
|
|
85
|
+
if ! _drain_acquire_lock "$lock"; then
|
|
86
|
+
mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
|
|
87
|
+
printf '[hook-propagate-error] reason=lock-timeout parentSession=%s buffer=%s\n' \
|
|
88
|
+
"$PARENT_SESSION" "$(basename "$buf")" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true
|
|
47
89
|
continue
|
|
48
90
|
fi
|
|
49
|
-
#
|
|
50
|
-
#
|
|
91
|
+
# Re-check existence under lock — a peer drainer may have claimed it
|
|
92
|
+
# between our `for` iteration and our lock acquisition.
|
|
93
|
+
if [[ ! -f "$buf" ]]; then
|
|
94
|
+
rmdir "$lock" 2>/dev/null || true
|
|
95
|
+
continue
|
|
96
|
+
fi
|
|
97
|
+
CLAIMED="$MAXY_HOOK_BUFFER_DIR/claimed/$(basename "$buf" .jsonl)-${NOW_EPOCH}-$$.jsonl"
|
|
98
|
+
if ! mv "$buf" "$CLAIMED" 2>/dev/null; then
|
|
99
|
+
rmdir "$lock" 2>/dev/null || true
|
|
100
|
+
mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
|
|
101
|
+
printf '[hook-propagate-error] reason=claim-mv parentSession=%s buffer=%s\n' \
|
|
102
|
+
"$PARENT_SESSION" "$(basename "$buf")" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true
|
|
103
|
+
continue
|
|
104
|
+
fi
|
|
105
|
+
rmdir "$lock" 2>/dev/null || true
|
|
106
|
+
|
|
107
|
+
# Ground-truth observed count: non-empty lines in the claimed file. The
|
|
108
|
+
# printf-success N_EMITTED is incremented independently below, so a
|
|
51
109
|
# printf failure (SIGPIPE on a closed downstream pipe, EIO on stdout)
|
|
52
110
|
# produces N_OBSERVED > N_EMITTED — the propagation regression signal
|
|
53
111
|
# the census promises. The two counters must not be bumped together.
|
|
54
|
-
BUF_LINES=$(grep -c . "$
|
|
112
|
+
BUF_LINES=$(grep -c . "$CLAIMED" 2>/dev/null || echo 0)
|
|
55
113
|
N_OBSERVED=$((N_OBSERVED + BUF_LINES))
|
|
56
114
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
57
115
|
[[ -z "$line" ]] && continue
|
|
58
116
|
if printf '[hook-propagate] %s\n' "$line" 2>/dev/null; then
|
|
59
117
|
N_EMITTED=$((N_EMITTED+1))
|
|
60
118
|
fi
|
|
61
|
-
done <"$
|
|
62
|
-
TARGET="$MAXY_HOOK_BUFFER_DIR/consumed/$(basename "$
|
|
63
|
-
if ! mv "$
|
|
119
|
+
done <"$CLAIMED"
|
|
120
|
+
TARGET="$MAXY_HOOK_BUFFER_DIR/consumed/$(basename "$CLAIMED")"
|
|
121
|
+
if ! mv "$CLAIMED" "$TARGET" 2>/dev/null; then
|
|
64
122
|
mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
|
|
65
123
|
printf '[hook-propagate-error] reason=consumed-mv parentSession=%s buffer=%s\n' \
|
|
66
|
-
"$PARENT_SESSION" "$(basename "$
|
|
124
|
+
"$PARENT_SESSION" "$(basename "$CLAIMED")" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true
|
|
67
125
|
fi
|
|
68
126
|
done
|
|
69
127
|
shopt -u nullglob
|
|
70
128
|
|
|
71
|
-
if ! printf '%s' "$NOW_EPOCH" >"$CURSOR_FILE" 2>/dev/null; then
|
|
72
|
-
mkdir -p "$(dirname "$MAXY_HOOK_SERVER_LOG")" 2>/dev/null
|
|
73
|
-
printf '[hook-propagate-error] reason=cursor-write parentSession=%s cursor=%s\n' \
|
|
74
|
-
"$PARENT_SESSION" "$CURSOR_FILE" >>"$MAXY_HOOK_SERVER_LOG" 2>/dev/null || true
|
|
75
|
-
fi
|
|
76
|
-
|
|
77
129
|
printf '[hook-propagate-census] parentSession=%s subagentHooksObserved=%d attachmentsEmitted=%d\n' \
|
|
78
130
|
"$PARENT_SESSION" "$N_OBSERVED" "$N_EMITTED"
|
|
79
131
|
|
|
@@ -43,24 +43,25 @@ except Exception:
|
|
|
43
43
|
# ---------------------------------------------------------------------------
|
|
44
44
|
if [ "$AGENT_TYPE" = "admin" ]; then
|
|
45
45
|
|
|
46
|
-
# ── Orchestration-only authoring + research block (Task 529, 559)
|
|
46
|
+
# ── Orchestration-only authoring + research block (Task 529, 559, 566) ───
|
|
47
47
|
# The admin seat is orchestration + clarification + delivery only.
|
|
48
48
|
# Write/Edit/MultiEdit/Bash/WebFetch/WebSearch are specialist-owned —
|
|
49
49
|
# content-producer for authoring, research-assistant for research, the
|
|
50
50
|
# shell tools for whichever specialist owns the workflow that needs them.
|
|
51
51
|
# In-window Task subagents must keep these tools (Task 222 regression
|
|
52
|
-
# class).
|
|
52
|
+
# class). Maxy MCP `Agent`-spawned specialists are separate top-level
|
|
53
|
+
# `claude rc` processes — they have no `parent_tool_use_id` but pty-spawner
|
|
54
|
+
# stamps `MAXY_SPECIALIST=<name>` on the child env (pty-spawner.ts:1601).
|
|
53
55
|
#
|
|
54
|
-
# Discriminator (
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
# hooks, so the glob never matched and every specialist subagent Bash
|
|
60
|
-
# was blocked with the admin orchestration-only wording.
|
|
56
|
+
# Discriminator (union of positive markers, never path-shape inference):
|
|
57
|
+
# parent_tool_use_id non-empty → in-window Task subagent (allow)
|
|
58
|
+
# $MAXY_SPECIALIST non-empty → maxy MCP Agent specialist (allow)
|
|
59
|
+
# $MAXY_SESSION_ROLE = admin → admin-direct (block, orchestration-only)
|
|
60
|
+
# none of the above → fail closed (missing-marker)
|
|
61
61
|
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
62
|
+
# Adding a future spawn shape that lands neither marker surfaces as a loud
|
|
63
|
+
# `reason=missing-marker` block, not a silent passthrough or a silent
|
|
64
|
+
# admin-direct block on legitimate specialist work.
|
|
64
65
|
case "$TOOL_NAME" in
|
|
65
66
|
Write|Edit|MultiEdit|Bash|WebFetch|WebSearch)
|
|
66
67
|
PARENT_TOOL_USE_ID=$(echo "$INPUT" | python3 -c '
|
|
@@ -72,11 +73,18 @@ except Exception:
|
|
|
72
73
|
print("")
|
|
73
74
|
' 2>/dev/null)
|
|
74
75
|
SPECIALIST="${MAXY_SPECIALIST:-}"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
SESSION_ROLE="${MAXY_SESSION_ROLE:-}"
|
|
77
|
+
if [ -n "$PARENT_TOOL_USE_ID" ] || [ -n "$SPECIALIST" ]; then
|
|
78
|
+
# Subagent — passthrough. Tool name resolves on the subagent's
|
|
79
|
+
# own frontmatter `tools:` allowlist downstream.
|
|
80
|
+
SOURCE="task-subagent"
|
|
81
|
+
if [ -n "$SPECIALIST" ] && [ -n "$PARENT_TOOL_USE_ID" ]; then
|
|
82
|
+
SOURCE="task-subagent+mcp-specialist"
|
|
83
|
+
elif [ -n "$SPECIALIST" ]; then
|
|
84
|
+
SOURCE="mcp-specialist"
|
|
85
|
+
fi
|
|
86
|
+
echo "[admin-tool-gate] role=subagent parent_tool_use_id=${PARENT_TOOL_USE_ID:--} specialist=${SPECIALIST:--} session_role=${SESSION_ROLE:--} tool=${TOOL_NAME} decision=allow source=${SOURCE}" >&2
|
|
87
|
+
elif [ "$SESSION_ROLE" = "admin" ]; then
|
|
80
88
|
# Admin direct invocation — refuse with dispatch hint.
|
|
81
89
|
case "$TOOL_NAME" in
|
|
82
90
|
Write|Edit|MultiEdit)
|
|
@@ -89,11 +97,20 @@ except Exception:
|
|
|
89
97
|
OWNER="research-assistant"
|
|
90
98
|
;;
|
|
91
99
|
esac
|
|
92
|
-
echo "[admin-tool-gate] role=admin parent_tool_use_id=- specialist
|
|
100
|
+
echo "[admin-tool-gate] role=admin parent_tool_use_id=- specialist=- session_role=admin tool=${TOOL_NAME} decision=block reason=orchestration-only owner=${OWNER}" >&2
|
|
93
101
|
hook_block_with_emit "$SESSION_ID" "admin-tool-gate" "${TOOL_NAME}" \
|
|
94
102
|
"orchestration-only" \
|
|
95
103
|
"Blocked: \`${TOOL_NAME}\` is not callable from the admin seat. The admin seat is orchestration + clarification + delivery only — authoring, shell work, and web research are owned by specialists.
|
|
96
104
|
Dispatch the work to ${OWNER} via the Agent tool."
|
|
105
|
+
else
|
|
106
|
+
# No positive marker — fail closed. A new spawn shape that lands
|
|
107
|
+
# neither parent_tool_use_id nor MAXY_SPECIALIST and is not the
|
|
108
|
+
# admin seat should surface visibly, not silently pass or silently
|
|
109
|
+
# block under the wrong reason.
|
|
110
|
+
echo "[admin-tool-gate] role=unknown parent_tool_use_id=- specialist=- session_role=${SESSION_ROLE:--} tool=${TOOL_NAME} decision=block reason=missing-marker" >&2
|
|
111
|
+
hook_block_with_emit "$SESSION_ID" "admin-tool-gate" "${TOOL_NAME}" \
|
|
112
|
+
"missing-marker" \
|
|
113
|
+
"Blocked: \`${TOOL_NAME}\` call carries no positive role marker (no parent_tool_use_id, no MAXY_SPECIALIST, MAXY_SESSION_ROLE is not 'admin'). Failing closed so a new spawn shape cannot silently bypass orchestration gating."
|
|
97
114
|
fi
|
|
98
115
|
;;
|
|
99
116
|
esac
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities-here.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/capabilities-here.test.ts"],"names":[],"mappings":""}
|