@rubytech/create-sitedesk-code 0.1.507 → 0.1.509
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/dist/index.js +53 -0
- package/package.json +2 -2
- package/payload/platform/config/brand.json +12 -0
- package/payload/platform/docs/superpowers/plans/2026-07-26-task-1926-portal-push-concurrency-guard.md +482 -0
- package/payload/platform/docs/superpowers/specs/2026-07-26-task-1926-portal-push-concurrency-guard-design.md +200 -0
- package/payload/platform/plugins/admin/PLUGIN.md +10 -5
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +96 -13
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +13 -0
- package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +68 -28
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +271 -29
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +5 -4
- package/payload/platform/plugins/docs/references/admin-ui.md +95 -12
- package/payload/platform/plugins/memory/PLUGIN.md +6 -3
- package/payload/platform/scripts/__tests__/check-brand-cache.test.sh +231 -0
- package/payload/platform/scripts/__tests__/provision-honours-disabled-agents.test.sh +84 -0
- package/payload/platform/scripts/check-brand-cache.sh +138 -0
- package/payload/platform/scripts/check-risk-class.mjs +7 -2
- package/payload/platform/scripts/check-specialist-tool-surface.mjs +5 -2
- package/payload/platform/scripts/lib/canonical-tool-names.mjs +7 -4
- package/payload/platform/scripts/lib/provision-account-dir.sh +43 -0
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts +1 -1
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +56 -3
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/tool-surface.d.ts +19 -5
- package/payload/platform/services/claude-session-manager/dist/tool-surface.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/tool-surface.js +50 -11
- package/payload/platform/services/claude-session-manager/dist/tool-surface.js.map +1 -1
- package/payload/server/maxy-edge.js +15 -10
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/{AdminLoginScreens-YT7jsh_H.js → AdminLoginScreens-8Jk7hnWH.js} +1 -1
- package/payload/server/public/assets/{AdminShell-Ckht9hmU.js → AdminShell-BMPEWNZk.js} +1 -1
- package/payload/server/public/assets/{activity-DH3Wgc3O.js → activity-ByWYF5dC.js} +1 -1
- package/payload/server/public/assets/admin-CnUWbKcx.js +1 -0
- package/payload/server/public/assets/agents-C0rTfrYd.js +1 -0
- package/payload/server/public/assets/{browser-Bym8QhfA.js → browser-4tTaem8N.js} +1 -1
- package/payload/server/public/assets/{calendar-CFnufJOy.js → calendar-BEjnkKG1.js} +1 -1
- package/payload/server/public/assets/chat-DBgzjDIE.js +1 -0
- package/payload/server/public/assets/chevron-left-DqYv3oFh.js +1 -0
- package/payload/server/public/assets/chevron-right-CQfPGsFb.js +1 -0
- package/payload/server/public/assets/clock-Dn6FHB51.js +1 -0
- package/payload/server/public/assets/data-D_e6Vdfd.js +1 -0
- package/payload/server/public/assets/{file-text-zch0E1GJ.js → file-text-DMKw3nIk.js} +1 -1
- package/payload/server/public/assets/{graph-BavHLzpV.js → graph-C3vW8lvw.js} +1 -1
- package/payload/server/public/assets/{graph-labels-CBwh4g-6.js → graph-labels-BZgd0L6n.js} +1 -1
- package/payload/server/public/assets/{maximize-2-bTn_Ozpi.js → maximize-2-la3IBA3M.js} +1 -1
- package/payload/server/public/assets/{operator-a4IOwf62.js → operator-RRRdqoRQ.js} +1 -1
- package/payload/server/public/assets/{page-3T2mE63b.js → page-BWFHRIAH.js} +1 -1
- package/payload/server/public/assets/{page-D6M1CmPe.js → page-DC11gesX.js} +1 -1
- package/payload/server/public/assets/{public-B3y_YU1G.js → public-DQbbLHQN.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-BGvzNB1b.js → rotate-ccw-OQD5si8N.js} +1 -1
- package/payload/server/public/assets/{routines-C7Dr16am.js → routines-asrBGZUR.js} +1 -1
- package/payload/server/public/assets/{skills-DFeb3XO3.js → skills-D4ECwoxa.js} +1 -1
- package/payload/server/public/assets/{tasks-CSfnCKJe.js → tasks-U3QdbUsO.js} +1 -1
- package/payload/server/public/assets/{time-entry-format-BgZqOOqG.js → time-entry-format-BgdTKgYr.js} +1 -1
- package/payload/server/public/assets/{triangle-alert-DBJ7xz_t.js → triangle-alert-Ck_3VhT9.js} +1 -1
- package/payload/server/public/assets/{useCopyFeedback-BFOujMl-.js → useCopyFeedback-BdUwyNpa.js} +1 -1
- package/payload/server/public/assets/useSubAccountSwitcher-CJKjEPNN.js +9 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DOWQXDT8.css +1 -0
- package/payload/server/public/assets/{useVoiceRecorder-BP6CpYQD.js → useVoiceRecorder-ChnBrzbZ.js} +1 -1
- package/payload/server/public/assets/{wrench-DdaRhfzS.js → wrench-Cpee3C3J.js} +1 -1
- package/payload/server/public/brand/fonts/inter-600.woff2 +0 -0
- package/payload/server/public/brand/fonts/inter-700.woff2 +0 -0
- package/payload/server/public/brand/fonts/inter-800.woff2 +0 -0
- package/payload/server/public/brand-defaults.css +22 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +7 -7
- package/payload/server/public/chat.html +13 -13
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +9 -9
- package/payload/server/public/index.html +14 -14
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +13 -13
- package/payload/server/public/routines.html +6 -6
- package/payload/server/public/skills.html +5 -5
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +1139 -427
- package/payload/server/public/assets/admin-DetvulAu.js +0 -1
- package/payload/server/public/assets/agents-X_IvDHLZ.js +0 -1
- package/payload/server/public/assets/chat-wrckyIpw.js +0 -1
- package/payload/server/public/assets/chevron-left-Dtk3jEK5.js +0 -1
- package/payload/server/public/assets/chevron-right-Dih6IZ8B.js +0 -1
- package/payload/server/public/assets/clock-iJCrrmWM.js +0 -1
- package/payload/server/public/assets/data-D76--TYs.js +0 -1
- package/payload/server/public/assets/useSubAccountSwitcher-0PfRrN6i.js +0 -9
- package/payload/server/public/assets/useSubAccountSwitcher-BkQ5tA8A.css +0 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tests for check-brand-cache.sh (Task 2019). Fixture-driven: builds a throwaway
|
|
3
|
+
# install dir + config dir per case and stubs the fetch command, because the real
|
|
4
|
+
# one crosses a Cloudflare edge that no test can hold still. sha256 and the
|
|
5
|
+
# ingress parse are the real code paths — those are what this asserts.
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
CHECK="$SCRIPT_DIR/../check-brand-cache.sh"
|
|
10
|
+
|
|
11
|
+
PASS=0
|
|
12
|
+
FAIL=0
|
|
13
|
+
|
|
14
|
+
run_case() {
|
|
15
|
+
local name="$1"; shift
|
|
16
|
+
echo ""
|
|
17
|
+
echo "=== CASE: $name ==="
|
|
18
|
+
if "$@"; then
|
|
19
|
+
PASS=$((PASS + 1)); echo "PASS: $name"
|
|
20
|
+
else
|
|
21
|
+
FAIL=$((FAIL + 1)); echo "FAIL: $name"
|
|
22
|
+
fi
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Build install dir with two brand files and brand-defaults.css. Echoes its path.
|
|
26
|
+
mk_install() {
|
|
27
|
+
local d
|
|
28
|
+
d="$(mktemp -d "${TMPDIR:-/tmp}/brand-cache-install.XXXXXX")"
|
|
29
|
+
mkdir -p "$d/server/public/brand"
|
|
30
|
+
printf 'lime-logo-bytes' > "$d/server/public/brand/logo.png"
|
|
31
|
+
printf 'lime-icon-bytes' > "$d/server/public/brand/icon.png"
|
|
32
|
+
printf ':root{--brand:#c3f53c}' > "$d/server/public/brand-defaults.css"
|
|
33
|
+
echo "$d"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Build a config dir whose cloudflared/config.yml has the given ingress hosts.
|
|
37
|
+
mk_config_with_hosts() {
|
|
38
|
+
local d; d="$(mktemp -d "${TMPDIR:-/tmp}/brand-cache-config.XXXXXX")"
|
|
39
|
+
mkdir -p "$d/cloudflared"
|
|
40
|
+
{ echo "ingress:"; for h in "$@"; do echo " - hostname: $h"; done; echo " - service: http_status:404"; } \
|
|
41
|
+
> "$d/cloudflared/config.yml"
|
|
42
|
+
echo "$d"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
mk_config_no_tunnel() {
|
|
46
|
+
local d; d="$(mktemp -d "${TMPDIR:-/tmp}/brand-cache-config.XXXXXX")"
|
|
47
|
+
echo "$d"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# A config dir with no cloudflared ingress but an alias-domains.json, the
|
|
51
|
+
# second source resolvePublicHostname reads.
|
|
52
|
+
mk_config_alias_only() {
|
|
53
|
+
local d; d="$(mktemp -d "${TMPDIR:-/tmp}/brand-cache-config.XXXXXX")"
|
|
54
|
+
printf '["public.realagent.app"]' > "$d/alias-domains.json"
|
|
55
|
+
echo "$d"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Stub curl. $1 = dir whose files it serves as the "edge", keyed by basename.
|
|
59
|
+
# Writes headers to the -D path and the body to the -o path, exit 0.
|
|
60
|
+
mk_curl_stub() {
|
|
61
|
+
local serve_dir="$1" stub
|
|
62
|
+
stub="$(mktemp "${TMPDIR:-/tmp}/brand-cache-curl.XXXXXX")"
|
|
63
|
+
cat > "$stub" <<STUB
|
|
64
|
+
#!/usr/bin/env bash
|
|
65
|
+
hdr=""; out=""; url=""
|
|
66
|
+
while [ \$# -gt 0 ]; do
|
|
67
|
+
case "\$1" in
|
|
68
|
+
-D) hdr="\$2"; shift 2 ;;
|
|
69
|
+
-o) out="\$2"; shift 2 ;;
|
|
70
|
+
-*) shift ;;
|
|
71
|
+
*) url="\$1"; shift ;;
|
|
72
|
+
esac
|
|
73
|
+
done
|
|
74
|
+
name="\${url##*/}"
|
|
75
|
+
printf 'HTTP/2 200\r\ncf-cache-status: HIT\r\nage: 5352\r\n\r\n' > "\$hdr"
|
|
76
|
+
cat "$serve_dir/\$name" > "\$out"
|
|
77
|
+
STUB
|
|
78
|
+
chmod +x "$stub"
|
|
79
|
+
echo "$stub"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case_clean_install_reports_clean() {
|
|
83
|
+
local i c stub edge out rc
|
|
84
|
+
i="$(mk_install)"; c="$(mk_config_with_hosts operations.realagent.network)"
|
|
85
|
+
# Edge serves exactly what is on disk.
|
|
86
|
+
edge="$(mktemp -d "${TMPDIR:-/tmp}/brand-cache-edge.XXXXXX")"
|
|
87
|
+
cp "$i/server/public/brand/"* "$edge/"
|
|
88
|
+
cp "$i/server/public/brand-defaults.css" "$edge/"
|
|
89
|
+
stub="$(mk_curl_stub "$edge")"
|
|
90
|
+
out="$(BRAND_CACHE_CURL="$stub" "$CHECK" --install-dir "$i" --config-dir "$c" 2>/dev/null)"
|
|
91
|
+
rc=$?
|
|
92
|
+
[ "$rc" -eq 0 ] || { echo " expected exit 0, got $rc"; rm -rf "$i" "$c" "$edge" "$stub"; return 1; }
|
|
93
|
+
# files=2 pinned as an integer: a \d+ match also accepts files=0, and a
|
|
94
|
+
# hostname resolving to nothing would then read as a pass.
|
|
95
|
+
echo "$out" | grep -qx "\[brand-cache\] clean host=operations.realagent.network files=2" \
|
|
96
|
+
|| { echo " no clean line with files=2; got: $out"; rm -rf "$i" "$c" "$edge" "$stub"; return 1; }
|
|
97
|
+
rm -rf "$i" "$c" "$edge" "$stub"; return 0
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case_stale_edge_reports_stale_on_stderr() {
|
|
101
|
+
local i c stub edge err rc
|
|
102
|
+
i="$(mk_install)"; c="$(mk_config_with_hosts operations.realagent.network)"
|
|
103
|
+
edge="$(mktemp -d "${TMPDIR:-/tmp}/brand-cache-edge.XXXXXX")"
|
|
104
|
+
cp "$i/server/public/brand/"* "$edge/"
|
|
105
|
+
cp "$i/server/public/brand-defaults.css" "$edge/"
|
|
106
|
+
# The measured symptom: the edge holds the retired mark, disk holds the new one.
|
|
107
|
+
printf 'black-square-bytes' > "$edge/logo.png"
|
|
108
|
+
stub="$(mk_curl_stub "$edge")"
|
|
109
|
+
err="$(BRAND_CACHE_CURL="$stub" "$CHECK" --install-dir "$i" --config-dir "$c" 2>&1 >/dev/null)"
|
|
110
|
+
rc=$?
|
|
111
|
+
[ "$rc" -eq 1 ] || { echo " expected exit 1, got $rc"; rm -rf "$i" "$c" "$edge" "$stub"; return 1; }
|
|
112
|
+
echo "$err" | grep -qE "^\[brand-cache\] STALE host=operations\.realagent\.network path=/brand/logo\.png edge=[0-9a-f]{7} disk=[0-9a-f]{7} cf-cache-status=HIT age=5352$" \
|
|
113
|
+
|| { echo " no STALE line for /brand/logo.png; got: $err"; rm -rf "$i" "$c" "$edge" "$stub"; return 1; }
|
|
114
|
+
rm -rf "$i" "$c" "$edge" "$stub"; return 0
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
case_no_tunnel_reports_skipped() {
|
|
118
|
+
local i c out rc
|
|
119
|
+
i="$(mk_install)"; c="$(mk_config_no_tunnel)"
|
|
120
|
+
out="$("$CHECK" --install-dir "$i" --config-dir "$c" 2>/dev/null)"
|
|
121
|
+
rc=$?
|
|
122
|
+
[ "$rc" -eq 0 ] || { echo " expected exit 0, got $rc"; rm -rf "$i" "$c"; return 1; }
|
|
123
|
+
echo "$out" | grep -qx "\[brand-cache\] skipped host=none reason=no-tunnel-ingress" \
|
|
124
|
+
|| { echo " no skipped line; got: $out"; rm -rf "$i" "$c"; return 1; }
|
|
125
|
+
rm -rf "$i" "$c"; return 0
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case_localhost_only_ingress_is_no_tunnel() {
|
|
129
|
+
local i c out rc
|
|
130
|
+
i="$(mk_install)"; c="$(mk_config_with_hosts localhost muvin.local)"
|
|
131
|
+
out="$("$CHECK" --install-dir "$i" --config-dir "$c" 2>/dev/null)"
|
|
132
|
+
rc=$?
|
|
133
|
+
[ "$rc" -eq 0 ] || { echo " expected exit 0, got $rc"; rm -rf "$i" "$c"; return 1; }
|
|
134
|
+
echo "$out" | grep -qx "\[brand-cache\] skipped host=none reason=no-tunnel-ingress" \
|
|
135
|
+
|| { echo " localhost/.local counted as ingress; got: $out"; rm -rf "$i" "$c"; return 1; }
|
|
136
|
+
rm -rf "$i" "$c"; return 0
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
case_unfetchable_asset_is_not_silently_clean() {
|
|
140
|
+
local i c stub err rc
|
|
141
|
+
i="$(mk_install)"; c="$(mk_config_with_hosts operations.realagent.network)"
|
|
142
|
+
# Stub that answers 502 with an empty body for every path.
|
|
143
|
+
stub="$(mktemp "${TMPDIR:-/tmp}/brand-cache-curl.XXXXXX")"
|
|
144
|
+
cat > "$stub" <<'STUB'
|
|
145
|
+
#!/usr/bin/env bash
|
|
146
|
+
hdr=""; out=""
|
|
147
|
+
while [ $# -gt 0 ]; do
|
|
148
|
+
case "$1" in
|
|
149
|
+
-D) hdr="$2"; shift 2 ;;
|
|
150
|
+
-o) out="$2"; shift 2 ;;
|
|
151
|
+
-*) shift ;;
|
|
152
|
+
*) shift ;;
|
|
153
|
+
esac
|
|
154
|
+
done
|
|
155
|
+
printf 'HTTP/2 502\r\n\r\n' > "$hdr"
|
|
156
|
+
: > "$out"
|
|
157
|
+
STUB
|
|
158
|
+
chmod +x "$stub"
|
|
159
|
+
err="$(BRAND_CACHE_CURL="$stub" "$CHECK" --install-dir "$i" --config-dir "$c" 2>&1 >/dev/null)"
|
|
160
|
+
rc=$?
|
|
161
|
+
[ "$rc" -eq 1 ] || { echo " expected exit 1, got $rc"; rm -rf "$i" "$c" "$stub"; return 1; }
|
|
162
|
+
echo "$err" | grep -qE "^\[brand-cache\] UNREACHABLE host=operations\.realagent\.network path=/brand/logo\.png http=502$" \
|
|
163
|
+
|| { echo " no UNREACHABLE line; got: $err"; rm -rf "$i" "$c" "$stub"; return 1; }
|
|
164
|
+
rm -rf "$i" "$c" "$stub"; return 0
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case_no_assets_is_a_named_skip_not_a_clean() {
|
|
168
|
+
local i c out rc
|
|
169
|
+
i="$(mktemp -d "${TMPDIR:-/tmp}/brand-cache-install.XXXXXX")"; mkdir -p "$i/server/public"
|
|
170
|
+
c="$(mk_config_with_hosts operations.realagent.network)"
|
|
171
|
+
# bash 3.2 (macOS) errors on "${PATHS[@]}" for an empty array under `set -u`,
|
|
172
|
+
# so this also pins that the loop is never entered with nothing to compare.
|
|
173
|
+
out="$(/bin/bash "$CHECK" --install-dir "$i" --config-dir "$c" 2>&1)"
|
|
174
|
+
rc=$?
|
|
175
|
+
[ "$rc" -eq 0 ] || { echo " expected exit 0, got $rc; got: $out"; rm -rf "$i" "$c"; return 1; }
|
|
176
|
+
echo "$out" | grep -qx "\[brand-cache\] skipped host=operations.realagent.network reason=no-brand-assets" \
|
|
177
|
+
|| { echo " no named skip for an empty asset list; got: $out"; rm -rf "$i" "$c"; return 1; }
|
|
178
|
+
# files=0 must never be emitted: the task's own pinning exists so an empty
|
|
179
|
+
# comparison cannot read as a pass.
|
|
180
|
+
echo "$out" | grep -q "files=0" \
|
|
181
|
+
&& { echo " emitted files=0, which reads as a pass"; rm -rf "$i" "$c"; return 1; }
|
|
182
|
+
rm -rf "$i" "$c"; return 0
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
case_alias_only_install_is_checked_not_skipped() {
|
|
186
|
+
local i c stub edge out rc
|
|
187
|
+
i="$(mk_install)"; c="$(mk_config_alias_only)"
|
|
188
|
+
edge="$(mktemp -d "${TMPDIR:-/tmp}/brand-cache-edge.XXXXXX")"
|
|
189
|
+
cp "$i/server/public/brand/"* "$edge/"
|
|
190
|
+
stub="$(mk_curl_stub "$edge")"
|
|
191
|
+
out="$(BRAND_CACHE_CURL="$stub" "$CHECK" --install-dir "$i" --config-dir "$c" 2>/dev/null)"
|
|
192
|
+
rc=$?
|
|
193
|
+
[ "$rc" -eq 0 ] || { echo " expected exit 0, got $rc"; rm -rf "$i" "$c" "$edge" "$stub"; return 1; }
|
|
194
|
+
# An install with an alias edge and no ingress row does have an edge that can
|
|
195
|
+
# diverge, so reporting `skipped` there would read as nothing-to-check on the
|
|
196
|
+
# one state the skip wording promises it is not.
|
|
197
|
+
echo "$out" | grep -qx "\[brand-cache\] clean host=public.realagent.app files=2" \
|
|
198
|
+
|| { echo " alias-only install was not checked; got: $out"; rm -rf "$i" "$c" "$edge" "$stub"; return 1; }
|
|
199
|
+
rm -rf "$i" "$c" "$edge" "$stub"; return 0
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case_brand_defaults_css_is_not_fetched() {
|
|
203
|
+
local i c stub edge out rc
|
|
204
|
+
i="$(mk_install)"; c="$(mk_config_with_hosts admin.realagent.network public.realagent.network)"
|
|
205
|
+
edge="$(mktemp -d "${TMPDIR:-/tmp}/brand-cache-edge.XXXXXX")"
|
|
206
|
+
cp "$i/server/public/brand/"* "$edge/"
|
|
207
|
+
# No brand-defaults.css at the edge. The gate at server/index.ts:1311-1322
|
|
208
|
+
# exempts /brand/ but not /brand-defaults.css, so through an admin-gated host
|
|
209
|
+
# that path answers with a login page at status 200 — hashing that against
|
|
210
|
+
# the stylesheet would be a permanent false STALE with no cache involved.
|
|
211
|
+
stub="$(mk_curl_stub "$edge")"
|
|
212
|
+
out="$(BRAND_CACHE_CURL="$stub" "$CHECK" --install-dir "$i" --config-dir "$c" 2>/dev/null)"
|
|
213
|
+
rc=$?
|
|
214
|
+
[ "$rc" -eq 0 ] || { echo " expected exit 0, got $rc"; rm -rf "$i" "$c" "$edge" "$stub"; return 1; }
|
|
215
|
+
echo "$out" | grep -qx "\[brand-cache\] clean host=admin.realagent.network files=2" \
|
|
216
|
+
|| { echo " expected only the two /brand/ files; got: $out"; rm -rf "$i" "$c" "$edge" "$stub"; return 1; }
|
|
217
|
+
rm -rf "$i" "$c" "$edge" "$stub"; return 0
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
run_case "clean install reports clean with a pinned file count" case_clean_install_reports_clean
|
|
221
|
+
run_case "stale edge reports STALE on stderr and exits 1" case_stale_edge_reports_stale_on_stderr
|
|
222
|
+
run_case "no tunnel ingress reports skipped and exits 0" case_no_tunnel_reports_skipped
|
|
223
|
+
run_case "localhost/.local-only ingress counts as no tunnel" case_localhost_only_ingress_is_no_tunnel
|
|
224
|
+
run_case "an unfetchable asset is reported, never counted clean" case_unfetchable_asset_is_not_silently_clean
|
|
225
|
+
run_case "an empty asset list is a named skip, never files=0" case_no_assets_is_a_named_skip_not_a_clean
|
|
226
|
+
run_case "an alias-only install is checked, not skipped" case_alias_only_install_is_checked_not_skipped
|
|
227
|
+
run_case "brand-defaults.css is not fetched through a gated host" case_brand_defaults_css_is_not_fetched
|
|
228
|
+
|
|
229
|
+
echo ""
|
|
230
|
+
echo "PASS=$PASS FAIL=$FAIL"
|
|
231
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Task 1996 — an upgrade must not re-deliver an agent the operator switched off.
|
|
3
|
+
#
|
|
4
|
+
# provision_account_dir wipes every core specialist and re-copies it from the
|
|
5
|
+
# bundled templates on every run. Without this, disabling an agent lapses on the
|
|
6
|
+
# next publish: the file reappears in the dispatchable directory, nothing logs,
|
|
7
|
+
# and the card still reads "disabled". That is the no-event failure the task
|
|
8
|
+
# names, so it is pinned here.
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
PLAT_SCRIPTS="$SCRIPT_DIR/.."
|
|
12
|
+
PROJECT_DIR="$(cd "$PLAT_SCRIPTS/.." && pwd)"
|
|
13
|
+
TEMPLATES_DIR="$PROJECT_DIR/templates"
|
|
14
|
+
PASS=0; FAIL=0; FAILED=()
|
|
15
|
+
assert_absent() { if [ ! -e "$1" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$2: expected absent [$1]"); fi; }
|
|
16
|
+
assert_present() { if [ -e "$1" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$2: expected present [$1]"); fi; }
|
|
17
|
+
assert_eq() { if [ "$1" = "$2" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$3: expected [$2] got [$1]"); fi; }
|
|
18
|
+
|
|
19
|
+
. "$PLAT_SCRIPTS/lib/provision-account-dir.sh"
|
|
20
|
+
export TEMPLATES_DIR PROJECT_DIR
|
|
21
|
+
export SCRIPT_DIR="$PLAT_SCRIPTS"
|
|
22
|
+
export USERS_FILE="/nonexistent/users.json"
|
|
23
|
+
|
|
24
|
+
# A real shipped specialist, so the test exercises the same wipe-and-recopy loop
|
|
25
|
+
# a publish runs rather than a synthetic filename.
|
|
26
|
+
DISABLED_AGENT="coding-assistant.md"
|
|
27
|
+
|
|
28
|
+
T1=$(mktemp -d); A1="$T1/acct"
|
|
29
|
+
provision_account_dir "$A1" "dddddddd-dddd-dddd-dddd-dddddddddddd" "house" >/dev/null 2>&1
|
|
30
|
+
assert_present "$A1/specialists/agents/$DISABLED_AGENT" "baseline-agent-delivered"
|
|
31
|
+
|
|
32
|
+
# The operator disables it: the file moves to quarantine and the store names it.
|
|
33
|
+
mkdir -p "$A1/specialists/agents-disabled"
|
|
34
|
+
mv "$A1/specialists/agents/$DISABLED_AGENT" "$A1/specialists/agents-disabled/$DISABLED_AGENT"
|
|
35
|
+
printf '{"disabled":["%s"]}\n' "$DISABLED_AGENT" > "$A1/agents-disabled.json"
|
|
36
|
+
|
|
37
|
+
# The upgrade runs again.
|
|
38
|
+
OUT="$(provision_account_dir "$A1" "dddddddd-dddd-dddd-dddd-dddddddddddd" "house" 2>&1)"
|
|
39
|
+
|
|
40
|
+
assert_absent "$A1/specialists/agents/$DISABLED_AGENT" "disabled-agent-not-redelivered"
|
|
41
|
+
assert_present "$A1/specialists/agents-disabled/$DISABLED_AGENT" "quarantined-copy-survives-upgrade"
|
|
42
|
+
# Every other core specialist is still delivered: the guard removes what the
|
|
43
|
+
# operator named, not the whole set.
|
|
44
|
+
assert_present "$A1/specialists/agents/librarian.md" "other-agents-still-delivered"
|
|
45
|
+
# The removal is reported, so an operator reading the publish output can see
|
|
46
|
+
# which agents provisioning deliberately withheld.
|
|
47
|
+
assert_eq "$(printf '%s' "$OUT" | grep -c "agents-withheld=1")" "1" "withheld-count-logged"
|
|
48
|
+
rm -rf "$T1"
|
|
49
|
+
|
|
50
|
+
# No store, no behaviour change: every core specialist is delivered.
|
|
51
|
+
T2=$(mktemp -d); A2="$T2/acct"
|
|
52
|
+
provision_account_dir "$A2" "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee" "house" >/dev/null 2>&1
|
|
53
|
+
assert_present "$A2/specialists/agents/$DISABLED_AGENT" "no-store-delivers-everything"
|
|
54
|
+
rm -rf "$T2"
|
|
55
|
+
|
|
56
|
+
# A malformed store withholds nothing rather than withholding everything: an
|
|
57
|
+
# unreadable file is not evidence that the operator disabled an agent.
|
|
58
|
+
T3=$(mktemp -d); A3="$T3/acct"
|
|
59
|
+
provision_account_dir "$A3" "ffffffff-ffff-ffff-ffff-ffffffffffff" "house" >/dev/null 2>&1
|
|
60
|
+
printf '{ not json\n' > "$A3/agents-disabled.json"
|
|
61
|
+
OUT3="$(provision_account_dir "$A3" "ffffffff-ffff-ffff-ffff-ffffffffffff" "house" 2>&1)"
|
|
62
|
+
assert_present "$A3/specialists/agents/$DISABLED_AGENT" "malformed-store-withholds-nothing"
|
|
63
|
+
assert_eq "$(printf '%s' "$OUT3" | grep -c 'agents-disabled-store-unreadable')" "1" "malformed-store-reported"
|
|
64
|
+
rm -rf "$T3"
|
|
65
|
+
|
|
66
|
+
# Review finding: the withhold used `rm -f` unconditionally. In the drift state
|
|
67
|
+
# the reconcile check exists to catch — store names it, file is live, quarantine
|
|
68
|
+
# empty — that deletes the only copy of a premium `--` agent or an operator-edited
|
|
69
|
+
# override, because the replace loop skips `*--*` and the copy loop only restores
|
|
70
|
+
# templates. Withholding must move, never destroy.
|
|
71
|
+
T4=$(mktemp -d); A4="$T4/acct"
|
|
72
|
+
provision_account_dir "$A4" "aaaa1111-aaaa-1111-aaaa-111111111111" "house" >/dev/null 2>&1
|
|
73
|
+
mkdir -p "$A4/specialists/agents"
|
|
74
|
+
printf -- '---\nname: quoter\n---\n\nOperator edited this premium agent.\n' > "$A4/specialists/agents/sitedesk--quoter.md"
|
|
75
|
+
printf '{"disabled":["sitedesk--quoter.md"]}\n' > "$A4/agents-disabled.json"
|
|
76
|
+
provision_account_dir "$A4" "aaaa1111-aaaa-1111-aaaa-111111111111" "house" >/dev/null 2>&1
|
|
77
|
+
|
|
78
|
+
assert_absent "$A4/specialists/agents/sitedesk--quoter.md" "premium-withheld-from-dispatch"
|
|
79
|
+
assert_present "$A4/specialists/agents-disabled/sitedesk--quoter.md" "premium-preserved-not-destroyed"
|
|
80
|
+
assert_eq "$(grep -c 'Operator edited this premium agent' "$A4/specialists/agents-disabled/sitedesk--quoter.md" 2>/dev/null || echo 0)" "1" "premium-content-intact"
|
|
81
|
+
rm -rf "$T4"
|
|
82
|
+
|
|
83
|
+
echo "PASS=$PASS FAIL=$FAIL"
|
|
84
|
+
if [ "$FAIL" -gt 0 ]; then printf '%s\n' "${FAILED[@]}"; exit 1; fi
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-brand-cache.sh — reconcile the bytes the public hostname returns
|
|
3
|
+
# against the bytes on disk (Task 2019).
|
|
4
|
+
#
|
|
5
|
+
# The failure this exists for emits no event at either end. The installer
|
|
6
|
+
# writes the new brand bytes and logs a successful upgrade; the origin serves
|
|
7
|
+
# them correctly to anyone asking it directly; Cloudflare's edge serves the
|
|
8
|
+
# pre-upgrade ones and reports `HIT` to nobody looking. There is no action to
|
|
9
|
+
# log and re-running the upgrade does not surface it, so only a reconciliation
|
|
10
|
+
# of served bytes against on-disk bytes reveals the state. That is the
|
|
11
|
+
# condition behind the 2026-07-26 report, where the regenerated Real Agent logo
|
|
12
|
+
# was correct on disk and invisible through the hostname for four hours.
|
|
13
|
+
#
|
|
14
|
+
# $ check-brand-cache.sh --install-dir <dir> --config-dir <dir>
|
|
15
|
+
#
|
|
16
|
+
# stdout, exit 0: [brand-cache] clean host=<h> files=<n>
|
|
17
|
+
# stdout, exit 0: [brand-cache] skipped host=none reason=no-tunnel-ingress
|
|
18
|
+
# stderr, exit 1: [brand-cache] STALE host=<h> path=<p> edge=<sha7> disk=<sha7> cf-cache-status=<s> age=<n>
|
|
19
|
+
# stderr, exit 1: [brand-cache] UNREACHABLE host=<h> path=<p> http=<code>
|
|
20
|
+
#
|
|
21
|
+
# No ingress hostname and no alias domain means no edge and therefore no
|
|
22
|
+
# possible divergence, so that is a skip, not a pass — a caller must never read
|
|
23
|
+
# the two as the same.
|
|
24
|
+
#
|
|
25
|
+
# Scope: the files under server/public/brand/ only. See the PATHS block below.
|
|
26
|
+
#
|
|
27
|
+
# BRAND_CACHE_CURL overrides the fetch command (tests stub it).
|
|
28
|
+
set -uo pipefail
|
|
29
|
+
|
|
30
|
+
INSTALL_DIR=""
|
|
31
|
+
CONFIG_DIR=""
|
|
32
|
+
while [ $# -gt 0 ]; do
|
|
33
|
+
case "$1" in
|
|
34
|
+
--install-dir) INSTALL_DIR="$2"; shift 2 ;;
|
|
35
|
+
--config-dir) CONFIG_DIR="$2"; shift 2 ;;
|
|
36
|
+
*) echo "[brand-cache] unknown argument: $1" >&2; exit 2 ;;
|
|
37
|
+
esac
|
|
38
|
+
done
|
|
39
|
+
if [ -z "$INSTALL_DIR" ] || [ -z "$CONFIG_DIR" ]; then
|
|
40
|
+
echo "[brand-cache] usage: check-brand-cache.sh --install-dir <dir> --config-dir <dir>" >&2
|
|
41
|
+
exit 2
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
CURL="${BRAND_CACHE_CURL:-curl}"
|
|
45
|
+
|
|
46
|
+
# sha256sum is GNU; darwin ships shasum. Both print "<digest> <path>".
|
|
47
|
+
sha256() {
|
|
48
|
+
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}'
|
|
49
|
+
else shasum -a 256 "$1" | awk '{print $1}'; fi
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# The hostname to probe, read from the same two sources and with the same
|
|
53
|
+
# exclusions as platform/plugins/admin/mcp/src/lib/public-hostname.ts:
|
|
54
|
+
# cloudflared/config.yml first, then alias-domains.json. First non-excluded
|
|
55
|
+
# wins within each source.
|
|
56
|
+
#
|
|
57
|
+
# No apex tiebreak, and no attempt to pick the *public* host over the admin
|
|
58
|
+
# one: only /brand/ assets are fetched, and the admin gate exempts /brand/ on
|
|
59
|
+
# every host class (server/index.ts:1311-1322), so any ingress host serves them
|
|
60
|
+
# identically. The alias source is read because an install whose edge is an
|
|
61
|
+
# alias domain with no ingress row does have an edge that can diverge, and
|
|
62
|
+
# reporting `skipped` there would read as nothing-to-check.
|
|
63
|
+
resolve_host() {
|
|
64
|
+
local cfg="$CONFIG_DIR/cloudflared/config.yml" alias="$CONFIG_DIR/alias-domains.json" h
|
|
65
|
+
if [ -f "$cfg" ]; then
|
|
66
|
+
h="$(sed -n 's/^[[:space:]]*-[[:space:]]*hostname:[[:space:]]*\([^[:space:]]*\).*/\1/p' "$cfg" \
|
|
67
|
+
| grep -v '^localhost$' | grep -v '\.local$' | head -1)"
|
|
68
|
+
if [ -n "$h" ]; then echo "$h"; return 0; fi
|
|
69
|
+
fi
|
|
70
|
+
[ -f "$alias" ] || return 0
|
|
71
|
+
tr ',' '\n' < "$alias" | sed -n 's/.*"\([^"]*\)".*/\1/p' \
|
|
72
|
+
| grep -v '^localhost$' | grep -v '\.local$' | head -1
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
HOST="$(resolve_host)"
|
|
76
|
+
if [ -z "$HOST" ]; then
|
|
77
|
+
echo "[brand-cache] skipped host=none reason=no-tunnel-ingress"
|
|
78
|
+
exit 0
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# /brand/ only. /brand-defaults.css is NOT exempt from the admin gate
|
|
82
|
+
# (server/index.ts:1311-1322 lists /favicon.ico, /assets/, /brand/,
|
|
83
|
+
# /api/calendar/free-busy and the PWA paths, and nothing else), so through an
|
|
84
|
+
# admin-gated or operator host it answers with a login page at status 200
|
|
85
|
+
# (index.ts:1379). Hashing that against the stylesheet on disk would be a
|
|
86
|
+
# permanent STALE with no cache involved, which would both bury a real
|
|
87
|
+
# divergence and make the clean line unreachable. Covering it, and the page
|
|
88
|
+
# shells, needs a resolved public hostname — Task 2026.
|
|
89
|
+
PUBLIC_DIR="$INSTALL_DIR/server/public"
|
|
90
|
+
PATHS=()
|
|
91
|
+
if [ -d "$PUBLIC_DIR/brand" ]; then
|
|
92
|
+
while IFS= read -r f; do PATHS+=("/brand/$(basename "$f")"); done \
|
|
93
|
+
< <(find "$PUBLIC_DIR/brand" -maxdepth 1 -type f | sort)
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# Nothing to compare is a skip with its own reason, not a clean run. Emitting
|
|
97
|
+
# `files=0` here would read as a pass on an install whose payload never
|
|
98
|
+
# deployed, and that is the dead signal the file count is pinned against.
|
|
99
|
+
# Also the guard for bash 3.2 (darwin), where "${PATHS[@]}" on an empty array
|
|
100
|
+
# is an unbound-variable error under `set -u`.
|
|
101
|
+
if [ "${#PATHS[@]}" -eq 0 ]; then
|
|
102
|
+
echo "[brand-cache] skipped host=$HOST reason=no-brand-assets"
|
|
103
|
+
exit 0
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
TMP="$(mktemp -d "${TMPDIR:-/tmp}/brand-cache.XXXXXX")"
|
|
107
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
108
|
+
|
|
109
|
+
FAILED=0
|
|
110
|
+
CHECKED=0
|
|
111
|
+
for p in "${PATHS[@]}"; do
|
|
112
|
+
body="$TMP/body"; hdr="$TMP/hdr"
|
|
113
|
+
"$CURL" -sS -L -D "$hdr" -o "$body" "https://$HOST$p" >/dev/null 2>&1
|
|
114
|
+
code="$(awk 'tolower($0) ~ /^http\// {c=$2} END{print c+0}' "$hdr" 2>/dev/null)"
|
|
115
|
+
if [ "$code" != "200" ]; then
|
|
116
|
+
echo "[brand-cache] UNREACHABLE host=$HOST path=$p http=$code" >&2
|
|
117
|
+
FAILED=1
|
|
118
|
+
continue
|
|
119
|
+
fi
|
|
120
|
+
edge="$(sha256 "$body")"
|
|
121
|
+
disk="$(sha256 "$PUBLIC_DIR$p")"
|
|
122
|
+
if [ "$edge" != "$disk" ]; then
|
|
123
|
+
# tail -1, not head -1: -L means $hdr can hold several header blocks, and
|
|
124
|
+
# the cache state that matters is the one on the response the bytes came
|
|
125
|
+
# from. head -1 would name a redirect hop's cache state instead, which is
|
|
126
|
+
# the field an operator reads to tell this fault from an origin fault.
|
|
127
|
+
status="$(awk 'tolower($1) == "cf-cache-status:" {print $2}' "$hdr" | tr -d '\r' | tail -1)"
|
|
128
|
+
age="$(awk 'tolower($1) == "age:" {print $2}' "$hdr" | tr -d '\r' | tail -1)"
|
|
129
|
+
echo "[brand-cache] STALE host=$HOST path=$p edge=${edge:0:7} disk=${disk:0:7} cf-cache-status=${status:-none} age=${age:-0}" >&2
|
|
130
|
+
FAILED=1
|
|
131
|
+
continue
|
|
132
|
+
fi
|
|
133
|
+
CHECKED=$((CHECKED + 1))
|
|
134
|
+
done
|
|
135
|
+
|
|
136
|
+
[ "$FAILED" -eq 1 ] && exit 1
|
|
137
|
+
echo "[brand-cache] clean host=$HOST files=$CHECKED"
|
|
138
|
+
exit 0
|
|
@@ -92,13 +92,18 @@ function collectPluginDirs() {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/** Independent of tool-surface.ts on purpose: a gate that shares the parser it
|
|
95
|
-
* is checking cannot detect that parser drifting.
|
|
95
|
+
* is checking cannot detect that parser drifting.
|
|
96
|
+
*
|
|
97
|
+
* Task 1998 — `hidden:` entries carry the same name+riskClass shape as
|
|
98
|
+
* `tools:` entries and are served by the same MCP server, so a hidden tool
|
|
99
|
+
* can contradict its own name exactly as a visible one can. Scanning only
|
|
100
|
+
* `tools:` would leave eight declarations outside the gate. */
|
|
96
101
|
function declaredTools(manifestPath) {
|
|
97
102
|
const out = []
|
|
98
103
|
let current = null
|
|
99
104
|
let inTools = false
|
|
100
105
|
for (const line of readFileSync(manifestPath, 'utf8').split(/\r?\n/)) {
|
|
101
|
-
if (/^tools:\s*$/.test(line)) { inTools = true; continue }
|
|
106
|
+
if (/^(tools|hidden):\s*$/.test(line)) { inTools = true; current = null; continue }
|
|
102
107
|
if (inTools && /^[A-Za-z_]/.test(line)) inTools = false
|
|
103
108
|
if (!inTools) continue
|
|
104
109
|
const name = line.match(/^\s+-\s+name:\s*(.+?)\s*$/)
|
|
@@ -88,8 +88,11 @@ function parsePluginManifest(rawManifest) {
|
|
|
88
88
|
if (nameLine) { tools.push(nameLine[1]); continue; }
|
|
89
89
|
}
|
|
90
90
|
if (mode === 'hidden') {
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
// Task 1998 — hidden entries carry the same `- name:` + `riskClass:`
|
|
92
|
+
// shape as tools entries. Names only here; the gate checks membership
|
|
93
|
+
// of the `all` surface, not consequence.
|
|
94
|
+
const nameLine = line.match(/^\s+-\s+name:\s*(.+?)\s*$/);
|
|
95
|
+
if (nameLine) hidden.push(nameLine[1]);
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
98
|
return { tools, hidden, hasTools };
|
|
@@ -36,9 +36,12 @@ function extractFrontmatter(md) {
|
|
|
36
36
|
return md.slice(3, end)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// Collect `- name: <x>` entries under a bare `tools:` block and
|
|
40
|
-
//
|
|
41
|
-
//
|
|
39
|
+
// Collect `- name: <x>` entries under a bare `tools:` block and under a bare
|
|
40
|
+
// `hidden:` block. Both contribute names a specialist may list in its `tools:`
|
|
41
|
+
// frontmatter, so both belong to the live set. Task 1998 gave the hidden block
|
|
42
|
+
// the same name+riskClass shape as tools, so the two branches now read alike;
|
|
43
|
+
// this parser stays separate from tool-surface.ts on purpose, so a drift in one
|
|
44
|
+
// shows up as a diff rather than as agreement between a parser and itself.
|
|
42
45
|
function parseToolNames(frontmatter) {
|
|
43
46
|
const lines = frontmatter.split('\n')
|
|
44
47
|
const names = []
|
|
@@ -57,7 +60,7 @@ function parseToolNames(frontmatter) {
|
|
|
57
60
|
const m = line.match(/^\s+-\s+name:\s*(.+?)\s*$/)
|
|
58
61
|
if (m) names.push(m[1])
|
|
59
62
|
} else if (mode === 'hidden') {
|
|
60
|
-
const m = line.match(/^\s+-\s+(.+?)\s*$/)
|
|
63
|
+
const m = line.match(/^\s+-\s+name:\s*(.+?)\s*$/)
|
|
61
64
|
if (m) names.push(m[1])
|
|
62
65
|
}
|
|
63
66
|
}
|
|
@@ -251,6 +251,49 @@ SETTINGS_EOF
|
|
|
251
251
|
[ -f "$specialist" ] && cp "$specialist" "$ACCOUNT_DIR/specialists/agents/$(basename "$specialist")"
|
|
252
252
|
done
|
|
253
253
|
|
|
254
|
+
# Task 1996 — the copy loop above re-delivers every core specialist, which
|
|
255
|
+
# would silently undo a disable on the next publish. specialists/agents/ is
|
|
256
|
+
# the dispatchable set (spawn-context.ts reads it), so an agent the operator
|
|
257
|
+
# switched off is removed again here. The quarantined copy under
|
|
258
|
+
# specialists/agents-disabled/ is never touched: it is the operator's own
|
|
259
|
+
# file, and for a premium or edited agent it is the only copy.
|
|
260
|
+
#
|
|
261
|
+
# An unreadable store withholds NOTHING. A corrupt file is not evidence that
|
|
262
|
+
# an agent was disabled, and withholding the whole set on a parse error would
|
|
263
|
+
# turn a bad byte into a fleet-wide loss of specialists.
|
|
264
|
+
if [ -f "$ACCOUNT_DIR/agents-disabled.json" ]; then
|
|
265
|
+
local _disabled_names _withheld=0
|
|
266
|
+
if _disabled_names="$(python3 -c '
|
|
267
|
+
import json, sys
|
|
268
|
+
with open(sys.argv[1]) as fh:
|
|
269
|
+
names = json.load(fh)["disabled"]
|
|
270
|
+
if not isinstance(names, list):
|
|
271
|
+
raise SystemExit(1)
|
|
272
|
+
for n in names:
|
|
273
|
+
if isinstance(n, str) and n.endswith(".md") and "/" not in n and ".." not in n:
|
|
274
|
+
print(n)
|
|
275
|
+
' "$ACCOUNT_DIR/agents-disabled.json" 2>/dev/null)"; then
|
|
276
|
+
local _name
|
|
277
|
+
while IFS= read -r _name; do
|
|
278
|
+
[ -n "$_name" ] || continue
|
|
279
|
+
if [ -e "$ACCOUNT_DIR/specialists/agents/$_name" ]; then
|
|
280
|
+
# MOVE, never delete. In the drift state the standing parity check
|
|
281
|
+
# exists to catch (store names it, file is live, quarantine empty) a
|
|
282
|
+
# delete would destroy the only copy of a premium `--` agent or an
|
|
283
|
+
# operator-edited override: the replace loop above skips `*--*` and
|
|
284
|
+
# the copy loop only restores templates, so nothing re-delivers it.
|
|
285
|
+
mkdir -p "$ACCOUNT_DIR/specialists/agents-disabled"
|
|
286
|
+
mv -f "$ACCOUNT_DIR/specialists/agents/$_name" \
|
|
287
|
+
"$ACCOUNT_DIR/specialists/agents-disabled/$_name"
|
|
288
|
+
_withheld=$((_withheld + 1))
|
|
289
|
+
fi
|
|
290
|
+
done <<< "$_disabled_names"
|
|
291
|
+
echo " specialists agents-withheld=$_withheld (disabled by operator)"
|
|
292
|
+
else
|
|
293
|
+
echo " specialists agents-disabled-store-unreadable — withholding nothing"
|
|
294
|
+
fi
|
|
295
|
+
fi
|
|
296
|
+
|
|
254
297
|
rm -f "$ACCOUNT_DIR/.claude/agents/"*.md 2>/dev/null || true
|
|
255
298
|
|
|
256
299
|
# SOUL.md — user-controlled. Only create if missing. Never overwrite.
|
package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface ReconcileResult extends ReconcileCounts {
|
|
|
15
15
|
/** The single per-run summary line body. A non-zero count is the standing signal
|
|
16
16
|
* that an agent authored folders the hard guards could not prevent. Pure
|
|
17
17
|
* formatter so the emitter and its test share one wording. */
|
|
18
|
-
export declare function formatReconcileLine(c: ReconcileCounts, homeMissing?: number, homeUnaudited?: number): string;
|
|
18
|
+
export declare function formatReconcileLine(c: ReconcileCounts, homeMissing?: number, homeUnaudited?: number, agentDrift?: number): string;
|
|
19
19
|
/** Parse the fenced ```allowed-top-level block from an account's SCHEMA.md —
|
|
20
20
|
* the same structured set fs-schema-guard.sh parses with awk. Returns the
|
|
21
21
|
* entry names (blank lines dropped). An absent block yields an empty set. */
|
package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-dir-schema-reconcile.d.ts","sourceRoot":"","sources":["../src/account-dir-schema-reconcile.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"account-dir-schema-reconcile.d.ts","sourceRoot":"","sources":["../src/account-dir-schema-reconcile.ts"],"names":[],"mappings":"AA2FA,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD;;;;;0CAKsC;IACtC,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;+DAE+D;AAC/D,wBAAgB,mBAAmB,CACjC,CAAC,EAAE,eAAe,EAClB,WAAW,SAAI,EACf,aAAa,SAAI,EACjB,UAAU,SAAI,GACb,MAAM,CAGR;AAED;;8EAE8E;AAC9E,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAcpE;AA+ED;;;8EAG8E;AAC9E,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,CAoCpE;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAC/B;AA0DD;;;;;;;;;;;;;iCAaiC;AACjC,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CA8DtD;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,IAAI,IAAI,CAAA;IACb,IAAI,IAAI,IAAI,CAAA;CACb;AAED;oDACoD;AACpD,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,aAAa,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC3C,yBAAyB,CAiB3B"}
|