@rubytech/create-maxy-code 0.1.216 → 0.1.221

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.
Files changed (82) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/mcp-eager/dist/index.d.ts +0 -55
  3. package/payload/platform/lib/mcp-eager/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/mcp-eager/dist/index.js +0 -76
  5. package/payload/platform/lib/mcp-eager/dist/index.js.map +1 -1
  6. package/payload/platform/lib/mcp-eager/src/index.ts +0 -101
  7. package/payload/platform/plugins/admin/PLUGIN.md +2 -3
  8. package/payload/platform/plugins/admin/hooks/__tests__/hook-emit-stale-lock-ttl.test.sh +102 -0
  9. package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +1 -2
  10. package/payload/platform/plugins/admin/hooks/lib/hook-emit.sh +32 -3
  11. package/payload/platform/plugins/admin/hooks/post-tool-use-agent.sh +20 -1
  12. package/payload/platform/plugins/admin/mcp/dist/index.js +2 -2
  13. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  14. package/payload/platform/plugins/admin/skills/admin-user-management/SKILL.md +1 -1
  15. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +18 -18
  16. package/payload/platform/plugins/admin/skills/plugin-management/SKILL.md +2 -2
  17. package/payload/platform/plugins/browser/mcp/dist/index.js +19 -19
  18. package/payload/platform/plugins/browser/mcp/dist/index.js.map +1 -1
  19. package/payload/platform/plugins/docs/references/platform.md +10 -17
  20. package/payload/platform/plugins/docs/references/troubleshooting.md +7 -0
  21. package/payload/platform/plugins/email/mcp/dist/index.js +12 -12
  22. package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
  23. package/payload/platform/plugins/memory/mcp/dist/index.js +8 -8
  24. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  25. package/payload/platform/plugins/memory/references/graph-primitives.md +2 -2
  26. package/payload/platform/plugins/url-get/mcp/dist/index.js +2 -2
  27. package/payload/platform/plugins/url-get/mcp/dist/index.js.map +1 -1
  28. package/payload/platform/scripts/check-no-esm-require.mjs +3 -0
  29. package/payload/platform/scripts/setup-account.sh +0 -15
  30. package/payload/platform/services/claude-session-manager/dist/auth-snapshot.d.ts +4 -0
  31. package/payload/platform/services/claude-session-manager/dist/auth-snapshot.d.ts.map +1 -0
  32. package/payload/platform/services/claude-session-manager/dist/auth-snapshot.js +50 -0
  33. package/payload/platform/services/claude-session-manager/dist/auth-snapshot.js.map +1 -0
  34. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +0 -14
  35. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  36. package/payload/platform/services/claude-session-manager/dist/http-server.js +44 -71
  37. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  38. package/payload/platform/services/claude-session-manager/dist/index.js +12 -31
  39. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  40. package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts +3 -0
  41. package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts.map +1 -1
  42. package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js +55 -1
  43. package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js.map +1 -1
  44. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +1 -1
  45. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  46. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +27 -20
  47. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  48. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +23 -17
  49. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  50. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +94 -73
  51. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  52. package/payload/platform/services/claude-session-manager/dist/rc-life.d.ts +4 -0
  53. package/payload/platform/services/claude-session-manager/dist/rc-life.d.ts.map +1 -0
  54. package/payload/platform/services/claude-session-manager/dist/rc-life.js +16 -0
  55. package/payload/platform/services/claude-session-manager/dist/rc-life.js.map +1 -0
  56. package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts +12 -46
  57. package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts.map +1 -1
  58. package/payload/platform/services/claude-session-manager/dist/systemd-scope.js +24 -73
  59. package/payload/platform/services/claude-session-manager/dist/systemd-scope.js.map +1 -1
  60. package/payload/platform/templates/agents/admin/IDENTITY.md +3 -3
  61. package/payload/premium-plugins/writer-craft/mcp/dist/index.d.ts.map +1 -1
  62. package/payload/premium-plugins/writer-craft/mcp/dist/index.js +4 -5
  63. package/payload/premium-plugins/writer-craft/mcp/dist/index.js.map +1 -1
  64. package/payload/premium-plugins/writer-craft/mcp/src/index.ts +4 -5
  65. package/payload/server/public/assets/AdminShell-BcxEZnpu.js +1 -0
  66. package/payload/server/public/assets/{admin-DjGGOx2K.js → admin-DMo5DWKH.js} +1 -1
  67. package/payload/server/public/assets/{data-WBEvtRV-.js → data-D6IQdm0R.js} +1 -1
  68. package/payload/server/public/assets/{graph-DsOLsIPf.js → graph-PkZKVkWD.js} +1 -1
  69. package/payload/server/public/data.html +2 -2
  70. package/payload/server/public/graph.html +2 -2
  71. package/payload/server/public/index.html +2 -2
  72. package/payload/server/server.js +49 -12
  73. package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-admin-tool-gate.test.sh +0 -169
  74. package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-base64-guard.test.sh +0 -204
  75. package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-memory-write-passthrough.test.sh +0 -118
  76. package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +0 -409
  77. package/payload/premium-plugins/writer-craft/lib/mcp-eager/dist/index.d.ts +0 -116
  78. package/payload/premium-plugins/writer-craft/lib/mcp-eager/dist/index.d.ts.map +0 -1
  79. package/payload/premium-plugins/writer-craft/lib/mcp-eager/dist/index.js +0 -125
  80. package/payload/premium-plugins/writer-craft/lib/mcp-eager/dist/index.js.map +0 -1
  81. package/payload/premium-plugins/writer-craft/lib/mcp-eager/package.json +0 -7
  82. package/payload/server/public/assets/AdminShell-CPWbJ_I8.js +0 -1
@@ -824,13 +824,10 @@ import { monitorEventLoopDelay } from "perf_hooks";
824
824
  // app/lib/agent-slug-pattern.ts
825
825
  var AGENT_SLUG_PATTERN = /^\/([a-z][a-z0-9-]{2,49})$/;
826
826
 
827
- // server/routes/health.ts
828
- import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
829
- import { createConnection as createConnection2 } from "net";
830
-
831
827
  // app/lib/claude-auth.ts
832
828
  import { readFileSync, writeFileSync } from "fs";
833
829
  import { sep } from "path";
830
+ import { lock } from "proper-lockfile";
834
831
  if (!CLAUDE_CREDENTIALS_FILE.startsWith(MAXY_DIR + sep)) {
835
832
  console.error(
836
833
  `[claude-auth] WARN cross-brand-path-detected resolved=${CLAUDE_CREDENTIALS_FILE} brand=${BRAND_NAME} maxyDir=${MAXY_DIR}`
@@ -929,11 +926,39 @@ function logRefreshOutcome(outcome) {
929
926
  );
930
927
  }
931
928
  async function doRefresh() {
929
+ const pre = readCredentials();
930
+ if (!pre) return { status: "missing" };
931
+ if (!pre.refreshToken) return { status: "dead", expiresAt: pre.expiresAt };
932
+ let release = null;
933
+ try {
934
+ release = await lock(CLAUDE_CREDENTIALS_FILE, {
935
+ retries: { retries: 8, minTimeout: 100, maxTimeout: 1e3, factor: 1.5 },
936
+ stale: 3e4
937
+ });
938
+ } catch (err) {
939
+ console.error(
940
+ `[auth-refresh] op=lock-acquire-failed err=${err instanceof Error ? err.message : String(err)}`
941
+ );
942
+ logRefreshOutcome("fail-network");
943
+ return { status: "expired", expiresAt: pre.expiresAt };
944
+ }
945
+ console.log(`[auth-refresh] op=lock-acquired pid=${process.pid} path=${CLAUDE_CREDENTIALS_FILE}`);
946
+ try {
947
+ return await doRefreshLocked();
948
+ } finally {
949
+ await release().catch(() => {
950
+ });
951
+ }
952
+ }
953
+ async function doRefreshLocked() {
932
954
  const creds = readCredentials();
933
955
  if (!creds) return { status: "missing" };
934
956
  if (!creds.refreshToken) return { status: "dead", expiresAt: creds.expiresAt };
935
957
  const now = Date.now();
936
958
  if (creds.expiresAt - now > EXPIRING_THRESHOLD_MS) {
959
+ console.log(
960
+ `[auth-refresh] op=skipped-fresh expiresAt=${new Date(creds.expiresAt).toISOString()}`
961
+ );
937
962
  return { status: "ok", expiresAt: creds.expiresAt };
938
963
  }
939
964
  let res;
@@ -989,13 +1014,26 @@ async function doRefresh() {
989
1014
  expiresIn: typeof expiresIn === "number" && expiresIn > 0 ? expiresIn : void 0
990
1015
  });
991
1016
  if (newExpiresAt) {
992
- console.log(`[auth-refresh] success, expires ${new Date(newExpiresAt).toISOString()}`);
1017
+ console.log(`[auth-refresh] op=renewed expiresAt=${new Date(newExpiresAt).toISOString()}`);
993
1018
  } else {
994
- console.warn("[auth-refresh] write-back failed");
1019
+ console.warn("[auth-refresh] op=renewed-write-back-failed");
995
1020
  }
996
1021
  logRefreshOutcome("ok");
997
1022
  return { status: "ok", expiresAt: newExpiresAt ?? Date.now() + 3600 * 1e3 };
998
1023
  }
1024
+ function startAuthHealthHeartbeat(intervalMs = 5 * 60 * 1e3) {
1025
+ const tick = () => {
1026
+ const h = computeAuthHealth();
1027
+ const expiresIn = h.expiresAt != null ? Math.round((h.expiresAt - Date.now()) / 1e3) : null;
1028
+ console.log(
1029
+ `[auth-health] brand=${BRAND_NAME} status=${h.status} expiresIn=${expiresIn != null ? expiresIn + "s" : "n/a"}`
1030
+ );
1031
+ };
1032
+ tick();
1033
+ const handle = setInterval(tick, intervalMs);
1034
+ if (typeof handle.unref === "function") handle.unref();
1035
+ return () => clearInterval(handle);
1036
+ }
999
1037
  async function ensureAuth() {
1000
1038
  const health = assessAuthHealth();
1001
1039
  if (health.status === "ok" || health.status === "missing" || health.status === "dead") {
@@ -1004,6 +1042,10 @@ async function ensureAuth() {
1004
1042
  return refreshAccessToken();
1005
1043
  }
1006
1044
 
1045
+ // server/routes/health.ts
1046
+ import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
1047
+ import { createConnection as createConnection2 } from "net";
1048
+
1007
1049
  // app/lib/network.ts
1008
1050
  import { networkInterfaces } from "os";
1009
1051
  function getLanIp() {
@@ -12157,12 +12199,6 @@ app25.post("/", requireAdminSession, async (c) => {
12157
12199
  const payload = {};
12158
12200
  if (typeof body.sessionId === "string" && body.sessionId.length > 0) payload.sessionId = body.sessionId;
12159
12201
  if (typeof body.name === "string" && body.name.length > 0) payload.name = body.name;
12160
- if (body.spawnPath !== void 0) {
12161
- if (body.spawnPath !== "node-pty" && body.spawnPath !== "script-scope") {
12162
- return c.json({ error: `spawnPath must be 'node-pty' or 'script-scope'` }, 400);
12163
- }
12164
- payload.spawnPath = body.spawnPath;
12165
- }
12166
12202
  let res;
12167
12203
  try {
12168
12204
  res = await fetch(`${managerBase4()}/rc-spawn`, {
@@ -15167,6 +15203,7 @@ var port = requirePortEnv("MAXY_UI_INTERNAL_PORT", { tag: "ui-server" });
15167
15203
  var hostname = process.env.HOSTNAME ?? "127.0.0.1";
15168
15204
  var httpServer = serve({ fetch: app41.fetch, port, hostname });
15169
15205
  console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
15206
+ startAuthHealthHeartbeat();
15170
15207
  {
15171
15208
  const loopHist = monitorEventLoopDelay({ resolution: 50 });
15172
15209
  loopHist.enable();
@@ -1,169 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Task 529 — admin-tool-gate enforces orchestration-only at the PreToolUse
3
- # hook. Admin-direct invocations of Write/Edit/MultiEdit/Bash/WebFetch/
4
- # WebSearch are refused with the dispatch hint; subagents pass through.
5
- #
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.
15
-
16
- set -u
17
-
18
- HOOK="$(cd "$(dirname "$0")/.." && pwd)/pre-tool-use.sh"
19
- if [[ ! -x "$HOOK" ]]; then
20
- echo "FAIL: $HOOK not executable" >&2
21
- exit 1
22
- fi
23
-
24
- TMPFILES=()
25
- cleanup() {
26
- for f in "${TMPFILES[@]:-}"; do
27
- [[ -n "$f" ]] && rm -f "$f" 2>/dev/null || true
28
- done
29
- }
30
- trap cleanup EXIT
31
-
32
- PASS=0; FAIL=0
33
- pass() { PASS=$((PASS+1)); echo "PASS: $1"; }
34
- fail() { FAIL=$((FAIL+1)); echo "FAIL: $1" >&2; }
35
-
36
- # The hook receives the parent session's transcript_path even on subagent
37
- # calls in Claude Code 2.1.x. The fixture mirrors production.
38
- SESSION_TRANSCRIPT="/tmp/sess/44283ae1-2b82-4bd7-b8af-99a11141ed4b.jsonl"
39
- SUBAGENT_PARENT_ID="toolu_01QFC4W32wFUyHy6WJMNjHf6"
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
46
- run() {
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-}"
50
- local input_json
51
- input_json=$(python3 -c '
52
- import json, sys
53
- payload = {
54
- "hook_event_name": "PreToolUse",
55
- "tool_name": sys.argv[1],
56
- "tool_input": {"file_path": "/tmp/x", "content": "x", "command": "echo hi"},
57
- "transcript_path": sys.argv[2],
58
- }
59
- parent = sys.argv[3]
60
- if parent:
61
- payload["parent_tool_use_id"] = parent
62
- print(json.dumps(payload, separators=(",", ":")))
63
- ' "$tool" "$SESSION_TRANSCRIPT" "$parent_id")
64
- local stdout_file; stdout_file=$(mktemp); TMPFILES+=("$stdout_file")
65
- local stderr_file; stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
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"
70
- local rc=$?
71
- if [[ "$rc" -ne "$expected_rc" ]]; then
72
- fail "$label: expected exit $expected_rc, got $rc. Stderr: $(cat "$stderr_file")"
73
- return
74
- fi
75
- if [[ -n "$expected_pattern" ]] && ! grep -qE "$expected_pattern" "$stderr_file"; then
76
- fail "$label: stderr missing pattern '$expected_pattern'. Got: $(cat "$stderr_file")"
77
- return
78
- fi
79
- pass "$label"
80
- }
81
-
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' \
84
- "Test 1: admin-direct Write rejected with content-producer dispatch hint"
85
- run Edit "" 2 '\[admin-tool-gate\] role=admin .*tool=Edit decision=block.*owner=content-producer' \
86
- "Test 2: admin-direct Edit rejected"
87
- run MultiEdit "" 2 '\[admin-tool-gate\] role=admin .*tool=MultiEdit decision=block.*owner=content-producer' \
88
- "Test 3: admin-direct MultiEdit rejected"
89
- run Bash "" 2 '\[admin-tool-gate\] role=admin .*tool=Bash decision=block' \
90
- "Test 4: admin-direct Bash rejected"
91
- run WebFetch "" 2 '\[admin-tool-gate\] role=admin .*tool=WebFetch decision=block.*owner=research-assistant' \
92
- "Test 5: admin-direct WebFetch rejected"
93
- run WebSearch "" 2 '\[admin-tool-gate\] role=admin .*tool=WebSearch decision=block.*owner=research-assistant' \
94
- "Test 6: admin-direct WebSearch rejected"
95
-
96
- # ── In-window subagent passthrough (parent_tool_use_id non-empty) ───────
97
- run Write "$SUBAGENT_PARENT_ID" 0 "\[admin-tool-gate\] role=subagent parent_tool_use_id=${SUBAGENT_PARENT_ID} .*tool=Write decision=allow" \
98
- "Test 7: subagent Write passes through"
99
- run Edit "$SUBAGENT_PARENT_ID" 0 '\[admin-tool-gate\] role=subagent .*tool=Edit decision=allow' \
100
- "Test 8: subagent Edit passes through"
101
- run Bash "$SUBAGENT_PARENT_ID" 0 '\[admin-tool-gate\] role=subagent .*tool=Bash decision=allow' \
102
- "Test 9: subagent Bash passes through (Task 559 regression — coding-assistant git pull)"
103
- run WebFetch "$SUBAGENT_PARENT_ID" 0 '\[admin-tool-gate\] role=subagent .*tool=WebFetch decision=allow' \
104
- "Test 10: subagent WebFetch passes through"
105
- run MultiEdit "$SUBAGENT_PARENT_ID" 0 '\[admin-tool-gate\] role=subagent .*tool=MultiEdit decision=allow' \
106
- "Test 11: subagent MultiEdit passes through"
107
-
108
- # ── Unrelated tools untouched ───────────────────────────────────────────
109
- run Read "" 0 '' \
110
- "Test 12: admin-direct Read passes (Read is admin-keep-list)"
111
-
112
- # ── Task 560: admin-direct block emits propagation record ──────────────
113
- # Admin-direct path: MAXY_SESSION_ROLE=admin, no MAXY_SPECIALIST, no parent.
114
- PROP_TMPDIR=$(mktemp -d); TMPFILES+=("$PROP_TMPDIR")
115
- export MAXY_HOOK_BUFFER_DIR="$PROP_TMPDIR/buffers"
116
- export MAXY_HOOK_SERVER_LOG="$PROP_TMPDIR/server.log"
117
- SESSION="task560-test-sess"
118
- PROP_INPUT=$(python3 -c '
119
- import json, sys
120
- print(json.dumps({"hook_event_name":"PreToolUse","tool_name":"Bash",
121
- "tool_input":{"command":"ls"},"transcript_path":sys.argv[1],
122
- "session_id":sys.argv[2]}, separators=(",",":")))
123
- ' "$SESSION_TRANSCRIPT" "$SESSION")
124
- printf '%s' "$PROP_INPUT" | \
125
- env -u MAXY_SPECIALIST MAXY_SESSION_ROLE=admin \
126
- bash "$HOOK" admin >/dev/null 2>/dev/null
127
- BUF="$MAXY_HOOK_BUFFER_DIR/$SESSION.jsonl"
128
- if [[ -f "$BUF" ]] && grep -q '"hookName":"admin-tool-gate"' "$BUF" \
129
- && grep -q '"decision":"block"' "$BUF" \
130
- && grep -q '"reason":"orchestration-only"' "$BUF"; then
131
- pass "Test 13b: admin-direct Bash block emits Task 560 propagation record"
132
- else
133
- fail "Test 13b: propagation record missing/malformed — $(cat "$BUF" 2>/dev/null || echo '<no buffer>')"
134
- fi
135
- unset MAXY_HOOK_BUFFER_DIR MAXY_HOOK_SERVER_LOG
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
-
163
- echo
164
- echo "──────── pre-tool-use admin-tool-gate test summary ────────"
165
- echo "PASS: $PASS"
166
- echo "FAIL: $FAIL"
167
-
168
- [[ "$FAIL" -gt 0 ]] && exit 1
169
- exit 0
@@ -1,204 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Regression test for the base64 context-overflow guard.
3
- #
4
- # Covers two PreToolUse rejection paths in pre-tool-use.sh:
5
- #
6
- # 1. Bash producer guard — `tool_input.command` invoking `base64` (encode
7
- # direction) or `xxd -p` is rejected; `base64 -d|-D|--decode` is allowed.
8
- # 2. Write/Edit consumer guard — `tool_input.content` (Write) or
9
- # `tool_input.new_string` (Edit) carrying `data:<mime>;base64,<≥4096 chars>`
10
- # is rejected; small inline data URIs and plain content are allowed.
11
- #
12
- # Plus a fail-open case (malformed stdin → exit 0 silent) to pin the contract
13
- # established by the playwright-file-guard test (terminal-stdin guard + parse-
14
- # error fail-open).
15
-
16
- set -u
17
-
18
- HOOK="$(cd "$(dirname "$0")/.." && pwd)/pre-tool-use.sh"
19
- if [[ ! -x "$HOOK" ]]; then
20
- echo "FAIL: $HOOK not executable" >&2
21
- exit 1
22
- fi
23
-
24
- TMPFILES=()
25
- cleanup_test_state() {
26
- for f in "${TMPFILES[@]:-}"; do
27
- [[ -n "$f" ]] && rm -f "$f" 2>/dev/null || true
28
- done
29
- }
30
- trap cleanup_test_state EXIT
31
-
32
- PASS=0
33
- FAIL=0
34
- pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
35
- fail() { echo "FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
36
-
37
- # Helper: run hook with Bash tool_input.command and assert exit code + stderr.
38
- run_bash() {
39
- local command_text="$1"; local expected_rc="$2"; local stderr_pattern="$3"; local label="$4"
40
- local input_json
41
- # Build the input JSON via python3 so command_text with quotes / specials is safe.
42
- input_json=$(python3 -c '
43
- import json, sys
44
- print(json.dumps({"hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": {"command": sys.argv[1]}, "transcript_path": "/tmp/sess/parent.jsonl", "parent_tool_use_id": "toolu_test_subagent"}, separators=(",", ":")))
45
- ' "$command_text")
46
- local stdout_file; stdout_file=$(mktemp); TMPFILES+=("$stdout_file")
47
- local stderr_file; stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
48
- printf '%s' "$input_json" | bash "$HOOK" admin >"$stdout_file" 2>"$stderr_file"
49
- local rc=$?
50
- if [[ "$rc" -ne "$expected_rc" ]]; then
51
- fail "$label: expected exit $expected_rc, got $rc. Stderr: $(cat "$stderr_file")"
52
- return
53
- fi
54
- if [[ -n "$stderr_pattern" ]] && ! grep -qE "$stderr_pattern" "$stderr_file"; then
55
- fail "$label: stderr missing pattern '$stderr_pattern'. Got: $(cat "$stderr_file")"
56
- return
57
- fi
58
- pass "$label"
59
- }
60
-
61
- # Helper: run hook with Write tool_input.content and assert exit code + stderr.
62
- run_write() {
63
- local content="$1"; local expected_rc="$2"; local stderr_pattern="$3"; local label="$4"
64
- local input_json
65
- input_json=$(python3 -c '
66
- import json, sys
67
- print(json.dumps({"hook_event_name": "PreToolUse", "tool_name": "Write", "tool_input": {"file_path": "/tmp/test.html", "content": sys.argv[1]}, "transcript_path": "/tmp/sess/parent.jsonl", "parent_tool_use_id": "toolu_test_subagent"}, separators=(",", ":")))
68
- ' "$content")
69
- local stdout_file; stdout_file=$(mktemp); TMPFILES+=("$stdout_file")
70
- local stderr_file; stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
71
- printf '%s' "$input_json" | bash "$HOOK" admin >"$stdout_file" 2>"$stderr_file"
72
- local rc=$?
73
- if [[ "$rc" -ne "$expected_rc" ]]; then
74
- fail "$label: expected exit $expected_rc, got $rc. Stderr: $(cat "$stderr_file")"
75
- return
76
- fi
77
- if [[ -n "$stderr_pattern" ]] && ! grep -qE "$stderr_pattern" "$stderr_file"; then
78
- fail "$label: stderr missing pattern '$stderr_pattern'. Got: $(cat "$stderr_file")"
79
- return
80
- fi
81
- pass "$label"
82
- }
83
-
84
- # Helper: run hook with Edit tool_input.new_string and assert exit code.
85
- run_edit() {
86
- local new_string="$1"; local expected_rc="$2"; local stderr_pattern="$3"; local label="$4"
87
- local input_json
88
- input_json=$(python3 -c '
89
- import json, sys
90
- print(json.dumps({"hook_event_name": "PreToolUse", "tool_name": "Edit", "tool_input": {"file_path": "/tmp/test.html", "old_string": "OLD", "new_string": sys.argv[1]}, "transcript_path": "/tmp/sess/parent.jsonl", "parent_tool_use_id": "toolu_test_subagent"}, separators=(",", ":")))
91
- ' "$new_string")
92
- local stdout_file; stdout_file=$(mktemp); TMPFILES+=("$stdout_file")
93
- local stderr_file; stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
94
- printf '%s' "$input_json" | bash "$HOOK" admin >"$stdout_file" 2>"$stderr_file"
95
- local rc=$?
96
- if [[ "$rc" -ne "$expected_rc" ]]; then
97
- fail "$label: expected exit $expected_rc, got $rc. Stderr: $(cat "$stderr_file")"
98
- return
99
- fi
100
- if [[ -n "$stderr_pattern" ]] && ! grep -qE "$stderr_pattern" "$stderr_file"; then
101
- fail "$label: stderr missing pattern '$stderr_pattern'. Got: $(cat "$stderr_file")"
102
- return
103
- fi
104
- pass "$label"
105
- }
106
-
107
- # Generate a base64-character blob >= 4096 chars (data-URI body trigger).
108
- LARGE_B64=$(python3 -c "print('A' * 5000)")
109
-
110
- # ───────── Bash producer guard ──────────────────────────────────────────────
111
- run_bash "echo hello world" 0 "" \
112
- "Test 1: bare Bash command (no base64) allowed"
113
-
114
- run_bash "ls -la" 0 "" \
115
- "Test 2: ls -la (no base64 token) allowed"
116
-
117
- run_bash "base64 /tmp/foo.png" 2 '\[pre-tool-use\] guard=base64-tool-result.*tool=Bash.*reason=base64-encoder.*action=reject' \
118
- "Test 3: 'base64 file' (encode) rejected"
119
-
120
- run_bash "cat foo.png | base64" 2 '\[pre-tool-use\] guard=base64-tool-result.*action=reject' \
121
- "Test 4: 'cat | base64' (encode pipeline) rejected"
122
-
123
- run_bash "cat foo.png|base64 -w0" 2 '\[pre-tool-use\] guard=base64-tool-result.*action=reject' \
124
- "Test 5: 'base64 -w0' (encode with line-wrap flag) rejected"
125
-
126
- run_bash "xxd -p file.bin" 2 '\[pre-tool-use\] guard=base64-tool-result.*reason=xxd-plain-hex.*action=reject' \
127
- "Test 6: 'xxd -p' (plain hex encode) rejected"
128
-
129
- run_bash "base64 -d input.b64 > output.bin" 0 "" \
130
- "Test 7: 'base64 -d' (decode direction) allowed"
131
-
132
- run_bash "base64 --decode < x.b64 > y.bin" 0 "" \
133
- "Test 8: 'base64 --decode' (decode long-form) allowed"
134
-
135
- run_bash "echo Zm9v | base64 -d" 0 "" \
136
- "Test 9: 'base64 -d' decode pipeline allowed"
137
-
138
- run_bash "echo '--debug-base64-foo'" 0 "" \
139
- "Test 10: 'base64' substring inside flag name does NOT false-match"
140
-
141
- run_bash "ls mybase64tool" 0 "" \
142
- "Test 11: 'base64' substring inside identifier does NOT false-match"
143
-
144
- run_bash "cat in.b64 | base64 -d > /tmp/foo.bin; cat /tmp/bar.png | base64" 2 '\[pre-tool-use\] guard=base64-tool-result.*reason=base64-encoder.*action=reject' \
145
- "Test 11b: compound (decode ; encode) rejects encoder segment (per-segment scan)"
146
-
147
- run_bash "echo data | base64 -d > x.bin && cat y.png | base64 > y.b64" 2 '\[pre-tool-use\] guard=base64-tool-result.*action=reject' \
148
- "Test 11c: compound (decode && encode) rejects encoder segment"
149
-
150
- run_bash "base64 -d in.b64 > out.bin; base64 -d in2.b64 > out2.bin" 0 "" \
151
- "Test 11d: compound (decode ; decode) allowed"
152
-
153
- # ───────── Write/Edit consumer guard ────────────────────────────────────────
154
- run_write "<html><body>hello world</body></html>" 0 "" \
155
- "Test 12: Write small HTML content (no data URI) allowed"
156
-
157
- run_write "<img src='data:image/png;base64,AAAA'>" 0 "" \
158
- "Test 13: Write content with small inline data URI (<4096 chars) allowed"
159
-
160
- run_write "<img src='data:image/png;base64,${LARGE_B64}'>" 2 '\[pre-tool-use\] guard=base64-write-content.*action=reject' \
161
- "Test 14: Write content with large inline data URI (>4096 chars) rejected"
162
-
163
- run_edit "<img src='data:image/png;base64,${LARGE_B64}'>" 2 '\[pre-tool-use\] guard=base64-write-content.*action=reject' \
164
- "Test 15: Edit new_string with large inline data URI rejected"
165
-
166
- run_edit "<p>just text replacement</p>" 0 "" \
167
- "Test 16: Edit new_string with plain text allowed"
168
-
169
- # ───────── Fail-open / structural ───────────────────────────────────────────
170
- STDOUT_FILE=$(mktemp); STDERR_FILE=$(mktemp); TMPFILES+=("$STDOUT_FILE" "$STDERR_FILE")
171
- printf '%s' 'not json at all { ' | bash "$HOOK" admin >"$STDOUT_FILE" 2>"$STDERR_FILE"
172
- RC=$?
173
- if [[ "$RC" -ne 0 ]]; then
174
- fail "Test 17: malformed stdin should fail open (exit 0), got $RC. Stderr: $(cat "$STDERR_FILE")"
175
- else
176
- pass "Test 17: malformed stdin → silent passthrough (fail-open)"
177
- fi
178
-
179
- # Terminal-stdin guard preserved (no -t 0 test runs in test harness; assert
180
- # the guard line exists in source).
181
- if ! grep -q '\[ -t 0 \]' "$HOOK"; then
182
- fail "Test 18: terminal stdin guard missing from hook source"
183
- else
184
- pass "Test 18: terminal stdin guard present in source"
185
- fi
186
-
187
- # Pre-existing guards still active — entitlement file edit still rejected.
188
- ENT_JSON=$(python3 -c 'import json; print(json.dumps({"hook_event_name":"PreToolUse","tool_name":"Write","tool_input":{"file_path":"/srv/entitlement.json","content":"{\"tier\":\"max\"}"},"transcript_path":"/tmp/sess/parent.jsonl","parent_tool_use_id":"toolu_test_subagent"}, separators=(",", ":")))')
189
- STDOUT_FILE=$(mktemp); STDERR_FILE=$(mktemp); TMPFILES+=("$STDOUT_FILE" "$STDERR_FILE")
190
- printf '%s' "$ENT_JSON" | bash "$HOOK" admin >"$STDOUT_FILE" 2>"$STDERR_FILE"
191
- RC=$?
192
- if [[ "$RC" -ne 2 ]]; then
193
- fail "Test 19: pre-existing entitlement guard regressed (expected exit 2, got $RC)"
194
- else
195
- pass "Test 19: pre-existing entitlement guard still rejects entitlement.json"
196
- fi
197
-
198
- echo
199
- echo "──────── pre-tool-use base64 guard test summary ────────"
200
- echo "PASS: $PASS"
201
- echo "FAIL: $FAIL"
202
-
203
- [[ "$FAIL" -gt 0 ]] && exit 1
204
- exit 0
@@ -1,118 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Task 225 — passthrough regression test for memory-write / memory-update
3
- # under the admin agent's pre-tool-use.sh.
4
- #
5
- # The Task 213→222 chain previously gated both writers at the top of the
6
- # admin branch with an exit-2 block (admin direct) and an exit-0 allow
7
- # (Task-subagent). Task 225 reversed the doctrine: admin holds the
8
- # writers in its own surface and the case-block was deleted. This test
9
- # pins the new behaviour so a future re-introduction of the block
10
- # cannot land silently.
11
- #
12
- # Five cases:
13
- # 1. mcp__plugin_memory_memory__memory-write admin-direct → exit 0, no rejection stderr
14
- # 2. mcp__plugin_memory_memory__memory-update admin-direct → exit 0, no rejection stderr
15
- # 3. mcp__plugin_memory_memory__memory-search admin-direct → exit 0, no rejection stderr (read tool control)
16
- # 4. mcp__memory__memory-write admin-direct → exit 0 (pre-209 namespace, falls through harmlessly)
17
- # 5. malformed JSON stdin → exit 0 (no crash; case-block removal does not break fall-through)
18
-
19
- set -u
20
-
21
- HOOK="$(cd "$(dirname "$0")/.." && pwd)/pre-tool-use.sh"
22
- if [[ ! -x "$HOOK" ]]; then
23
- echo "FAIL: $HOOK not executable" >&2
24
- exit 1
25
- fi
26
-
27
- TMPFILES=()
28
- cleanup() {
29
- for f in "${TMPFILES[@]:-}"; do
30
- [[ -n "$f" ]] && rm -f "$f" 2>/dev/null || true
31
- done
32
- }
33
- trap cleanup EXIT
34
-
35
- PASS=0
36
- FAIL=0
37
- pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
38
- fail() { echo "FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
39
-
40
- # Admin-direct calls land with a non-subagent transcript path. A synthetic
41
- # empty file in a temp dir is enough — the hook no longer reads the path
42
- # for the deleted writers' case-block, but other gates may still touch it.
43
- ADMIN_TRANSCRIPT=$(mktemp); TMPFILES+=("$ADMIN_TRANSCRIPT")
44
- : > "$ADMIN_TRANSCRIPT"
45
-
46
- run_with_tool() {
47
- local tool="$1"
48
- local sid="$2"
49
- local transcript_path="${3:-}"
50
- local stdout_file; stdout_file=$(mktemp); TMPFILES+=("$stdout_file")
51
- local stderr_file; stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
52
- python3 -c '
53
- import json, sys
54
- payload = {
55
- "hook_event_name": "PreToolUse",
56
- "session_id": sys.argv[1],
57
- "tool_name": sys.argv[2],
58
- "tool_input": {"name":"Smalleys","accountId":"acct-x","scope":"shared"},
59
- }
60
- if sys.argv[3]:
61
- payload["transcript_path"] = sys.argv[3]
62
- print(json.dumps(payload, separators=(",", ":")))
63
- ' "$sid" "$tool" "$transcript_path" | bash "$HOOK" admin >"$stdout_file" 2>"$stderr_file"
64
- HOOK_RC=$?
65
- HOOK_STDERR=$(cat "$stderr_file")
66
- }
67
-
68
- assert_passthrough() {
69
- local label="$1"
70
- if [[ "$HOOK_RC" -ne 0 ]]; then
71
- fail "${label}: expected exit 0, got $HOOK_RC stderr: $HOOK_STDERR"
72
- return
73
- fi
74
- if echo "$HOOK_STDERR" | grep -qF "does not write to the graph directly"; then
75
- fail "${label}: stderr carries deleted-block rejection message: $HOOK_STDERR"
76
- return
77
- fi
78
- pass "${label}: exit 0, no rejection stderr"
79
- }
80
-
81
- # --- 1. memory-write admin-direct passthrough ---------------------------
82
- run_with_tool "mcp__plugin_memory_memory__memory-write" "sess-write-1" "$ADMIN_TRANSCRIPT"
83
- assert_passthrough "admin-direct memory-write"
84
-
85
- # --- 2. memory-update admin-direct passthrough --------------------------
86
- run_with_tool "mcp__plugin_memory_memory__memory-update" "sess-update-1" "$ADMIN_TRANSCRIPT"
87
- assert_passthrough "admin-direct memory-update"
88
-
89
- # --- 3. memory-search admin-direct passthrough (read tool control) -----
90
- run_with_tool "mcp__plugin_memory_memory__memory-search" "sess-search-1" "$ADMIN_TRANSCRIPT"
91
- assert_passthrough "admin-direct memory-search (read tool control)"
92
-
93
- # --- 4. pre-209 namespace mcp__memory__memory-write ---------------------
94
- # The bare mcp__memory__ namespace was never in the admin runtime surface
95
- # after Task 203's plugin_ prefix rename; this case pins that it still
96
- # falls through harmlessly after the case-block removal.
97
- run_with_tool "mcp__memory__memory-write" "sess-old-ns-1" "$ADMIN_TRANSCRIPT"
98
- assert_passthrough "pre-209 namespace mcp__memory__memory-write"
99
-
100
- # --- 5. malformed JSON stdin — hook must not crash ----------------------
101
- stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
102
- stdout_file=$(mktemp); TMPFILES+=("$stdout_file")
103
- echo "not-valid-json{{{" | bash "$HOOK" admin >"$stdout_file" 2>"$stderr_file"
104
- HOOK_RC=$?
105
- HOOK_STDERR=$(cat "$stderr_file")
106
- if [[ "$HOOK_RC" -ne 0 ]]; then
107
- fail "malformed payload: expected exit 0, got $HOOK_RC stderr: $HOOK_STDERR"
108
- elif echo "$HOOK_STDERR" | grep -qF "does not write to the graph directly"; then
109
- fail "malformed payload: stderr carries deleted-block rejection message: $HOOK_STDERR"
110
- else
111
- pass "malformed payload: exit 0, hook does not crash"
112
- fi
113
-
114
- # --- Summary -----------------------------------------------------------
115
- echo "---"
116
- echo "PASSED: $PASS FAILED: $FAIL"
117
- [[ "$FAIL" -eq 0 ]] || exit 1
118
- exit 0