@rubytech/create-realagent-code 0.1.568 → 0.1.570
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +7 -5
- package/payload/platform/plugins/admin/hooks/__tests__/pdf-text-layer-inject.test.sh +177 -0
- package/payload/platform/plugins/admin/hooks/__tests__/quote-path-write-gate.test.sh +152 -0
- package/payload/platform/plugins/admin/hooks/__tests__/quote-pdf-bash-gate.test.sh +95 -0
- package/payload/platform/plugins/admin/hooks/__tests__/quote-render-gate.test.sh +141 -45
- package/payload/platform/plugins/admin/hooks/__tests__/quote-render-pdf-conformance.test.sh +114 -0
- package/payload/platform/plugins/admin/hooks/pdf-text-layer-inject.sh +137 -0
- package/payload/platform/plugins/admin/hooks/quote-path-write-gate.sh +137 -0
- package/payload/platform/plugins/admin/hooks/quote-pdf-bash-gate.sh +70 -0
- package/payload/platform/plugins/admin/hooks/quote-render-gate.sh +105 -37
- package/payload/platform/plugins/admin/hooks/quote-render-pdf-conformance.sh +178 -0
- package/payload/platform/plugins/admin/mcp/dist/lib/roster-census.d.ts.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/lib/roster-census.js +50 -4
- package/payload/platform/plugins/admin/mcp/dist/lib/roster-census.js.map +1 -1
- package/payload/platform/plugins/admin/skills/a4-print-documents/SKILL.md +12 -1
- package/payload/platform/plugins/admin/skills/a4-print-documents/pdf-inspect.mjs +113 -0
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +14 -0
- package/payload/platform/plugins/docs/references/admin-session.md +15 -0
- package/payload/platform/scripts/__tests__/mailbox-inject-registered.test.sh +19 -43
- package/payload/platform/scripts/__tests__/preference-hooks-registered.test.sh +3 -1
- package/payload/platform/scripts/__tests__/public-prompt-gate-registered.test.sh +22 -37
- package/payload/platform/scripts/__tests__/public-surface-registered.test.sh +12 -35
- package/payload/platform/scripts/__tests__/quote-ownership-contract.test.sh +86 -0
- package/payload/platform/scripts/lib/__tests__/account-settings-reconcile.test.sh +73 -0
- package/payload/platform/scripts/lib/account-settings-reconcile.sh +88 -0
- package/payload/platform/scripts/lib/provision-account-dir.sh +11 -144
- package/payload/platform/scripts/setup-account.sh +11 -53
- package/payload/platform/services/claude-session-manager/dist/account-agent-registry.d.ts +20 -0
- package/payload/platform/services/claude-session-manager/dist/account-agent-registry.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/account-agent-registry.js +78 -1
- package/payload/platform/services/claude-session-manager/dist/account-agent-registry.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/blocked-tool-census.d.ts +129 -0
- package/payload/platform/services/claude-session-manager/dist/blocked-tool-census.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/blocked-tool-census.js +344 -0
- package/payload/platform/services/claude-session-manager/dist/blocked-tool-census.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/index.js +107 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/quote-delivery-reconcile.d.ts +42 -0
- package/payload/platform/services/claude-session-manager/dist/quote-delivery-reconcile.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/quote-delivery-reconcile.js +215 -0
- package/payload/platform/services/claude-session-manager/dist/quote-delivery-reconcile.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/specialist-preference-audit.d.ts +108 -0
- package/payload/platform/services/claude-session-manager/dist/specialist-preference-audit.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/specialist-preference-audit.js +299 -0
- package/payload/platform/services/claude-session-manager/dist/specialist-preference-audit.js.map +1 -0
- package/payload/platform/templates/account-settings.json +159 -0
- package/payload/server/server.js +750 -656
- package/payload/platform/scripts/lib/__tests__/account-settings-askgate.test.sh +0 -144
- package/payload/platform/scripts/lib/__tests__/account-settings-mailbox-inject.test.sh +0 -166
- package/payload/platform/scripts/lib/__tests__/account-settings-pdf-conformance.test.sh +0 -151
- package/payload/platform/scripts/lib/__tests__/account-settings-public-surface.test.sh +0 -109
- package/payload/platform/scripts/lib/account-settings-askgate.sh +0 -98
- package/payload/platform/scripts/lib/account-settings-mailbox-inject.sh +0 -91
- package/payload/platform/scripts/lib/account-settings-pdf-conformance.sh +0 -98
- package/payload/platform/scripts/lib/account-settings-public-surface.sh +0 -109
|
@@ -9,6 +9,20 @@ 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-06 (0.1.570)
|
|
13
|
+
|
|
14
|
+
- Reading a PDF now injects the text layer, so values copied from a PDF are not transcribed by eye any more.
|
|
15
|
+
- The four one-hook account-settings backfill libs have been retired; hook registration is now driven from the account-settings template.
|
|
16
|
+
- The operator is now notified when a tool call stays blocked, with a per-account census, and the notification is scoped to the blocked session's own account.
|
|
17
|
+
|
|
18
|
+
## 2026-08-06 (0.1.569)
|
|
19
|
+
|
|
20
|
+
- Every account's safeguards are now brought up to date on each install. They used to be written once when the account was created and never revisited, so an account could be missing a protection that every other account had, with nothing reporting the gap.
|
|
21
|
+
- A customer quote can now only be produced by the specialist that owns it. Other assistants were writing quote documents by hand, bypassing the template and the renderer entirely; writes to a quote from anything else are now refused, including producing the PDF through the shell.
|
|
22
|
+
- A specialist you wrote for your own account is dispatchable again. The roster was matching on filenames rather than the name declared inside the file, so cards that looked registered could not actually be reached and the work fell to a general-purpose assistant instead.
|
|
23
|
+
- Two new standing checks: one counts work written into an owned area by the wrong specialist, the other reconciles quotes that were sent against the records of what was actually rendered.
|
|
24
|
+
- A quote's running header no longer depends on where the author happened to put page breaks, so it appears on every page rather than the ones that were hand-marked.
|
|
25
|
+
|
|
12
26
|
## 2026-08-06 (0.1.568)
|
|
13
27
|
|
|
14
28
|
- Answers reach the conversation they came from again. An answer was being routed by who the account's admin is rather than by the conversation it was sent to, so a public agent's reply could go nowhere and an admin answer could strand with nothing said about it. Delivery now returns to the door the message arrived on, and every failure to find one is named rather than passing silently. If you rolled an install back because of this, you can move forward again.
|
|
@@ -182,6 +182,21 @@ grep -E "\[(pty-spawn-tool-inventory|recorder-session-init|recorder-turn|mcp:(me
|
|
|
182
182
|
|
|
183
183
|
**Loopback gate.** The `/api/admin/log-ingest` route is loopback-only on both the socket address and (when present) the `X-Forwarded-For` header. A LAN client whose request reaches the route via a proxy still gets a 403 because XFF tokens are required to be loopback when XFF is set. Without this gate the evidence chain would be worthless — any LAN client could forge `[recorder-session-init]` lines under a spoofed sessionId.
|
|
184
184
|
|
|
185
|
+
## A session that stopped with the PTY alive
|
|
186
|
+
|
|
187
|
+
Claude Code raises a permission card for some checks even under `permissionMode=bypassPermissions`: `dangerousRemoval` is `bypassImmune: true` in its safety table, so no allow rule and no permission mode pre-approves it. When such a card is raised inside a background subagent, its only prompt surface is the parent session's UI, so an operator on WhatsApp or Telegram cannot see or answer it and the session simply stops.
|
|
188
|
+
|
|
189
|
+
The manager's blocked-tool census reconciles this against live state every 60s. It reads a bounded tail of each live session's own transcript and of every `<sessionId>/subagents/agent-*.jsonl`, and treats an assistant `tool_use` with no matching `tool_result` as blocked once that transcript has not advanced for 120s. Three line shapes, all under `[blocked-tool]`:
|
|
190
|
+
|
|
191
|
+
- `op=census sessions=<n> scanned=<n> blocked=<n>` — every tick, including zero. A gap in this line means the census stopped, not that nothing was blocked. `sessions` counts live PTYs and `scanned` counts transcripts actually read, so `sessions=3 scanned=0` means no transcript could be resolved for any live session, which is a fault rather than a quiet box.
|
|
192
|
+
- `op=blocked sessionId=<id8> agentId=<id> toolUseId=<id> tool=<name> blockedForMs=<n>` — once per blocked call. `agentId=—` means the card is in the session's own transcript, where the operator can see it in the session UI; `agent-<id>` means it is inside that background subagent, where they cannot. `blockedForMs` is the age of the transcript's last record, not a timer on the call.
|
|
193
|
+
- `op=cleared` with the same ids, once the result lands on a still-live session.
|
|
194
|
+
- `op=session-gone` with the same ids when the session stopped being live with the card still unanswered. This is deliberately not `op=cleared`: the call never resolved, and conflating the two would read as "the block lifted".
|
|
195
|
+
|
|
196
|
+
At 180s blocked the manager sends one notification through the loopback route `/api/admin/session-blocked`, which is gated the same way as `log-ingest`. `op=blocked-notify channel=<name> delivered=<bool>` records where it went and whether it landed. `channel=whatsapp` or `telegram` means the session's own admin door; `channel=house-admin` means the house admin phones, which is what a public session always gets — a public session's own channel is a customer and never receives internal machinery. `delivered=false` with `channel=none` means the manager could not reach the route at all.
|
|
197
|
+
|
|
198
|
+
The notification cannot answer the card. Answer it in the session's PTY or in Claude Desktop.
|
|
199
|
+
|
|
185
200
|
## Out of scope
|
|
186
201
|
|
|
187
202
|
- Public/WhatsApp/Telegram sessions — out of scope. Their sessionKeys remain `crypto.randomUUID()` and follow the existing rejection-on-restart contract.
|
|
@@ -1,32 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Task 2157 — the hook must be registered in
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# backfill would not recognise what provision wrote, and would append a second
|
|
7
|
-
# copy to every house account on every install.
|
|
2
|
+
# Task 2157 — the hook must be registered in the account-settings template, in
|
|
3
|
+
# the UserPromptSubmit event, with a fully resolved command path. The template
|
|
4
|
+
# is the single writer since Task 2467, and Task 2475 retired the backfill lib
|
|
5
|
+
# that used to be the second one, so losing the entry here loses it everywhere.
|
|
8
6
|
#
|
|
9
|
-
# Substring greps are deliberately avoided for the command itself:
|
|
10
|
-
#
|
|
11
|
-
#
|
|
7
|
+
# Substring greps are deliberately avoided for the command itself: a bare grep
|
|
8
|
+
# for "mailbox-inject.sh" is satisfied by a comment and proves nothing about
|
|
9
|
+
# what would be written.
|
|
12
10
|
set -uo pipefail
|
|
13
11
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
-
PROV="$DIR
|
|
15
|
-
BACKFILL="$DIR/../lib/account-settings-mailbox-inject.sh"
|
|
16
|
-
SETUP="$DIR/../setup-account.sh"
|
|
12
|
+
PROV="$DIR/../../templates/account-settings.json"
|
|
17
13
|
PASS=0; FAIL=0
|
|
18
14
|
|
|
19
15
|
ok() { echo "PASS: $1"; PASS=$((PASS+1)); }
|
|
20
16
|
bad() { echo "FAIL: $1" >&2; FAIL=$((FAIL+1)); }
|
|
21
17
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
PROV_LINE='{ "type": "command", "command": "bash $HOOKS_PATH/mailbox-inject.sh" }'
|
|
26
|
-
RESOLVED='bash $PLATFORM_ROOT/plugins/admin/hooks/mailbox-inject.sh'
|
|
18
|
+
# The exact line the template must carry. $PLATFORM_ROOT is a literal in the
|
|
19
|
+
# settings file, resolved by Claude Code at hook-run time.
|
|
20
|
+
PROV_LINE='{ "type": "command", "command": "bash $PLATFORM_ROOT/plugins/admin/hooks/mailbox-inject.sh" }'
|
|
27
21
|
|
|
28
22
|
if /usr/bin/grep -qF "$PROV_LINE" "$PROV"; then
|
|
29
|
-
ok "
|
|
23
|
+
ok "the template writes the hook into a UserPromptSubmit entry"
|
|
30
24
|
else
|
|
31
25
|
bad "exact provision command line absent from $PROV"
|
|
32
26
|
fi
|
|
@@ -44,36 +38,18 @@ else
|
|
|
44
38
|
bad "mailbox-inject.sh is not inside the UserPromptSubmit array in $PROV"
|
|
45
39
|
fi
|
|
46
40
|
|
|
47
|
-
# The
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
# The command must be fully resolved in the template. An unresolved HOOKS_PATH
|
|
42
|
+
# indirection would leave the path to be expanded by whoever copies the file,
|
|
43
|
+
# and the account would carry a hook command that resolves to nothing.
|
|
44
|
+
if ! /usr/bin/grep -q 'HOOKS_PATH' "$PROV"; then
|
|
45
|
+
ok "the template carries no unresolved hook-path indirection"
|
|
50
46
|
else
|
|
51
|
-
bad "
|
|
52
|
-
fi
|
|
53
|
-
|
|
54
|
-
# HOOKS_PATH must still resolve to the path the backfill lib assumes; if someone
|
|
55
|
-
# repoints it, the two writers silently disagree and every account gets two hooks.
|
|
56
|
-
if /usr/bin/grep -qF 'HOOKS_PATH="\$PLATFORM_ROOT/plugins/admin/hooks"' "$PROV"; then
|
|
57
|
-
ok "HOOKS_PATH resolves to the path the backfill lib assumes"
|
|
58
|
-
else
|
|
59
|
-
bad "HOOKS_PATH in $PROV no longer matches the backfill lib assumption"
|
|
60
|
-
fi
|
|
61
|
-
|
|
62
|
-
if /usr/bin/grep -qF 'account-settings-mailbox-inject.sh' "$SETUP"; then
|
|
63
|
-
ok "backfill lib sourced by setup-account.sh"
|
|
64
|
-
else
|
|
65
|
-
bad "backfill lib not sourced by $SETUP"
|
|
66
|
-
fi
|
|
67
|
-
|
|
68
|
-
if /usr/bin/grep -qE '^reconcile_all_accounts_mailbox_inject "\$ACCOUNTS_DIR"' "$SETUP"; then
|
|
69
|
-
ok "backfill invoked over every account by setup-account.sh"
|
|
70
|
-
else
|
|
71
|
-
bad "reconcile_all_accounts_mailbox_inject not invoked in $SETUP"
|
|
47
|
+
bad "$PROV still carries a HOOKS_PATH indirection, so the written command would not resolve"
|
|
72
48
|
fi
|
|
73
49
|
|
|
74
50
|
echo "----"
|
|
75
51
|
echo "PASS=$PASS FAIL=$FAIL"
|
|
76
|
-
if [ $((PASS+FAIL)) -ne
|
|
52
|
+
if [ $((PASS+FAIL)) -ne 3 ]; then
|
|
77
53
|
echo "FAIL: expected 6 checks, ran $((PASS+FAIL))" >&2
|
|
78
54
|
exit 1
|
|
79
55
|
fi
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -uo pipefail
|
|
3
3
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
4
|
-
|
|
4
|
+
# The provision-time source of the hook set is the template provision_account_dir
|
|
5
|
+
# copies verbatim, not a heredoc inside the writer.
|
|
6
|
+
LIB="$DIR/../../templates/account-settings.json"
|
|
5
7
|
PASS=0; FAIL=0; FAILED=()
|
|
6
8
|
c(){ if grep -qF "$1" "$LIB"; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$2"); fi; }
|
|
7
9
|
c 'preference-consult-gate.sh' "gate registered"
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
# event and does not reproduce on demand: the only symptom is a stranger being
|
|
5
5
|
# told something about the operator.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
7
|
+
# The template is the single writer: provision-account-dir.sh copies
|
|
8
|
+
# platform/templates/account-settings.json at account-creation time, and
|
|
9
|
+
# setup-account.sh's reconcile_all_accounts_settings converges every existing
|
|
10
|
+
# account against the same file on every install. Task 2475 retired the four
|
|
11
|
+
# one-hook backfill libs that used to patch this array, so the template is the
|
|
12
|
+
# only place a UserPromptSubmit hook can be registered.
|
|
13
13
|
#
|
|
14
14
|
# The guard is matched as a fixed string, never as a mention of the variable: a
|
|
15
15
|
# hook that merely READS MAXY_PUBLIC_ALLOWED_TOOLS is not gated by it, and
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
set -uo pipefail
|
|
18
18
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
19
|
PROV="$DIR/../lib/provision-account-dir.sh"
|
|
20
|
+
TPL="$DIR/../../templates/account-settings.json"
|
|
20
21
|
HOOKS_DIR="$DIR/../../plugins/admin/hooks"
|
|
21
22
|
GUARD='[ -n "${MAXY_PUBLIC_ALLOWED_TOOLS:-}" ]'
|
|
22
23
|
INERT="archive-ingest-surface-gate.sh public-tool-surface.sh"
|
|
@@ -25,44 +26,28 @@ PASS=0; FAIL=0
|
|
|
25
26
|
ok() { echo "PASS: $1"; PASS=$((PASS+1)); }
|
|
26
27
|
bad() { echo "FAIL: $1" >&2; FAIL=$((FAIL+1)); }
|
|
27
28
|
|
|
28
|
-
# Every hook script named inside the
|
|
29
|
-
#
|
|
30
|
-
# `hooks/` prefix:
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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]
|
|
29
|
+
# Every hook script named inside the template's UserPromptSubmit array. The
|
|
30
|
+
# script name is taken from the END of each `"command"` string, not from a
|
|
31
|
+
# `hooks/` prefix: the template writes `bash $PLATFORM_ROOT/plugins/admin/hooks/
|
|
32
|
+
# <name>.sh`, and anchoring on a literal directory would break the first time
|
|
33
|
+
# that prefix is spelled differently.
|
|
34
|
+
TPL_HOOKS=$(python3 - "$TPL" <<'PY'
|
|
35
|
+
import json, re, sys
|
|
36
|
+
doc = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
37
|
+
cmds = [h.get("command", "")
|
|
38
|
+
for entry in doc.get("hooks", {}).get("UserPromptSubmit", [])
|
|
39
|
+
for h in entry.get("hooks", [])]
|
|
40
|
+
names = [m.group(1) for m in (re.search(r'([A-Za-z0-9_.-]+\.sh)\s*$', c.strip()) for c in cmds) if m]
|
|
41
41
|
print("\n".join(sorted(set(names))))
|
|
42
42
|
PY
|
|
43
43
|
)
|
|
44
44
|
|
|
45
|
-
|
|
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)
|
|
45
|
+
ALL=$(printf '%s\n' "$TPL_HOOKS" | grep -v '^$' | sort -u)
|
|
61
46
|
|
|
62
47
|
if [ -z "$ALL" ]; then
|
|
63
|
-
bad "no UserPromptSubmit hooks found in
|
|
48
|
+
bad "no UserPromptSubmit hooks found in the template — the parser is broken, not the tree"
|
|
64
49
|
else
|
|
65
|
-
ok "found $(printf '%s\n' "$ALL" | wc -l | tr -d ' ') registered UserPromptSubmit hooks
|
|
50
|
+
ok "found $(printf '%s\n' "$ALL" | wc -l | tr -d ' ') registered UserPromptSubmit hooks in the template"
|
|
66
51
|
fi
|
|
67
52
|
|
|
68
53
|
for h in $ALL; do
|
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Task 2339 — the hook must be registered in
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# losing one: the backfill would not recognise what provision wrote and would
|
|
7
|
-
# append a second copy to every house account on every install.
|
|
2
|
+
# Task 2339 — the hook must be registered in the account-settings template, on
|
|
3
|
+
# all three events, with a fully resolved command path. The template is the
|
|
4
|
+
# single writer since Task 2467, and Task 2475 retired the backfill lib that
|
|
5
|
+
# used to be the second one, so losing an entry here loses it everywhere.
|
|
8
6
|
set -uo pipefail
|
|
9
7
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
-
PROV="$DIR
|
|
11
|
-
BACKFILL="$DIR/../lib/account-settings-public-surface.sh"
|
|
12
|
-
SETUP="$DIR/../setup-account.sh"
|
|
8
|
+
PROV="$DIR/../../templates/account-settings.json"
|
|
13
9
|
PASS=0; FAIL=0
|
|
14
10
|
|
|
15
11
|
ok() { echo "PASS: $1"; PASS=$((PASS+1)); }
|
|
16
12
|
bad() { echo "FAIL: $1" >&2; FAIL=$((FAIL+1)); }
|
|
17
13
|
|
|
18
|
-
PROV_LINE='{ "type": "command", "command": "bash $
|
|
19
|
-
RESOLVED='bash $PLATFORM_ROOT/plugins/admin/hooks/public-tool-surface.sh'
|
|
14
|
+
PROV_LINE='{ "type": "command", "command": "bash $PLATFORM_ROOT/plugins/admin/hooks/public-tool-surface.sh" }'
|
|
20
15
|
|
|
21
16
|
if [ "$(/usr/bin/grep -cF "$PROV_LINE" "$PROV")" = "3" ]; then
|
|
22
|
-
ok "
|
|
17
|
+
ok "the template writes the hook exactly three times"
|
|
23
18
|
else
|
|
24
19
|
bad "expected 3 provision command lines in $PROV"
|
|
25
20
|
fi
|
|
@@ -69,34 +64,16 @@ else
|
|
|
69
64
|
bad "public-tool-surface.sh is not inside the UserPromptSubmit array in $PROV"
|
|
70
65
|
fi
|
|
71
66
|
|
|
72
|
-
if /usr/bin/grep -
|
|
73
|
-
ok "the
|
|
67
|
+
if ! /usr/bin/grep -q 'HOOKS_PATH' "$PROV"; then
|
|
68
|
+
ok "the template carries no unresolved hook-path indirection"
|
|
74
69
|
else
|
|
75
|
-
bad "
|
|
76
|
-
fi
|
|
77
|
-
|
|
78
|
-
if /usr/bin/grep -qF 'HOOKS_PATH="\$PLATFORM_ROOT/plugins/admin/hooks"' "$PROV"; then
|
|
79
|
-
ok "HOOKS_PATH resolves to the path the backfill lib assumes"
|
|
80
|
-
else
|
|
81
|
-
bad "HOOKS_PATH in $PROV no longer matches the backfill lib assumption"
|
|
82
|
-
fi
|
|
83
|
-
|
|
84
|
-
if /usr/bin/grep -qF 'account-settings-public-surface.sh' "$SETUP"; then
|
|
85
|
-
ok "backfill lib sourced by setup-account.sh"
|
|
86
|
-
else
|
|
87
|
-
bad "backfill lib not sourced by $SETUP"
|
|
88
|
-
fi
|
|
89
|
-
|
|
90
|
-
if /usr/bin/grep -qE '^reconcile_all_accounts_public_surface "\$ACCOUNTS_DIR"' "$SETUP"; then
|
|
91
|
-
ok "backfill invoked over every account by setup-account.sh"
|
|
92
|
-
else
|
|
93
|
-
bad "reconcile_all_accounts_public_surface not invoked in $SETUP"
|
|
70
|
+
bad "$PROV still carries a HOOKS_PATH indirection, so the written command would not resolve"
|
|
94
71
|
fi
|
|
95
72
|
|
|
96
73
|
echo "----"
|
|
97
74
|
echo "PASS=$PASS FAIL=$FAIL"
|
|
98
|
-
if [ $((PASS+FAIL)) -ne
|
|
99
|
-
echo "FAIL: expected
|
|
75
|
+
if [ $((PASS+FAIL)) -ne 5 ]; then
|
|
76
|
+
echo "FAIL: expected 5 checks, ran $((PASS+FAIL))" >&2
|
|
100
77
|
exit 1
|
|
101
78
|
fi
|
|
102
79
|
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# The client quote document has exactly one owner, and that ownership is a
|
|
3
|
+
# capability as well as a claim.
|
|
4
|
+
#
|
|
5
|
+
# A hand-orchestrated quote reached a customer because the quoter's own card
|
|
6
|
+
# disclaimed document work ("not a document edit for another agent"), so the
|
|
7
|
+
# admin's routing to a general content specialist was correct against it. The
|
|
8
|
+
# guarantee that replaced that prose is a capability: the quote tools are
|
|
9
|
+
# granted to the quoter's card and to no other, so no other agent can produce a
|
|
10
|
+
# receipt-bearing quote by any route.
|
|
11
|
+
#
|
|
12
|
+
# Covers:
|
|
13
|
+
# A. the quoter's body claims ownership of the document's presentation
|
|
14
|
+
# B. the disclaimer that sent document work elsewhere is gone
|
|
15
|
+
# C. the description front-matter carries the same claim (the roster text the
|
|
16
|
+
# admin reads at dispatch time)
|
|
17
|
+
# D. quote-render / quote-engine-run are granted to the quoter
|
|
18
|
+
# E. no other card in the tree grants either tool
|
|
19
|
+
|
|
20
|
+
set -u
|
|
21
|
+
|
|
22
|
+
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
|
23
|
+
QUOTER="$ROOT/premium-plugins/sitedesk/agents/sitedesk--quoter.md"
|
|
24
|
+
|
|
25
|
+
PASS=0
|
|
26
|
+
FAIL=0
|
|
27
|
+
ok() { echo "PASS: $1"; PASS=$((PASS + 1)); }
|
|
28
|
+
bad() { echo "FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
|
|
29
|
+
|
|
30
|
+
if [ ! -f "$QUOTER" ]; then
|
|
31
|
+
echo "FAIL: quoter card missing at $QUOTER" >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Body = everything outside the frontmatter block.
|
|
36
|
+
BODY="$(sed -n '/^---$/,/^---$/!p' "$QUOTER")"
|
|
37
|
+
DESC="$(grep -m1 '^description:' "$QUOTER" || true)"
|
|
38
|
+
|
|
39
|
+
for word in layout "cover page" photograph "running header" correction; do
|
|
40
|
+
if printf '%s' "$BODY" | grep -qi -- "$word"; then
|
|
41
|
+
ok "A: body claims $word"
|
|
42
|
+
else
|
|
43
|
+
bad "A: body does not claim $word"
|
|
44
|
+
fi
|
|
45
|
+
done
|
|
46
|
+
|
|
47
|
+
if grep -q 'not a document edit for another agent' "$QUOTER"; then
|
|
48
|
+
bad "B: the document-edit disclaimer is still present"
|
|
49
|
+
else
|
|
50
|
+
ok "B: document-edit disclaimer absent"
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
if printf '%s' "$DESC" | grep -qi 'owns the client quote document'; then
|
|
54
|
+
ok "C: description carries the ownership claim"
|
|
55
|
+
else
|
|
56
|
+
bad "C: description does not carry the ownership claim"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
TOOLS="$(grep -m1 '^tools:' "$QUOTER" || true)"
|
|
60
|
+
for t in quote-render quote-engine-run; do
|
|
61
|
+
if printf '%s' "$TOOLS" | grep -q -- "$t"; then
|
|
62
|
+
ok "D: quoter grants $t"
|
|
63
|
+
else
|
|
64
|
+
bad "D: quoter does not grant $t"
|
|
65
|
+
fi
|
|
66
|
+
done
|
|
67
|
+
|
|
68
|
+
STRAY=0
|
|
69
|
+
while IFS= read -r card; do
|
|
70
|
+
[ "$card" = "$QUOTER" ] && continue
|
|
71
|
+
line="$(grep -m1 '^tools:' "$card" || true)"
|
|
72
|
+
if printf '%s' "$line" | grep -qE 'quote-render|quote-engine-run'; then
|
|
73
|
+
echo " stray grant: $card" >&2
|
|
74
|
+
STRAY=$((STRAY + 1))
|
|
75
|
+
fi
|
|
76
|
+
done < <(find "$ROOT/premium-plugins" "$ROOT/platform/templates/specialists/agents" -name '*.md' -type f 2>/dev/null)
|
|
77
|
+
|
|
78
|
+
if [ "$STRAY" -eq 0 ]; then
|
|
79
|
+
ok "E: no other card grants a quote tool"
|
|
80
|
+
else
|
|
81
|
+
bad "E: $STRAY other card(s) grant a quote tool"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
echo "──────── quote ownership contract summary ────────"
|
|
85
|
+
echo "PASS=$PASS FAIL=$FAIL"
|
|
86
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# reconcile_all_accounts_settings converges every account's hook set against the
|
|
3
|
+
# shipped template and states the divergence count on every run.
|
|
4
|
+
#
|
|
5
|
+
# provision_account_dir writes the account's .claude/settings.json at provision
|
|
6
|
+
# time only, and the upgrade path re-provisions the house account and no other.
|
|
7
|
+
# An account created before a hook was added therefore runs a stale hook set
|
|
8
|
+
# indefinitely, silently — which is how a client-quote gate came to be
|
|
9
|
+
# registered on one of four accounts on a live install. Five one-hook backfill
|
|
10
|
+
# libs were written that way, each after a different hook was found missing.
|
|
11
|
+
# This lib converges the whole set instead.
|
|
12
|
+
#
|
|
13
|
+
# Covers:
|
|
14
|
+
# A. an account missing a template hook converges → status=updated
|
|
15
|
+
# B. an already-matching account is left alone → status=already-set
|
|
16
|
+
# C. an account with no settings file is seeded → status=absent-seeded
|
|
17
|
+
# D. an unparseable file is reported, not rewritten → status=rewrite-failed
|
|
18
|
+
# E. the summary line is emitted on a re-run, stating its count
|
|
19
|
+
# F. the summary line counts the divergent accounts
|
|
20
|
+
|
|
21
|
+
set -u
|
|
22
|
+
|
|
23
|
+
LIB="$(cd "$(dirname "$0")/.." && pwd)/account-settings-reconcile.sh"
|
|
24
|
+
if [ ! -f "$LIB" ]; then
|
|
25
|
+
echo "FAIL: $LIB missing" >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
# shellcheck disable=SC1090
|
|
29
|
+
. "$LIB"
|
|
30
|
+
|
|
31
|
+
TMP=$(mktemp -d)
|
|
32
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
33
|
+
|
|
34
|
+
TPL="$TMP/account-settings.json"
|
|
35
|
+
printf '{"hooks":{"PreToolUse":[{"matcher":"Write","hooks":[{"type":"command","command":"bash a.sh"}]}]}}' > "$TPL"
|
|
36
|
+
|
|
37
|
+
ACCTS="$TMP/accounts"
|
|
38
|
+
mk() { mkdir -p "$ACCTS/$1/.claude"; printf '{}' > "$ACCTS/$1/account.json"; }
|
|
39
|
+
|
|
40
|
+
PASS=0
|
|
41
|
+
FAIL=0
|
|
42
|
+
ok() { echo "PASS: $1"; PASS=$((PASS + 1)); }
|
|
43
|
+
bad() { echo "FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
|
|
44
|
+
|
|
45
|
+
mk stale; printf '{"hooks":{"PreToolUse":[]}}' > "$ACCTS/stale/.claude/settings.json"
|
|
46
|
+
mk current; cp "$TPL" "$ACCTS/current/.claude/settings.json"
|
|
47
|
+
mk fresh
|
|
48
|
+
mk broken; printf 'not json' > "$ACCTS/broken/.claude/settings.json"
|
|
49
|
+
|
|
50
|
+
OUT="$(MAXY_ACCOUNT_SETTINGS_TEMPLATE="$TPL" reconcile_all_accounts_settings "$ACCTS" 2>&1)"
|
|
51
|
+
|
|
52
|
+
printf '%s' "$OUT" | grep -q 'account=stale status=updated' \
|
|
53
|
+
&& ok "A: stale account converged" || bad "A: stale account not converged"
|
|
54
|
+
printf '%s' "$OUT" | grep -q 'account=current status=already-set' \
|
|
55
|
+
&& ok "B: matching account left alone" || bad "B: matching account not already-set"
|
|
56
|
+
printf '%s' "$OUT" | grep -q 'account=fresh status=absent-seeded' \
|
|
57
|
+
&& ok "C: missing settings seeded" || bad "C: missing settings not seeded"
|
|
58
|
+
printf '%s' "$OUT" | grep -q 'account=broken status=rewrite-failed' \
|
|
59
|
+
&& ok "D: unparseable file reported" || bad "D: unparseable file not reported"
|
|
60
|
+
[ "$(cat "$ACCTS/broken/.claude/settings.json")" = "not json" ] \
|
|
61
|
+
&& ok "D: unparseable file left untouched" || bad "D: unparseable file was rewritten"
|
|
62
|
+
printf '%s' "$OUT" | grep -q 'accounts=4 divergent=3' \
|
|
63
|
+
&& ok "F: summary counts the divergent accounts" || bad "F: wrong summary — $OUT"
|
|
64
|
+
|
|
65
|
+
# The converged accounts stay converged; the unparseable one never does, which
|
|
66
|
+
# is the standing signal.
|
|
67
|
+
OUT2="$(MAXY_ACCOUNT_SETTINGS_TEMPLATE="$TPL" reconcile_all_accounts_settings "$ACCTS" 2>&1)"
|
|
68
|
+
printf '%s' "$OUT2" | grep -q 'accounts=4 divergent=1' \
|
|
69
|
+
&& ok "E: re-run states its count" || bad "E: wrong re-run summary — $OUT2"
|
|
70
|
+
|
|
71
|
+
echo "──────── account-settings-reconcile summary ────────"
|
|
72
|
+
echo "PASS=$PASS FAIL=$FAIL"
|
|
73
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# account-settings-reconcile.sh — converge every account's Claude Code project
|
|
2
|
+
# settings against the shipped template, and state the divergence count.
|
|
3
|
+
#
|
|
4
|
+
# provision_account_dir writes <accountDir>/.claude/settings.json from
|
|
5
|
+
# platform/templates/account-settings.json at provision time only. The upgrade
|
|
6
|
+
# path re-provisions the house account and no other, so an account created
|
|
7
|
+
# before a hook was added runs a stale hook set indefinitely, and nothing says
|
|
8
|
+
# so. A client-quote delivery gate was registered on one of four accounts on a
|
|
9
|
+
# live install for exactly that reason, and the account it was missing from was
|
|
10
|
+
# the one that quotes.
|
|
11
|
+
#
|
|
12
|
+
# Five one-hook backfill libs were written the same way, each after a different
|
|
13
|
+
# hook was found missing on a live account: owned-dirs, askgate,
|
|
14
|
+
# pdf-conformance, mailbox-inject, public-surface. This lib converges the whole
|
|
15
|
+
# set instead, so a hook added to the template reaches every existing account on
|
|
16
|
+
# the next install without a sixth bespoke backfill.
|
|
17
|
+
#
|
|
18
|
+
# Requires jq. Idempotent. Never echoes file contents. Fail-open: an unusable
|
|
19
|
+
# file logs a status and is left untouched, never rewritten from a guess.
|
|
20
|
+
#
|
|
21
|
+
# Sourced by setup-account.sh; also runnable standalone for a manual repair.
|
|
22
|
+
|
|
23
|
+
# ensure_account_settings <settings_file> <template_file>
|
|
24
|
+
# Returns 0 when the file already matched the template, 1 when it diverged
|
|
25
|
+
# (seeded, updated, unwritable, or unparseable). Logs exactly one
|
|
26
|
+
# [settings-reconcile] account=<id> status=<already-set|updated|absent-seeded|rewrite-failed|no-template>
|
|
27
|
+
ensure_account_settings() {
|
|
28
|
+
local f="$1" tpl="$2"
|
|
29
|
+
local acct
|
|
30
|
+
acct="$(basename "$(dirname "$(dirname "$f")")")"
|
|
31
|
+
|
|
32
|
+
if [ ! -f "$tpl" ]; then
|
|
33
|
+
echo "[settings-reconcile] account=$acct status=no-template"
|
|
34
|
+
return 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if [ ! -f "$f" ]; then
|
|
38
|
+
mkdir -p "$(dirname "$f")" 2>/dev/null || true
|
|
39
|
+
if cp "$tpl" "$f" 2>/dev/null; then
|
|
40
|
+
echo "[settings-reconcile] account=$acct status=absent-seeded"
|
|
41
|
+
else
|
|
42
|
+
echo "[settings-reconcile] account=$acct status=rewrite-failed"
|
|
43
|
+
fi
|
|
44
|
+
return 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Compare canonical forms, so key order and whitespace never read as drift.
|
|
48
|
+
# An empty result means jq refused the file; that is reported, not repaired —
|
|
49
|
+
# an unparseable settings file may carry an operator edit worth keeping, and
|
|
50
|
+
# this pass never destroys what it cannot read.
|
|
51
|
+
local before after
|
|
52
|
+
before="$(jq -S . "$f" 2>/dev/null || true)"
|
|
53
|
+
after="$(jq -S . "$tpl" 2>/dev/null || true)"
|
|
54
|
+
if [ -z "$before" ] || [ -z "$after" ]; then
|
|
55
|
+
echo "[settings-reconcile] account=$acct status=rewrite-failed"
|
|
56
|
+
return 1
|
|
57
|
+
fi
|
|
58
|
+
if [ "$before" = "$after" ]; then
|
|
59
|
+
echo "[settings-reconcile] account=$acct status=already-set"
|
|
60
|
+
return 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
if cp "$tpl" "$f" 2>/dev/null; then
|
|
64
|
+
echo "[settings-reconcile] account=$acct status=updated"
|
|
65
|
+
else
|
|
66
|
+
echo "[settings-reconcile] account=$acct status=rewrite-failed"
|
|
67
|
+
fi
|
|
68
|
+
return 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# reconcile_all_accounts_settings <accounts_dir>
|
|
72
|
+
# Standing check + durable backfill across every account carrying an
|
|
73
|
+
# account.json. Emits one status line per account plus one summary line on every
|
|
74
|
+
# run, including a zero count — silence is not evidence of convergence.
|
|
75
|
+
reconcile_all_accounts_settings() {
|
|
76
|
+
local accts="$1"
|
|
77
|
+
local tpl="${MAXY_ACCOUNT_SETTINGS_TEMPLATE:-${TEMPLATES_DIR:-}/account-settings.json}"
|
|
78
|
+
local n=0 divergent=0 acct
|
|
79
|
+
if [ -d "$accts" ]; then
|
|
80
|
+
for acct in "$accts"/*/; do
|
|
81
|
+
[ -f "$acct/account.json" ] || continue
|
|
82
|
+
n=$((n + 1))
|
|
83
|
+
ensure_account_settings "$acct/.claude/settings.json" "$tpl" || divergent=$((divergent + 1))
|
|
84
|
+
done
|
|
85
|
+
fi
|
|
86
|
+
echo "[settings-reconcile] accounts=$n divergent=$divergent"
|
|
87
|
+
return 0
|
|
88
|
+
}
|