@rubytech/create-sitedesk-code 0.1.506 → 0.1.508
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 +2 -2
- package/payload/platform/config/brand.json +101 -18
- package/payload/platform/lib/account-schema-regions/dist/index.d.ts +17 -5
- package/payload/platform/lib/account-schema-regions/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/account-schema-regions/dist/index.js +20 -6
- package/payload/platform/lib/account-schema-regions/dist/index.js.map +1 -1
- package/payload/platform/lib/account-schema-regions/src/index.ts +20 -6
- package/payload/platform/plugins/admin/PLUGIN.md +46 -5
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +138 -14
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +14 -0
- package/payload/platform/plugins/aeo/PLUGIN.md +3 -0
- package/payload/platform/plugins/browser/PLUGIN.md +18 -0
- package/payload/platform/plugins/connector/PLUGIN.md +4 -0
- package/payload/platform/plugins/contacts/PLUGIN.md +9 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +111 -13
- package/payload/platform/plugins/docs/references/plugins-guide.md +26 -0
- package/payload/platform/plugins/email/PLUGIN.md +17 -0
- package/payload/platform/plugins/filesystem/PLUGIN.md +6 -0
- package/payload/platform/plugins/google/PLUGIN.md +12 -0
- package/payload/platform/plugins/graph/PLUGIN.md +3 -0
- package/payload/platform/plugins/graph-viewer/PLUGIN.md +1 -0
- package/payload/platform/plugins/ledger/PLUGIN.md +7 -0
- package/payload/platform/plugins/memory/PLUGIN.md +47 -3
- package/payload/platform/plugins/outlook/PLUGIN.md +24 -0
- package/payload/platform/plugins/quickbooks/PLUGIN.md +15 -0
- package/payload/platform/plugins/replicate/PLUGIN.md +3 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +10 -0
- package/payload/platform/plugins/storage-broker/PLUGIN.md +12 -0
- package/payload/platform/plugins/telegram/PLUGIN.md +3 -0
- package/payload/platform/plugins/url-get/PLUGIN.md +1 -0
- package/payload/platform/plugins/voice-mirror/PLUGIN.md +5 -0
- package/payload/platform/plugins/whatsapp/PLUGIN.md +12 -0
- package/payload/platform/plugins/work/PLUGIN.md +15 -0
- package/payload/platform/plugins/workflows/PLUGIN.md +8 -0
- package/payload/platform/scripts/__tests__/provision-honours-disabled-agents.test.sh +84 -0
- package/payload/platform/scripts/check-risk-class.mjs +150 -0
- package/payload/platform/scripts/check-specialist-tool-surface.mjs +5 -2
- package/payload/platform/scripts/lib/__pycache__/account-schema-owned-dirs.cpython-314.pyc +0 -0
- package/payload/platform/scripts/lib/canonical-tool-names.mjs +7 -4
- package/payload/platform/scripts/lib/provision-account-dir.sh +47 -1
- 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/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +4 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +8 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/tool-surface.d.ts +32 -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 +83 -19
- package/payload/platform/services/claude-session-manager/dist/tool-surface.js.map +1 -1
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/PLUGIN.md +4 -0
- package/payload/server/{chunk-5ADBNEJA.js → chunk-2WAXM5N2.js} +111 -111
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/{AdminLoginScreens-D5KOQA8S.js → AdminLoginScreens-DKeCt1uf.js} +1 -1
- package/payload/server/public/assets/{AdminShell-C8wazpnz.js → AdminShell-HeELCCjJ.js} +1 -1
- package/payload/server/public/assets/{activity-CFqkDhTs.js → activity-BEIDAxDA.js} +1 -1
- package/payload/server/public/assets/admin-C0-WU1sf.js +1 -0
- package/payload/server/public/assets/agents-DjUvMFTj.js +1 -0
- package/payload/server/public/assets/{browser-T_5xbnJh.js → browser-GkVTGqlb.js} +1 -1
- package/payload/server/public/assets/{calendar-CKjuiG8c.js → calendar-CSqJeFEg.js} +1 -1
- package/payload/server/public/assets/chat-C0Ps84U5.js +1 -0
- package/payload/server/public/assets/chevron-left-DQ2uNlu9.js +1 -0
- package/payload/server/public/assets/chevron-right-C4QCiRGD.js +1 -0
- package/payload/server/public/assets/clock-BEeQ7JxX.js +1 -0
- package/payload/server/public/assets/data-CGuNfym7.js +1 -0
- package/payload/server/public/assets/{file-text-c6cA4Evd.js → file-text-DiscM2pP.js} +1 -1
- package/payload/server/public/assets/{graph-DIk-o45J.js → graph-CRP5DM8C.js} +1 -1
- package/payload/server/public/assets/{graph-labels-5bjxhC_p.js → graph-labels-Du-6KoBE.js} +1 -1
- package/payload/server/public/assets/{maximize-2-BCQkFb1_.js → maximize-2-D-H_uIV-.js} +1 -1
- package/payload/server/public/assets/{operator-0UnNo2ms.js → operator-D5rA76YY.js} +1 -1
- package/payload/server/public/assets/page-CHK1415C.js +1 -0
- package/payload/server/public/assets/{page-HVtUfub2.js → page-DZE3H_n4.js} +4 -4
- package/payload/server/public/assets/{public-CKEYyIPn.js → public-DFHXVkdD.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-dTpO5dhL.js → rotate-ccw-CesEJsuO.js} +1 -1
- package/payload/server/public/assets/{routines-C6P1-Dto.js → routines-DYMYY_r8.js} +1 -1
- package/payload/server/public/assets/{skills-ClrpVqlz.js → skills-67jtJ16D.js} +1 -1
- package/payload/server/public/assets/{tasks-C8SNhSFC.js → tasks-D2C2a-uk.js} +1 -1
- package/payload/server/public/assets/{time-entry-format-CCO6N6XZ.js → time-entry-format-BixTGQAd.js} +1 -1
- package/payload/server/public/assets/{triangle-alert-CHjdKpag.js → triangle-alert-Co9RWb9q.js} +1 -1
- package/payload/server/public/assets/{useCopyFeedback-BTUu3-l8.js → useCopyFeedback-7zwf9_w4.js} +1 -1
- package/payload/server/public/assets/useSubAccountSwitcher-Bg72hdQ8.js +9 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DE4v5_Fz.css +1 -0
- package/payload/server/public/assets/{useVoiceRecorder-BeNqxmm2.js → useVoiceRecorder-CZDDiwMD.js} +1 -1
- package/payload/server/public/assets/{wrench-CeNnc7oO.js → wrench-CjQqdj4P.js} +1 -1
- package/payload/server/public/brand/fonts/inter-400.woff2 +0 -0
- package/payload/server/public/brand/fonts/inter-500.woff2 +0 -0
- 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 +83 -5
- 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 +1584 -837
- package/payload/server/public/assets/admin-B-EGUWS4.js +0 -1
- package/payload/server/public/assets/agents-DPulfmrd.js +0 -1
- package/payload/server/public/assets/chat-DWLYAA70.js +0 -1
- package/payload/server/public/assets/chevron-left-DBMCTKlT.js +0 -1
- package/payload/server/public/assets/chevron-right-SBAr6zzc.js +0 -1
- package/payload/server/public/assets/clock-kkzt7Scf.js +0 -1
- package/payload/server/public/assets/data-CV8N9qcy.js +0 -1
- package/payload/server/public/assets/page-JJa2xdzf.js +0 -1
- package/payload/server/public/assets/useSubAccountSwitcher-DSW9vDi2.js +0 -9
- package/payload/server/public/assets/useSubAccountSwitcher-DyrH8DA8.css +0 -1
|
@@ -7,18 +7,23 @@ tools:
|
|
|
7
7
|
- name: voice-tag-content
|
|
8
8
|
publicAllowlist: false
|
|
9
9
|
adminAllowlist: true
|
|
10
|
+
riskClass: write_local
|
|
10
11
|
- name: voice-distil-profile
|
|
11
12
|
publicAllowlist: false
|
|
12
13
|
adminAllowlist: true
|
|
14
|
+
riskClass: write_local
|
|
13
15
|
- name: voice-retrieve-conditioning
|
|
14
16
|
publicAllowlist: false
|
|
15
17
|
adminAllowlist: false
|
|
18
|
+
riskClass: read
|
|
16
19
|
- name: voice-record-feedback
|
|
17
20
|
publicAllowlist: false
|
|
18
21
|
adminAllowlist: true
|
|
22
|
+
riskClass: write_local
|
|
19
23
|
- name: voice-ingest-session-text
|
|
20
24
|
publicAllowlist: false
|
|
21
25
|
adminAllowlist: true
|
|
26
|
+
riskClass: write_local
|
|
22
27
|
mcp:
|
|
23
28
|
command: node
|
|
24
29
|
args:
|
|
@@ -5,39 +5,51 @@ tools:
|
|
|
5
5
|
- name: whatsapp-login-start
|
|
6
6
|
publicAllowlist: false
|
|
7
7
|
adminAllowlist: false
|
|
8
|
+
riskClass: external
|
|
8
9
|
- name: whatsapp-login-wait
|
|
9
10
|
publicAllowlist: false
|
|
10
11
|
adminAllowlist: false
|
|
12
|
+
riskClass: external
|
|
11
13
|
- name: whatsapp-status
|
|
12
14
|
publicAllowlist: false
|
|
13
15
|
adminAllowlist: false
|
|
16
|
+
riskClass: read
|
|
14
17
|
- name: whatsapp-disconnect
|
|
15
18
|
publicAllowlist: false
|
|
16
19
|
adminAllowlist: false
|
|
20
|
+
riskClass: external
|
|
17
21
|
- name: whatsapp-config
|
|
18
22
|
publicAllowlist: false
|
|
19
23
|
adminAllowlist: false
|
|
24
|
+
riskClass: write_local
|
|
20
25
|
- name: whatsapp-activity
|
|
21
26
|
publicAllowlist: false
|
|
22
27
|
adminAllowlist: false
|
|
28
|
+
riskClass: read
|
|
23
29
|
- name: whatsapp-conversations
|
|
24
30
|
publicAllowlist: false
|
|
25
31
|
adminAllowlist: false
|
|
32
|
+
riskClass: read
|
|
26
33
|
- name: whatsapp-messages
|
|
27
34
|
publicAllowlist: false
|
|
28
35
|
adminAllowlist: false
|
|
36
|
+
riskClass: read
|
|
29
37
|
- name: whatsapp-conversation-graph-state
|
|
30
38
|
publicAllowlist: false
|
|
31
39
|
adminAllowlist: false
|
|
40
|
+
riskClass: read
|
|
32
41
|
- name: whatsapp-group-info
|
|
33
42
|
publicAllowlist: false
|
|
34
43
|
adminAllowlist: false
|
|
44
|
+
riskClass: read
|
|
35
45
|
- name: whatsapp-send-admin
|
|
36
46
|
publicAllowlist: false
|
|
37
47
|
adminAllowlist: false
|
|
48
|
+
riskClass: external
|
|
38
49
|
- name: whatsapp-reply
|
|
39
50
|
publicAllowlist: false
|
|
40
51
|
adminAllowlist: false
|
|
52
|
+
riskClass: external
|
|
41
53
|
metadata: {"platform":{"embed":["admin"]}}
|
|
42
54
|
mcp:
|
|
43
55
|
command: node
|
|
@@ -5,48 +5,63 @@ tools:
|
|
|
5
5
|
- name: work-create
|
|
6
6
|
publicAllowlist: false
|
|
7
7
|
adminAllowlist: false
|
|
8
|
+
riskClass: write_local
|
|
8
9
|
- name: work-update
|
|
9
10
|
publicAllowlist: false
|
|
10
11
|
adminAllowlist: false
|
|
12
|
+
riskClass: write_local
|
|
11
13
|
- name: work-list
|
|
12
14
|
publicAllowlist: false
|
|
13
15
|
adminAllowlist: false
|
|
16
|
+
riskClass: read
|
|
14
17
|
- name: work-get
|
|
15
18
|
publicAllowlist: false
|
|
16
19
|
adminAllowlist: false
|
|
20
|
+
riskClass: read
|
|
17
21
|
- name: work-relate
|
|
18
22
|
publicAllowlist: false
|
|
19
23
|
adminAllowlist: false
|
|
24
|
+
riskClass: write_local
|
|
20
25
|
- name: work-complete
|
|
21
26
|
publicAllowlist: false
|
|
22
27
|
adminAllowlist: false
|
|
28
|
+
riskClass: write_local
|
|
23
29
|
- name: work-ready
|
|
24
30
|
publicAllowlist: false
|
|
25
31
|
adminAllowlist: false
|
|
32
|
+
riskClass: read
|
|
26
33
|
- name: session-list
|
|
27
34
|
publicAllowlist: false
|
|
28
35
|
adminAllowlist: false
|
|
36
|
+
riskClass: read
|
|
29
37
|
- name: session-name
|
|
30
38
|
publicAllowlist: false
|
|
31
39
|
adminAllowlist: false
|
|
40
|
+
riskClass: write_local
|
|
32
41
|
- name: session-metering
|
|
33
42
|
publicAllowlist: false
|
|
34
43
|
adminAllowlist: false
|
|
44
|
+
riskClass: read
|
|
35
45
|
- name: project-create
|
|
36
46
|
publicAllowlist: false
|
|
37
47
|
adminAllowlist: false
|
|
48
|
+
riskClass: write_local
|
|
38
49
|
- name: project-list
|
|
39
50
|
publicAllowlist: false
|
|
40
51
|
adminAllowlist: false
|
|
52
|
+
riskClass: read
|
|
41
53
|
- name: project-get
|
|
42
54
|
publicAllowlist: false
|
|
43
55
|
adminAllowlist: false
|
|
56
|
+
riskClass: read
|
|
44
57
|
- name: project-update
|
|
45
58
|
publicAllowlist: false
|
|
46
59
|
adminAllowlist: false
|
|
60
|
+
riskClass: write_local
|
|
47
61
|
- name: project-complete
|
|
48
62
|
publicAllowlist: false
|
|
49
63
|
adminAllowlist: false
|
|
64
|
+
riskClass: write_local
|
|
50
65
|
metadata: {"platform":{"always":false,"embed":[],"pluginKey":"work"}}
|
|
51
66
|
skills:
|
|
52
67
|
- skills/execute-task/SKILL.md
|
|
@@ -5,27 +5,35 @@ tools:
|
|
|
5
5
|
- name: workflow-create
|
|
6
6
|
publicAllowlist: false
|
|
7
7
|
adminAllowlist: false
|
|
8
|
+
riskClass: write_local
|
|
8
9
|
- name: workflow-list
|
|
9
10
|
publicAllowlist: false
|
|
10
11
|
adminAllowlist: false
|
|
12
|
+
riskClass: read
|
|
11
13
|
- name: workflow-get
|
|
12
14
|
publicAllowlist: false
|
|
13
15
|
adminAllowlist: false
|
|
16
|
+
riskClass: read
|
|
14
17
|
- name: workflow-update
|
|
15
18
|
publicAllowlist: false
|
|
16
19
|
adminAllowlist: false
|
|
20
|
+
riskClass: write_local
|
|
17
21
|
- name: workflow-delete
|
|
18
22
|
publicAllowlist: false
|
|
19
23
|
adminAllowlist: false
|
|
24
|
+
riskClass: write_local
|
|
20
25
|
- name: workflow-validate
|
|
21
26
|
publicAllowlist: false
|
|
22
27
|
adminAllowlist: false
|
|
28
|
+
riskClass: read
|
|
23
29
|
- name: workflow-execute
|
|
24
30
|
publicAllowlist: false
|
|
25
31
|
adminAllowlist: false
|
|
32
|
+
riskClass: exec
|
|
26
33
|
- name: workflow-runs
|
|
27
34
|
publicAllowlist: false
|
|
28
35
|
adminAllowlist: false
|
|
36
|
+
riskClass: read
|
|
29
37
|
metadata: {"platform":{"always":false,"embed":[],"pluginKey":"workflows"}}
|
|
30
38
|
mcp:
|
|
31
39
|
command: node
|
|
@@ -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,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Publish-time gate: flag any `riskClass` declaration that contradicts what the
|
|
3
|
+
// tool's own name says it does.
|
|
4
|
+
//
|
|
5
|
+
// The session manager throws at boot when a riskClass is absent or off-enum
|
|
6
|
+
// (tool-surface.ts 'risk-class-missing' / 'risk-class-invalid'). It cannot
|
|
7
|
+
// catch a class that is present, valid, and wrong — `whatsapp-send` declared
|
|
8
|
+
// `read` parses fine, logs nothing, and stays invisible until a downstream
|
|
9
|
+
// gate acts on it. That is a no-event failure, so it needs a standing check
|
|
10
|
+
// rather than an event.
|
|
11
|
+
//
|
|
12
|
+
// The gate reports; it never rewrites a declaration. A contradiction is
|
|
13
|
+
// resolved either by correcting the manifest or by adding a named exception
|
|
14
|
+
// below with the reason it is not a mistake. An exception whose tool no longer
|
|
15
|
+
// contradicts is itself reported: a suppression that outlives its cause
|
|
16
|
+
// re-creates the blind spot the gate exists to remove.
|
|
17
|
+
|
|
18
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'
|
|
19
|
+
import { dirname, join, resolve } from 'node:path'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
23
|
+
const MAXY_CODE_ROOT = resolve(__dirname, '..', '..')
|
|
24
|
+
|
|
25
|
+
const PLUGIN_ROOTS = [
|
|
26
|
+
{ root: join(MAXY_CODE_ROOT, 'platform', 'plugins'), depth: 1 },
|
|
27
|
+
{ root: join(MAXY_CODE_ROOT, 'premium-plugins'), depth: 1 },
|
|
28
|
+
{ root: join(MAXY_CODE_ROOT, 'premium-plugins'), depth: 2, mid: 'plugins' },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
// A name that says the call sends something outward, declared as if it stays
|
|
32
|
+
// on the device.
|
|
33
|
+
const OUTWARD_SIGNAL = /-(send|reply|publish|deploy|delete|post)\b/
|
|
34
|
+
const OUTWARD_CONTRADICTS = new Set(['read', 'write_local'])
|
|
35
|
+
|
|
36
|
+
// A name that says the call only looks at something, declared as if it acts.
|
|
37
|
+
const INWARD_SIGNAL = /-(read|list|search|status|info|get)\b/
|
|
38
|
+
const INWARD_CONTRADICTS = new Set(['external', 'exec'])
|
|
39
|
+
|
|
40
|
+
/** Named exceptions. Each entry suppresses exactly one tool+class pair and
|
|
41
|
+
* must carry the reason the name signal is misleading for that tool. */
|
|
42
|
+
const EXCEPTIONS = [
|
|
43
|
+
{
|
|
44
|
+
tool: 'joblogic-job-set-status',
|
|
45
|
+
riskClass: 'external',
|
|
46
|
+
reason: 'The -status suffix reads as a query, but this sets a job status on the remote JobLogic tenant. external is correct and the name signal is what is wrong here.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
tool: 'memory-delete',
|
|
50
|
+
riskClass: 'write_local',
|
|
51
|
+
reason: 'Soft-deletes a node in the local Neo4j graph. Trashed nodes still resolve by elementId; nothing leaves the device.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
tool: 'contact-delete',
|
|
55
|
+
riskClass: 'write_local',
|
|
56
|
+
reason: 'Removes a contact from the local graph. No remote address book is touched.',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
tool: 'profile-delete',
|
|
60
|
+
riskClass: 'write_local',
|
|
61
|
+
reason: 'Removes a profile node from the local graph.',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
tool: 'workflow-delete',
|
|
65
|
+
riskClass: 'write_local',
|
|
66
|
+
reason: 'Removes a workflow definition from the local graph. Any run history it produced is untouched.',
|
|
67
|
+
},
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
function collectPluginDirs() {
|
|
71
|
+
const dirs = []
|
|
72
|
+
for (const spec of PLUGIN_ROOTS) {
|
|
73
|
+
if (!existsSync(spec.root)) continue
|
|
74
|
+
for (const entry of readdirSync(spec.root)) {
|
|
75
|
+
if (entry.startsWith('.')) continue
|
|
76
|
+
const lvl1 = join(spec.root, entry)
|
|
77
|
+
if (!statSync(lvl1).isDirectory()) continue
|
|
78
|
+
if (spec.depth === 1) {
|
|
79
|
+
dirs.push(lvl1)
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
const mid = join(lvl1, spec.mid)
|
|
83
|
+
if (!existsSync(mid) || !statSync(mid).isDirectory()) continue
|
|
84
|
+
for (const sub of readdirSync(mid)) {
|
|
85
|
+
if (sub.startsWith('.')) continue
|
|
86
|
+
const lvl2 = join(mid, sub)
|
|
87
|
+
if (statSync(lvl2).isDirectory()) dirs.push(lvl2)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return dirs
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Independent of tool-surface.ts on purpose: a gate that shares the parser it
|
|
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. */
|
|
101
|
+
function declaredTools(manifestPath) {
|
|
102
|
+
const out = []
|
|
103
|
+
let current = null
|
|
104
|
+
let inTools = false
|
|
105
|
+
for (const line of readFileSync(manifestPath, 'utf8').split(/\r?\n/)) {
|
|
106
|
+
if (/^(tools|hidden):\s*$/.test(line)) { inTools = true; current = null; continue }
|
|
107
|
+
if (inTools && /^[A-Za-z_]/.test(line)) inTools = false
|
|
108
|
+
if (!inTools) continue
|
|
109
|
+
const name = line.match(/^\s+-\s+name:\s*(.+?)\s*$/)
|
|
110
|
+
if (name) { current = name[1]; continue }
|
|
111
|
+
const risk = line.match(/^\s+riskClass:\s*(.+?)\s*$/)
|
|
112
|
+
if (risk && current) { out.push({ tool: current, riskClass: risk[1].trim() }); current = null }
|
|
113
|
+
}
|
|
114
|
+
return out
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const contradictions = []
|
|
118
|
+
const matchedExceptions = new Set()
|
|
119
|
+
let scanned = 0
|
|
120
|
+
|
|
121
|
+
for (const dir of collectPluginDirs()) {
|
|
122
|
+
const manifest = join(dir, 'PLUGIN.md')
|
|
123
|
+
if (!existsSync(manifest)) continue
|
|
124
|
+
for (const { tool, riskClass } of declaredTools(manifest)) {
|
|
125
|
+
scanned++
|
|
126
|
+
let signal = null
|
|
127
|
+
if (OUTWARD_SIGNAL.test(tool) && OUTWARD_CONTRADICTS.has(riskClass)) signal = 'outward-name'
|
|
128
|
+
if (INWARD_SIGNAL.test(tool) && INWARD_CONTRADICTS.has(riskClass)) signal = 'inward-name'
|
|
129
|
+
if (!signal) continue
|
|
130
|
+
const excepted = EXCEPTIONS.find((e) => e.tool === tool && e.riskClass === riskClass)
|
|
131
|
+
if (excepted) { matchedExceptions.add(`${tool}:${riskClass}`); continue }
|
|
132
|
+
contradictions.push({ tool, riskClass, signal, plugin: dir.replace(`${MAXY_CODE_ROOT}/`, '') })
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const stale = EXCEPTIONS.filter((e) => !matchedExceptions.has(`${e.tool}:${e.riskClass}`))
|
|
137
|
+
|
|
138
|
+
for (const c of contradictions) {
|
|
139
|
+
console.error(`[risk-class] contradiction tool=${c.tool} declared=${c.riskClass} signal=${c.signal} plugin=${c.plugin}`)
|
|
140
|
+
}
|
|
141
|
+
for (const s of stale) {
|
|
142
|
+
console.error(`[risk-class] stale-exception tool=${s.tool} riskClass=${s.riskClass} — no longer contradicts; remove it from EXCEPTIONS`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (contradictions.length > 0 || stale.length > 0) {
|
|
146
|
+
console.error(`[gate] check-risk-class: FAIL (${contradictions.length} contradiction(s), ${stale.length} stale exception(s), ${scanned} tools scanned)`)
|
|
147
|
+
process.exit(1)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(`[gate] check-risk-class: PASS (${scanned} tools scanned, ${EXCEPTIONS.length} named exception(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 };
|
|
Binary file
|
|
@@ -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
|
}
|
|
@@ -29,7 +29,10 @@ provision_account_dir() {
|
|
|
29
29
|
# Operator-data buckets (entity-anchored, flat) + tool-owned dirs. agents/ and
|
|
30
30
|
# specialists/ are created above. The seeded SCHEMA.md is the single source of
|
|
31
31
|
# the allowed top-level set, parsed at runtime by the fs-schema write guard.
|
|
32
|
-
|
|
32
|
+
# `sites` joins the seed set because it is a home bucket in the /data browser
|
|
33
|
+
# (HOME_FIXED); an unseeded home bucket is reported by the standing
|
|
34
|
+
# home-parity audit as missing on every account that has not published a site.
|
|
35
|
+
local _seed_dirs=(projects contacts documents url-get output generated extracted uploads sites)
|
|
33
36
|
local _d _seeded=0
|
|
34
37
|
for _d in "${_seed_dirs[@]}"; do
|
|
35
38
|
[ -d "$ACCOUNT_DIR/$_d" ] || { mkdir -p "$ACCOUNT_DIR/$_d" && _seeded=$((_seeded+1)); }
|
|
@@ -248,6 +251,49 @@ SETTINGS_EOF
|
|
|
248
251
|
[ -f "$specialist" ] && cp "$specialist" "$ACCOUNT_DIR/specialists/agents/$(basename "$specialist")"
|
|
249
252
|
done
|
|
250
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
|
+
|
|
251
297
|
rm -f "$ACCOUNT_DIR/.claude/agents/"*.md 2>/dev/null || true
|
|
252
298
|
|
|
253
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"}
|
package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js
CHANGED
|
@@ -64,6 +64,11 @@ const PROTECTED_TOP_LEVEL = new Set([
|
|
|
64
64
|
'telegram-channel-bindings.json',
|
|
65
65
|
'canonical-webchat-session.json',
|
|
66
66
|
'session-titles.json',
|
|
67
|
+
// Task 1996 — names the agents the operator switched off. Platform-written at
|
|
68
|
+
// the account root every disable, declared by no plugin, so without this every
|
|
69
|
+
// account with a disabled agent reports it as a stray forever and the standing
|
|
70
|
+
// line stops naming only what an operator must act on.
|
|
71
|
+
'agents-disabled.json',
|
|
67
72
|
]);
|
|
68
73
|
/** Buckets whose depth fs-schema-guard.sh checks, with the same 3-segment cap.
|
|
69
74
|
* Every other top-level bucket is tool-owned and unbounded — not walked. This
|
|
@@ -81,9 +86,9 @@ const FILE_NAME_RE = /^[a-z0-9-]+(\.[a-z0-9-]+)*$/;
|
|
|
81
86
|
/** The single per-run summary line body. A non-zero count is the standing signal
|
|
82
87
|
* that an agent authored folders the hard guards could not prevent. Pure
|
|
83
88
|
* formatter so the emitter and its test share one wording. */
|
|
84
|
-
export function formatReconcileLine(c, homeMissing = 0, homeUnaudited = 0) {
|
|
89
|
+
export function formatReconcileLine(c, homeMissing = 0, homeUnaudited = 0, agentDrift = 0) {
|
|
85
90
|
return `${TAG} stray-top-level=${c.strayTopLevel} over-deep=${c.overDeep} bad-name=${c.badName}`
|
|
86
|
-
+ ` home-missing=${homeMissing} home-unaudited=${homeUnaudited}`;
|
|
91
|
+
+ ` home-missing=${homeMissing} home-unaudited=${homeUnaudited} agent-drift=${agentDrift}`;
|
|
87
92
|
}
|
|
88
93
|
/** Parse the fenced ```allowed-top-level block from an account's SCHEMA.md —
|
|
89
94
|
* the same structured set fs-schema-guard.sh parses with awk. Returns the
|
|
@@ -256,6 +261,41 @@ function homeParity(accountDir) {
|
|
|
256
261
|
const declared = [...regions.ontologyRoots, ...HOME_FIXED].filter((d, i, a) => a.indexOf(d) === i);
|
|
257
262
|
return { declared, missing: declared.filter((d) => !existsSync(join(accountDir, d))) };
|
|
258
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Task 1996 — agents recorded as disabled whose file is still in the
|
|
266
|
+
* dispatchable directory.
|
|
267
|
+
*
|
|
268
|
+
* Disabling MOVES the file out of `<accountDir>/specialists/agents/`, which is
|
|
269
|
+
* what `spawn-context.ts` reads to build the spawn manifest. If the store names
|
|
270
|
+
* an agent that is still there, the operator sees a card marked disabled while
|
|
271
|
+
* the agent keeps being dispatched. Nothing throws and nothing logs, so there is
|
|
272
|
+
* no event to hang a signal on: the only way to see it is to look.
|
|
273
|
+
*
|
|
274
|
+
* An absent or unreadable store returns null, which the caller counts as "not
|
|
275
|
+
* audited" rather than "clean". A parse error here is the admin route's
|
|
276
|
+
* `op=disabled-store status=parse-failed` business.
|
|
277
|
+
*/
|
|
278
|
+
function agentParity(accountDir) {
|
|
279
|
+
let raw;
|
|
280
|
+
try {
|
|
281
|
+
raw = readFileSync(join(accountDir, 'agents-disabled.json'), 'utf8');
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
let names;
|
|
287
|
+
try {
|
|
288
|
+
names = JSON.parse(raw).disabled;
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
if (!Array.isArray(names))
|
|
294
|
+
return null;
|
|
295
|
+
const disabled = names.filter((n) => typeof n === 'string');
|
|
296
|
+
const stillOnDisk = disabled.filter((n) => existsSync(join(accountDir, 'specialists', 'agents', n)));
|
|
297
|
+
return { disabled: disabled.length, stillOnDisk };
|
|
298
|
+
}
|
|
259
299
|
/** One reconcile pass over every account under the accounts root: one op=strays
|
|
260
300
|
* line naming the unaccounted-for entries of each account that has any, then the
|
|
261
301
|
* single summary line. An account is any child dir with an account.json (the
|
|
@@ -280,6 +320,11 @@ export function runReconcile(deps) {
|
|
|
280
320
|
// buckets was checked. Without this, an install where every account lacks a
|
|
281
321
|
// schema reports home-missing=0 and reads as healthy.
|
|
282
322
|
let homeUnaudited = 0;
|
|
323
|
+
// Task 1996 — per-run total of agents recorded as disabled that are still in
|
|
324
|
+
// the dispatchable directory. Zero on the summary line is the per-cycle proof
|
|
325
|
+
// that the check ran and found nothing, which is what makes the absence of an
|
|
326
|
+
// op=agent-parity line readable as "clean" rather than "never audited".
|
|
327
|
+
let agentDrift = 0;
|
|
283
328
|
let entries;
|
|
284
329
|
try {
|
|
285
330
|
entries = readdirSync(deps.accountsRoot);
|
|
@@ -310,6 +355,14 @@ export function runReconcile(deps) {
|
|
|
310
355
|
`missing=${parity.missing.join(',')}`);
|
|
311
356
|
homeMissing += parity.missing.length;
|
|
312
357
|
}
|
|
358
|
+
// Task 1996 — named only when the store and the disk disagree, mirroring
|
|
359
|
+
// op=strays and op=home-parity above.
|
|
360
|
+
const agents = agentParity(accountDir);
|
|
361
|
+
if (agents && agents.stillOnDisk.length > 0) {
|
|
362
|
+
deps.logger(`${TAG} op=agent-parity account=${entry} disabled=${agents.disabled} ` +
|
|
363
|
+
`stillOnDisk=${agents.stillOnDisk.join(',')}`);
|
|
364
|
+
agentDrift += agents.stillOnDisk.length;
|
|
365
|
+
}
|
|
313
366
|
total.strayTopLevel += c.strayTopLevel;
|
|
314
367
|
total.overDeep += c.overDeep;
|
|
315
368
|
total.badName += c.badName;
|
|
@@ -318,7 +371,7 @@ export function runReconcile(deps) {
|
|
|
318
371
|
// best-effort: skip an account that throws unexpectedly
|
|
319
372
|
}
|
|
320
373
|
}
|
|
321
|
-
deps.logger(formatReconcileLine(total, homeMissing, homeUnaudited));
|
|
374
|
+
deps.logger(formatReconcileLine(total, homeMissing, homeUnaudited, agentDrift));
|
|
322
375
|
}
|
|
323
376
|
/** Standing periodic reconcile. Unref'd interval so it never holds the process
|
|
324
377
|
* open; each tick re-runs the pass best-effort. */
|