@rubytech/create-maxy-code 0.1.561 → 0.1.562

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 (31) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/hooks/__tests__/datetime-inject.test.sh +10 -0
  3. package/payload/platform/plugins/admin/hooks/__tests__/mailbox-inject.test.sh +15 -2
  4. package/payload/platform/plugins/admin/hooks/__tests__/preference-consult-directive.test.sh +6 -0
  5. package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-directive.test.sh +10 -0
  6. package/payload/platform/plugins/admin/hooks/datetime-inject.sh +10 -0
  7. package/payload/platform/plugins/admin/hooks/mailbox-inject.sh +14 -0
  8. package/payload/platform/plugins/admin/hooks/preference-consult-directive.sh +9 -0
  9. package/payload/platform/plugins/admin/hooks/prompt-optimiser-directive.sh +10 -0
  10. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +3 -1
  11. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +5 -0
  12. package/payload/platform/plugins/docs/references/internals.md +2 -0
  13. package/payload/platform/scripts/__tests__/public-prompt-gate-registered.test.sh +114 -0
  14. package/payload/platform/scripts/smoke-boot-services.sh +14 -0
  15. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  16. package/payload/platform/services/claude-session-manager/dist/http-server.js +26 -10
  17. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  18. package/payload/platform/services/claude-session-manager/dist/index.js +16 -0
  19. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  20. package/payload/platform/services/claude-session-manager/dist/public-prompt-surface.d.ts +56 -0
  21. package/payload/platform/services/claude-session-manager/dist/public-prompt-surface.d.ts.map +1 -0
  22. package/payload/platform/services/claude-session-manager/dist/public-prompt-surface.js +155 -0
  23. package/payload/platform/services/claude-session-manager/dist/public-prompt-surface.js.map +1 -0
  24. package/payload/platform/services/claude-session-manager/dist/rc-public-lockdown.d.ts +6 -0
  25. package/payload/platform/services/claude-session-manager/dist/rc-public-lockdown.d.ts.map +1 -1
  26. package/payload/platform/services/claude-session-manager/dist/rc-public-lockdown.js +1 -1
  27. package/payload/platform/services/claude-session-manager/dist/rc-public-lockdown.js.map +1 -1
  28. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +4 -0
  29. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
  30. package/payload/platform/services/claude-session-manager/dist/system-prompt.js +45 -27
  31. package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.561",
3
+ "version": "0.1.562",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -114,6 +114,16 @@ else
114
114
  fi
115
115
  rm -rf "$FAKEBIN2"
116
116
 
117
+ # Task 2424 — a public visitor's turn carries only its agent's authored
118
+ # IDENTITY/SOUL/KNOWLEDGE. MAXY_PUBLIC_ALLOWED_TOOLS is stamped for role=public
119
+ # spawns only, by both spawn paths.
120
+ pub_out=$(printf '%s' '{"session_id":"x","prompt":"hi"}' | MAXY_PUBLIC_ALLOWED_TOOLS='mcp__channel__reply' bash "$HOOK" 2>/dev/null); pub_rc=$?
121
+ if [[ "$pub_rc" -eq 0 && -z "$pub_out" ]]; then
122
+ echo "PASS: public turn exits 0 with no stdout"; PASS=$((PASS+1))
123
+ else
124
+ echo "FAIL: public turn injected (exit=$pub_rc stdout=$pub_out)" >&2; FAIL=$((FAIL+1))
125
+ fi
126
+
117
127
  echo "----"
118
128
  echo "PASS=$PASS FAIL=$FAIL"
119
129
  [[ "$FAIL" -eq 0 ]]
@@ -493,13 +493,26 @@ else
493
493
  fi
494
494
  rm -rf "$A"
495
495
 
496
+ # Task 2424 — a public visitor's turn must never carry the operator's mailbox
497
+ # list. This hook's only role gate was "is ACCOUNT_DIR set", which held on the
498
+ # pty-spawner path but not on the rc-spawn path, where buildRcChildEnv stamps it
499
+ # unconditionally. MAXY_PUBLIC_ALLOWED_TOOLS is stamped for role=public only.
500
+ PUB_A=$(mktemp -d)
501
+ PUB_OUT=$(printf '%s' '{"session_id":"x","prompt":"hi"}' | ACCOUNT_DIR="$PUB_A" MAXY_PUBLIC_ALLOWED_TOOLS='mcp__channel__reply' bash "$HOOK" 2>/dev/null); PUB_RC=$?
502
+ if [ "$PUB_RC" -eq 0 ] && [ -z "$PUB_OUT" ]; then
503
+ ok "public turn exits 0 with no stdout even with ACCOUNT_DIR set"
504
+ else
505
+ bad "public turn injected (exit=$PUB_RC stdout=$PUB_OUT)"
506
+ fi
507
+ rm -rf "$PUB_A"
508
+
496
509
  echo "----"
497
510
  echo "PASS=$PASS FAIL=$FAIL"
498
511
  # The count is asserted, not just the failures: a case that stops running (a
499
512
  # broken helper, a guard, an early exit) otherwise leaves a green suite that
500
513
  # tested less than it claims, and .husky/pre-commit reads only the exit status.
501
- EXPECTED=28
502
- if [ "$(id -u)" -eq 0 ]; then EXPECTED=26; fi
514
+ EXPECTED=29
515
+ if [ "$(id -u)" -eq 0 ]; then EXPECTED=27; fi
503
516
  if [[ $((PASS+FAIL)) -ne "$EXPECTED" ]]; then
504
517
  echo "FAIL: expected $EXPECTED cases, ran $((PASS+FAIL))" >&2
505
518
  exit 1
@@ -10,6 +10,12 @@ check 'printf "%s" "$OUT" | grep -q "hookSpecificOutput"' "emits envelope"
10
10
  check 'printf "%s" "$OUT" | grep -q "profile-read"' "names profile-read"
11
11
  check 'printf "%s" "$OUT" | grep -q "output/"' "names output scratch rule"
12
12
  check '! printf "%s" "$OUT" | grep -q "—"' "no em-dash"
13
+ # Task 2424 — a public visitor's turn carries only its agent's authored
14
+ # IDENTITY/SOUL/KNOWLEDGE. MAXY_PUBLIC_ALLOWED_TOOLS is stamped for role=public
15
+ # spawns only, by both spawn paths.
16
+ OUT_PUB=$(printf '{"session_id":"x","prompt":"hi"}' | MAXY_PUBLIC_ALLOWED_TOOLS='mcp__channel__reply' bash "$HOOK" 2>/dev/null); RC_PUB=$?
17
+ check '[ "$RC_PUB" = "0" ]' "public turn exits 0"
18
+ check '[ -z "$OUT_PUB" ]' "public turn emits nothing on stdout"
13
19
  echo "----- $PASS passed, $FAIL failed -----"
14
20
  for f in "${FAILED[@]:-}"; do [ -n "$f" ] && echo " $f"; done
15
21
  [ "$FAIL" -eq 0 ]
@@ -195,6 +195,16 @@ else
195
195
  echo "FAIL: false-suppression on a normal prompt (exit=$norm_exit stdout=$norm_out)" >&2; FAIL=$((FAIL+1))
196
196
  fi
197
197
 
198
+ # Task 2424 — a public visitor's turn carries only its agent's authored
199
+ # IDENTITY/SOUL/KNOWLEDGE. MAXY_PUBLIC_ALLOWED_TOOLS is stamped for role=public
200
+ # spawns only, by both spawn paths.
201
+ pub_out=$(printf '%s' '{"session_id":"x","prompt":"hi"}' | MAXY_PUBLIC_ALLOWED_TOOLS='mcp__channel__reply' bash "$HOOK" 2>/dev/null); pub_rc=$?
202
+ if [[ "$pub_rc" -eq 0 && -z "$pub_out" ]]; then
203
+ echo "PASS: public turn exits 0 with no stdout"; PASS=$((PASS+1))
204
+ else
205
+ echo "FAIL: public turn injected (exit=$pub_rc stdout=$pub_out)" >&2; FAIL=$((FAIL+1))
206
+ fi
207
+
198
208
  echo "----"
199
209
  echo "PASS=$PASS FAIL=$FAIL"
200
210
  [[ "$FAIL" -eq 0 ]]
@@ -34,6 +34,16 @@ set -uo pipefail
34
34
  # session_id is used here (for the breadcrumb); the block is prompt-independent.
35
35
  HOOK_INPUT=$(cat 2>/dev/null || true)
36
36
 
37
+ # Task 2424 — a public visitor's turn carries only its agent's authored
38
+ # IDENTITY/SOUL/KNOWLEDGE. MAXY_PUBLIC_ALLOWED_TOOLS is stamped for role=public
39
+ # spawns only, by both spawn paths, and is the signal public-tool-surface.sh
40
+ # already keys on. Placed after the stdin drain so the parent never sees EPIPE.
41
+ if [ -n "${MAXY_PUBLIC_ALLOWED_TOOLS:-}" ]; then
42
+ echo "[public-prompt-gate] op=gate hook=datetime-inject.sh public=1" >&2
43
+ exit 0
44
+ fi
45
+ echo "[public-prompt-gate] op=gate hook=datetime-inject.sh public=0" >&2
46
+
37
47
  # Fail-open if either encoder is unavailable: python3 wraps the JSON envelope,
38
48
  # node formats the instant in the target zone (Intl has the full IANA database).
39
49
  command -v python3 >/dev/null 2>&1 || exit 0
@@ -32,6 +32,20 @@ set -uo pipefail
32
32
  # hit with EPIPE. Same reason the two sibling hooks drain first.
33
33
  HOOK_INPUT=$(cat 2>/dev/null || true)
34
34
 
35
+ # Task 2424 — a public visitor's turn carries only its agent's authored
36
+ # IDENTITY/SOUL/KNOWLEDGE. The ACCOUNT_DIR test below is NOT a role gate on the
37
+ # rc-spawn path: buildRcChildEnv stamps that variable unconditionally
38
+ # (rc-daemon.ts), so Task 2271's reasoning held only for pty-spawner, and after
39
+ # Task 2423 routed public Telegram through /rc-spawn the operator's mailbox list
40
+ # reached visitors. MAXY_PUBLIC_ALLOWED_TOOLS is stamped for role=public spawns
41
+ # only, by both paths. Placed after the stdin drain so the parent never sees
42
+ # EPIPE.
43
+ if [ -n "${MAXY_PUBLIC_ALLOWED_TOOLS:-}" ]; then
44
+ echo "[public-prompt-gate] op=gate hook=mailbox-inject.sh public=1" >&2
45
+ exit 0
46
+ fi
47
+ echo "[public-prompt-gate] op=gate hook=mailbox-inject.sh public=0" >&2
48
+
35
49
  command -v python3 >/dev/null 2>&1 || exit 0
36
50
  command -v node >/dev/null 2>&1 || exit 0
37
51
 
@@ -5,6 +5,15 @@
5
5
  # promote finished deliverables out of output/. Prompt-independent; fail-open.
6
6
  set -uo pipefail
7
7
  HOOK_INPUT=$(cat 2>/dev/null || true)
8
+ # Task 2424 — a public visitor's turn carries only its agent's authored
9
+ # IDENTITY/SOUL/KNOWLEDGE. MAXY_PUBLIC_ALLOWED_TOOLS is stamped for role=public
10
+ # spawns only, by both spawn paths, and is the signal public-tool-surface.sh
11
+ # already keys on. Placed after the stdin drain so the parent never sees EPIPE.
12
+ if [ -n "${MAXY_PUBLIC_ALLOWED_TOOLS:-}" ]; then
13
+ echo "[public-prompt-gate] op=gate hook=preference-consult-directive.sh public=1" >&2
14
+ exit 0
15
+ fi
16
+ echo "[public-prompt-gate] op=gate hook=preference-consult-directive.sh public=0" >&2
8
17
  command -v python3 >/dev/null 2>&1 || exit 0
9
18
  python3 - <<'PY' 2>/dev/null || exit 0
10
19
  import json
@@ -38,6 +38,16 @@ set -uo pipefail
38
38
  # UserPromptSubmit payload carries session_id, used for the durable breadcrumb.
39
39
  HOOK_INPUT=$(cat 2>/dev/null || true)
40
40
 
41
+ # Task 2424 — a public visitor's turn carries only its agent's authored
42
+ # IDENTITY/SOUL/KNOWLEDGE. MAXY_PUBLIC_ALLOWED_TOOLS is stamped for role=public
43
+ # spawns only, by both spawn paths, and is the signal public-tool-surface.sh
44
+ # already keys on. Placed after the stdin drain so the parent never sees EPIPE.
45
+ if [ -n "${MAXY_PUBLIC_ALLOWED_TOOLS:-}" ]; then
46
+ echo "[public-prompt-gate] op=gate hook=prompt-optimiser-directive.sh public=1" >&2
47
+ exit 0
48
+ fi
49
+ echo "[public-prompt-gate] op=gate hook=prompt-optimiser-directive.sh public=0" >&2
50
+
41
51
  # Fail-open if the JSON encoder is unavailable.
42
52
  command -v python3 >/dev/null 2>&1 || exit 0
43
53
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:0a9fdc97ea6caba4625dfee58ef5c504befcd1adfef8c892330bf95b3c17c3ee
4
+ content-hash: sha256:2a6a3a040c8d55a472cfc2eda9180b15e5a0ebd8e5ce49a997c4348570ffd37b
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -5557,6 +5557,8 @@ There is exactly one place each datum is read from, and every subsystem reads it
5557
5557
  - **Default agent + account id** come from the canonical account config `data/accounts/<id>/account.json`, resolved via `resolveAccount` / `resolveDefaultAgentSlug` / `getDefaultAccountId` (`app/lib/claude-agent/account.ts`). The UI server's bare-root handler and branding loader read it through these helpers — never from `~/.<brand>/account.json`, which nothing writes. A `null` default means a genuinely unset `defaultAgent`; the root logs `[public-root] op=serve-default slug=<s>` on success and `op=no-default` only when the canonical default is truly empty.
5558
5558
  - **Identity files** (IDENTITY.md, SOUL.md, KNOWLEDGE.md) come from `agents/<slug>/` — keyed on the agent's slug, never the literal role. A public agent renamed to a slug `≠ "public"` (e.g. `maxy-info`) spawns from `agents/maxy-info/`. Admin reads `agents/admin/`. The spawn composer and the standing reachability audit resolve this directory through one shared function, `resolveAgentIdentityDir(accountDir, role, slug)` (`claude-session-manager/src/agent-identity-locator.ts`), so the path the audit vouches for is byte-identical to the path the spawn reads — the audit's `reachable=true` implies the spawn will find the files, and a slug whose files are misplaced is reported `reachable=false reason=files-missing file=<path>` without a visitor request.
5559
5559
 
5560
+ **What a public block contains.** A `role='public'` compose emits `<identity>`, `<soul>`, `<platform-boundary>`, `<data-stewardship>` and `<knowledge>`, and nothing else. The `<host>` section every other role gets is omitted: hostname, LAN IPv4, admin URL and tunnel URL are facts about the operator's machine, and a visitor's agent holds only what its author wrote. The two standing directives stay because they constrain behaviour rather than stating extractable facts. `op=public-persona-composed … hostBytes=0` is the per-spawn proof; non-zero on a public spawn is the regression signal, readable without reproducing a visitor turn.
5561
+
5560
5562
  Which account is `<id>`? The slug is the address, and the address names the account. `resolvePublicAddressFromDisk` (`platform/ui/app/lib/claude-agent/public-address.ts`) walks every valid account for an active agent at the requested slug: exactly one claimant serves; the house serves when it is among several claimants and the others are refused by name; two or more non-house claimants serve nobody. The bare root `/` carries no slug and stays the house's door with the house's `defaultAgent`. The four public surfaces — the `/{slug}` page, `POST /api/chat`, `POST /api/session` and the visitor transcript reader — share that one resolver, and the resolved account rides `agentAccountId` to `/public-spawn`, which re-scopes the spawn onto it. A visitor's uploads follow the same account, and the reader locates them from the session's own sidecar `accountId` rather than re-deriving them from the slug, because a slug's owner can change after the session ran. Slugs are not unique across accounts, so a collision is reported on every refusal (`[webchat] op=address-collision`) and counted on every run of the `webchat-address-audit` loop.
5561
5563
 
5562
5564
  ### Observability
@@ -9,6 +9,11 @@ Invoked by the admin agent directly.
9
9
 
10
10
  This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
11
11
 
12
+ ## 2026-08-05 (0.1.562)
13
+
14
+ - A public agent no longer sees anything about you or your machine. A stranger messaging a public bot was having your connected mailbox list, this machine's name and network addresses, its admin and tunnel web addresses, today's date and your working preferences added to every turn, alongside the persona you wrote. A public turn now carries only what the agent's author wrote plus its single reply tool.
15
+ - Two standing checks back that up: one records, for every account, exactly what gets added to a turn, and the other refuses to start if that picture drifts from what is expected. Each spawn also reports how much was added, so a public one showing anything other than nothing is visible immediately.
16
+
12
17
  ## 2026-08-04 (0.1.561)
13
18
 
14
19
  - A public Telegram bot now answers. Messages were arriving and the conversation staying alive, but nothing the visitor sent ever became a turn, so a public bot received everything and replied to nothing. Public bots now open through the same locked-down path the platform already uses elsewhere: the ordinary setup with only the reply tool available.
@@ -238,6 +238,8 @@ There is exactly one place each datum is read from, and every subsystem reads it
238
238
  - **Default agent + account id** come from the canonical account config `data/accounts/<id>/account.json`, resolved via `resolveAccount` / `resolveDefaultAgentSlug` / `getDefaultAccountId` (`app/lib/claude-agent/account.ts`). The UI server's bare-root handler and branding loader read it through these helpers — never from `~/.<brand>/account.json`, which nothing writes. A `null` default means a genuinely unset `defaultAgent`; the root logs `[public-root] op=serve-default slug=<s>` on success and `op=no-default` only when the canonical default is truly empty.
239
239
  - **Identity files** (IDENTITY.md, SOUL.md, KNOWLEDGE.md) come from `agents/<slug>/` — keyed on the agent's slug, never the literal role. A public agent renamed to a slug `≠ "public"` (e.g. `maxy-info`) spawns from `agents/maxy-info/`. Admin reads `agents/admin/`. The spawn composer and the standing reachability audit resolve this directory through one shared function, `resolveAgentIdentityDir(accountDir, role, slug)` (`claude-session-manager/src/agent-identity-locator.ts`), so the path the audit vouches for is byte-identical to the path the spawn reads — the audit's `reachable=true` implies the spawn will find the files, and a slug whose files are misplaced is reported `reachable=false reason=files-missing file=<path>` without a visitor request.
240
240
 
241
+ **What a public block contains.** A `role='public'` compose emits `<identity>`, `<soul>`, `<platform-boundary>`, `<data-stewardship>` and `<knowledge>`, and nothing else. The `<host>` section every other role gets is omitted: hostname, LAN IPv4, admin URL and tunnel URL are facts about the operator's machine, and a visitor's agent holds only what its author wrote. The two standing directives stay because they constrain behaviour rather than stating extractable facts. `op=public-persona-composed … hostBytes=0` is the per-spawn proof; non-zero on a public spawn is the regression signal, readable without reproducing a visitor turn.
242
+
241
243
  Which account is `<id>`? The slug is the address, and the address names the account. `resolvePublicAddressFromDisk` (`platform/ui/app/lib/claude-agent/public-address.ts`) walks every valid account for an active agent at the requested slug: exactly one claimant serves; the house serves when it is among several claimants and the others are refused by name; two or more non-house claimants serve nobody. The bare root `/` carries no slug and stays the house's door with the house's `defaultAgent`. The four public surfaces — the `/{slug}` page, `POST /api/chat`, `POST /api/session` and the visitor transcript reader — share that one resolver, and the resolved account rides `agentAccountId` to `/public-spawn`, which re-scopes the spawn onto it. A visitor's uploads follow the same account, and the reader locates them from the session's own sidecar `accountId` rather than re-deriving them from the slug, because a slug's owner can change after the session ran. Slugs are not unique across accounts, so a collision is reported on every refusal (`[webchat] op=address-collision`) and counted on every run of the `webchat-address-audit` loop.
242
244
 
243
245
  ### Observability
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env bash
2
+ # Task 2424 — every UserPromptSubmit hook must either carry the public gate or
3
+ # be on the inert list. A fifth injecting hook added without a gate emits no
4
+ # event and does not reproduce on demand: the only symptom is a stranger being
5
+ # told something about the operator.
6
+ #
7
+ # Both writers are scanned. provision-account-dir.sh writes the array at account
8
+ # creation; the account-settings-*.sh libs patch it onto accounts that already
9
+ # exist, which is the only path that reaches client sub-accounts. Of the four
10
+ # libs only mailbox-inject and public-surface touch this event — askgate patches
11
+ # PreToolUse and pdf-conformance patches PostToolUse, so the filter skipping
12
+ # them is correct rather than a broken parser.
13
+ #
14
+ # The guard is matched as a fixed string, never as a mention of the variable: a
15
+ # hook that merely READS MAXY_PUBLIC_ALLOWED_TOOLS is not gated by it, and
16
+ # counting it as gated is exactly the false green this test exists to avoid.
17
+ set -uo pipefail
18
+ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ PROV="$DIR/../lib/provision-account-dir.sh"
20
+ HOOKS_DIR="$DIR/../../plugins/admin/hooks"
21
+ GUARD='[ -n "${MAXY_PUBLIC_ALLOWED_TOOLS:-}" ]'
22
+ INERT="archive-ingest-surface-gate.sh public-tool-surface.sh"
23
+ PASS=0; FAIL=0
24
+
25
+ ok() { echo "PASS: $1"; PASS=$((PASS+1)); }
26
+ bad() { echo "FAIL: $1" >&2; FAIL=$((FAIL+1)); }
27
+
28
+ # Every hook script named inside the provision heredoc's UserPromptSubmit array.
29
+ # The script name is taken from the END of each `"command"` string, not from a
30
+ # `hooks/` prefix: provision writes `bash $HOOKS_PATH/<name>.sh`, so the literal
31
+ # directory never appears there and a prefix anchor silently matches nothing.
32
+ PROV_HOOKS=$(python3 - "$PROV" <<'PY'
33
+ import re, sys
34
+ src = open(sys.argv[1], encoding="utf-8").read()
35
+ m = re.search(r'"UserPromptSubmit"\s*:\s*\[(.*?)\n \]', src, re.S)
36
+ if not m:
37
+ print("")
38
+ sys.exit(0)
39
+ cmds = re.findall(r'"command"\s*:\s*"([^"]*)"', m.group(1))
40
+ names = [n.group(1) for n in (re.search(r'([A-Za-z0-9_.-]+\.sh)\s*$', c.strip()) for c in cmds) if n]
41
+ print("\n".join(sorted(set(names))))
42
+ PY
43
+ )
44
+
45
+ # Every hook script named by a backfill lib that patches the same array. Read
46
+ # only from the `_<NAME>_CMD=` constant each lib registers, not from every `.sh`
47
+ # in the file: these libs name their siblings in prose, and a bare `.sh` sweep
48
+ # turns those comments into phantom registrations.
49
+ BACKFILL_HOOKS=""
50
+ for lib in "$DIR"/../lib/account-settings-*.sh; do
51
+ grep -q 'hooks\.UserPromptSubmit' "$lib" || continue
52
+ cmd_names=$(grep -oE "^_[A-Z0-9_]+_CMD='[^']*'" "$lib" | grep -oE "[A-Za-z0-9_.-]+\.sh'$" | tr -d "'")
53
+ if [ -z "$cmd_names" ]; then
54
+ bad "$(basename "$lib") patches UserPromptSubmit but names no _<NAME>_CMD script — the parser cannot see what it registers"
55
+ fi
56
+ BACKFILL_HOOKS="$BACKFILL_HOOKS
57
+ $cmd_names"
58
+ done
59
+
60
+ ALL=$(printf '%s\n%s\n' "$PROV_HOOKS" "$BACKFILL_HOOKS" | grep -v '^$' | sort -u)
61
+
62
+ if [ -z "$ALL" ]; then
63
+ bad "no UserPromptSubmit hooks found in either writer — the parser is broken, not the tree"
64
+ else
65
+ ok "found $(printf '%s\n' "$ALL" | wc -l | tr -d ' ') registered UserPromptSubmit hooks across both writers"
66
+ fi
67
+
68
+ for h in $ALL; do
69
+ case " $INERT " in
70
+ *" $h "*) ok "$h is on the inert list"; continue ;;
71
+ esac
72
+ if [ ! -f "$HOOKS_DIR/$h" ]; then
73
+ bad "$h is registered but has no script at $HOOKS_DIR/$h"
74
+ elif grep -qF "$GUARD" "$HOOKS_DIR/$h"; then
75
+ ok "$h carries the public gate"
76
+ else
77
+ bad "$h is registered on UserPromptSubmit with no public gate and is not inert"
78
+ fi
79
+ done
80
+
81
+ # The guard and the inert list each exist twice: here, and as exported constants
82
+ # in public-prompt-surface.ts, which the standing census matches against. Two
83
+ # hand-maintained copies of one literal is the drift class that bites silently,
84
+ # so assert them equal. Divergence is not silent here — the census would FATAL
85
+ # every hook on every account and fail every install smoke run — but it would be
86
+ # a false alarm, and the cause would not be obvious from that output.
87
+ CENSUS_TS="$DIR/../../services/claude-session-manager/src/public-prompt-surface.ts"
88
+ if [ ! -f "$CENSUS_TS" ]; then
89
+ bad "the census module is not at $CENSUS_TS — this test can no longer check the shared literals"
90
+ else
91
+ if grep -qF "export const PUBLIC_GATE_GUARD = '$GUARD'" "$CENSUS_TS"; then
92
+ ok "the census module's PUBLIC_GATE_GUARD matches the guard this test greps for"
93
+ else
94
+ bad "PUBLIC_GATE_GUARD in $CENSUS_TS differs from this test's GUARD — the two would disagree about what 'gated' means"
95
+ fi
96
+ for h in $INERT; do
97
+ if grep -qF "'$h'," "$CENSUS_TS" || grep -qF "'$h'" "$CENSUS_TS"; then
98
+ ok "the census module lists $h as inert too"
99
+ else
100
+ bad "$h is inert here but not in $CENSUS_TS — the census would FATAL on it"
101
+ fi
102
+ done
103
+ fi
104
+
105
+ # The path the standing census reads must stay the path provision writes.
106
+ if grep -qF 'ACCOUNT_SETTINGS="$ACCOUNT_DIR/.claude/settings.json"' "$PROV"; then
107
+ ok "the settings path the census reads is still the path provision writes"
108
+ else
109
+ bad "provision no longer writes .claude/settings.json — the census path is stale"
110
+ fi
111
+
112
+ echo "----"
113
+ echo "PASS=$PASS FAIL=$FAIL"
114
+ [ "$FAIL" -eq 0 ]
@@ -284,6 +284,20 @@ run_one() {
284
284
  fi
285
285
  done
286
286
 
287
+ # Task 2424 — a public-prompt-surface drift line means some account has a
288
+ # per-turn hook that will inject operator context into a visitor's turn. The
289
+ # census fires once at boot, inside this window. Scoped to this one tag rather
290
+ # than widening the generic `fatal ` regex above, which would newly fail this
291
+ # gate on unrelated subsystems' pre-existing drift lines.
292
+ local drift_line
293
+ drift_line="$(grep -m1 -F '[public-prompt-surface] op=drift FATAL' "$log_file" || true)"
294
+ if [ -n "$drift_line" ]; then
295
+ echo "[smoke] service=$name reason=public-prompt-surface-drift matched=$drift_line" >&2
296
+ echo "--- log ($log_file) ---" >&2
297
+ cat "$log_file" >&2
298
+ return 1
299
+ fi
300
+
287
301
  echo "[smoke-boot] service=$name specialist-tool-drift=ok system-prompt-sentinels=ok identity-drift=ok boot=ok elapsed-ms=$elapsed_ms"
288
302
  return 0
289
303
  }
@@ -1 +1 @@
1
- {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"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"}
@@ -3502,7 +3502,13 @@ export function buildHttpApp(deps) {
3502
3502
  return c.json({ error: 'spawn-failed', reason: composed.reason }, 503);
3503
3503
  }
3504
3504
  appendSegments.push(composed.block);
3505
- deps.logger(`[rc-spawn] op=public-persona-composed slug=${rcAgentSlug} identityBytes=${composed.identityBytes} soulBytes=${composed.soulBytes} knowledgeBytes=${composed.knowledgeBytes}`);
3505
+ // Task 2424 — `hostBytes=0` is the healthy value here and the per-spawn
3506
+ // proof that a visitor's block carries no host addresses; non-zero is the
3507
+ // composer regressing, readable without reproducing a visitor turn.
3508
+ // `blockBytes` bounds the whole composed surface, so an unexpected growth
3509
+ // (a section reaching this path that should not) is visible as a number
3510
+ // rather than needing the block itself.
3511
+ deps.logger(`[rc-spawn] op=public-persona-composed slug=${rcAgentSlug} identityBytes=${composed.identityBytes} soulBytes=${composed.soulBytes} knowledgeBytes=${composed.knowledgeBytes} hostBytes=${composed.hostBytes} blockBytes=${Buffer.byteLength(composed.block, 'utf8')}`);
3506
3512
  }
3507
3513
  if (appendSegments.length > 0) {
3508
3514
  argv.push('--append-system-prompt', appendSegments.join('\n\n'));
@@ -3678,6 +3684,20 @@ export function buildHttpApp(deps) {
3678
3684
  // match to false — grep-able (`op=cwd-check` + `match=false`) with no
3679
3685
  // live repro needed.
3680
3686
  deps.logger(`[rc-spawn] op=cwd-check unitToken=${unitToken} resolved=${env.ACCOUNT_DIR} actual=${effectiveAccountDir} match=${env.ACCOUNT_DIR === effectiveAccountDir}`);
3687
+ // Task 2339 — the realised-surface hook's only input. `tool-allowlist` below
3688
+ // records the surface this spawn was GIVEN; nothing recorded the surface it
3689
+ // REALISES, so five tools ran under a one-tool allowlist and produced no
3690
+ // line. Stamped here rather than inside buildRcChildEnv so it lands after
3691
+ // the last argv push and the value is this spawn's final `--allowed-tools`
3692
+ // set. Public spawns only; its presence is the signal the hook keys on.
3693
+ //
3694
+ // Task 2424 — moved ABOVE the `tool-allowlist` line, which now reports this
3695
+ // value. It used to be stamped after, so the line could not see it. The four
3696
+ // per-turn hook gates also key on this variable, so its absence on a public
3697
+ // spawn is what `publicToolsEnv=0` names.
3698
+ if (publicLockdown) {
3699
+ env[PUBLIC_ALLOWED_TOOLS_ENV] = allowedToolsFromArgv(argv).join(',');
3700
+ }
3681
3701
  // Task 2260 — the realised tool surface, on EVERY rc-spawn. /rc-spawn emitted
3682
3702
  // no such line, which is why the incident session was invisible to every
3683
3703
  // tool-surface audit. An admin spawn reports count=0 strictMcp=n, which is
@@ -3688,16 +3708,12 @@ export function buildHttpApp(deps) {
3688
3708
  count: publicLockdown ? publicLockdown.allowedTools.length : 0,
3689
3709
  strictMcp: publicLockdown !== null,
3690
3710
  mode: effectiveRcPermissionMode,
3711
+ // Task 2424 — read back from the env actually stamped, not from the
3712
+ // allowlist computed above, so the line reports what reached the child.
3713
+ publicToolsEnv: (env[PUBLIC_ALLOWED_TOOLS_ENV] ?? '')
3714
+ .split(',')
3715
+ .filter((t) => t.length > 0).length,
3691
3716
  }));
3692
- // Task 2339 — the realised-surface hook's only input. `tool-allowlist` above
3693
- // records the surface this spawn was GIVEN; nothing recorded the surface it
3694
- // REALISES, so five tools ran under a one-tool allowlist and produced no
3695
- // line. Stamped here rather than inside buildRcChildEnv so it lands after
3696
- // the last argv push and the value is this spawn's final `--allowed-tools`
3697
- // set. Public spawns only; its presence is the signal the hook keys on.
3698
- if (publicLockdown) {
3699
- env[PUBLIC_ALLOWED_TOOLS_ENV] = allowedToolsFromArgv(argv).join(',');
3700
- }
3701
3717
  deps.logger(`[rc-spawn] op=argv unitToken=${unitToken} cwd=${effectiveAccountDir} permissionMode=${effectiveRcPermissionMode} model=${effectiveRcModel ?? 'unset'} effort=${effectiveRcEffort ?? 'unset'} positionalBytes=${positionalBytes} voiceBytes=${voiceBytes} argv=${JSON.stringify([deps.claudeBin, ...argv])}`);
3702
3718
  emitRcLife(deps.logger, 'argv', 'rc-spawn', 'node-pty', rcLifeMode, unitToken, {
3703
3719
  cwd: effectiveAccountDir,