@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.
Files changed (117) hide show
  1. package/package.json +2 -2
  2. package/payload/platform/config/brand.json +101 -18
  3. package/payload/platform/lib/account-schema-regions/dist/index.d.ts +17 -5
  4. package/payload/platform/lib/account-schema-regions/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/account-schema-regions/dist/index.js +20 -6
  6. package/payload/platform/lib/account-schema-regions/dist/index.js.map +1 -1
  7. package/payload/platform/lib/account-schema-regions/src/index.ts +20 -6
  8. package/payload/platform/plugins/admin/PLUGIN.md +46 -5
  9. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +138 -14
  10. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +14 -0
  11. package/payload/platform/plugins/aeo/PLUGIN.md +3 -0
  12. package/payload/platform/plugins/browser/PLUGIN.md +18 -0
  13. package/payload/platform/plugins/connector/PLUGIN.md +4 -0
  14. package/payload/platform/plugins/contacts/PLUGIN.md +9 -0
  15. package/payload/platform/plugins/docs/references/admin-ui.md +111 -13
  16. package/payload/platform/plugins/docs/references/plugins-guide.md +26 -0
  17. package/payload/platform/plugins/email/PLUGIN.md +17 -0
  18. package/payload/platform/plugins/filesystem/PLUGIN.md +6 -0
  19. package/payload/platform/plugins/google/PLUGIN.md +12 -0
  20. package/payload/platform/plugins/graph/PLUGIN.md +3 -0
  21. package/payload/platform/plugins/graph-viewer/PLUGIN.md +1 -0
  22. package/payload/platform/plugins/ledger/PLUGIN.md +7 -0
  23. package/payload/platform/plugins/memory/PLUGIN.md +47 -3
  24. package/payload/platform/plugins/outlook/PLUGIN.md +24 -0
  25. package/payload/platform/plugins/quickbooks/PLUGIN.md +15 -0
  26. package/payload/platform/plugins/replicate/PLUGIN.md +3 -0
  27. package/payload/platform/plugins/scheduling/PLUGIN.md +10 -0
  28. package/payload/platform/plugins/storage-broker/PLUGIN.md +12 -0
  29. package/payload/platform/plugins/telegram/PLUGIN.md +3 -0
  30. package/payload/platform/plugins/url-get/PLUGIN.md +1 -0
  31. package/payload/platform/plugins/voice-mirror/PLUGIN.md +5 -0
  32. package/payload/platform/plugins/whatsapp/PLUGIN.md +12 -0
  33. package/payload/platform/plugins/work/PLUGIN.md +15 -0
  34. package/payload/platform/plugins/workflows/PLUGIN.md +8 -0
  35. package/payload/platform/scripts/__tests__/provision-honours-disabled-agents.test.sh +84 -0
  36. package/payload/platform/scripts/check-risk-class.mjs +150 -0
  37. package/payload/platform/scripts/check-specialist-tool-surface.mjs +5 -2
  38. package/payload/platform/scripts/lib/__pycache__/account-schema-owned-dirs.cpython-314.pyc +0 -0
  39. package/payload/platform/scripts/lib/canonical-tool-names.mjs +7 -4
  40. package/payload/platform/scripts/lib/provision-account-dir.sh +47 -1
  41. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts +1 -1
  42. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -1
  43. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +56 -3
  44. package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -1
  45. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  46. package/payload/platform/services/claude-session-manager/dist/http-server.js +4 -0
  47. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  48. package/payload/platform/services/claude-session-manager/dist/index.js +8 -0
  49. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  50. package/payload/platform/services/claude-session-manager/dist/tool-surface.d.ts +32 -5
  51. package/payload/platform/services/claude-session-manager/dist/tool-surface.d.ts.map +1 -1
  52. package/payload/platform/services/claude-session-manager/dist/tool-surface.js +83 -19
  53. package/payload/platform/services/claude-session-manager/dist/tool-surface.js.map +1 -1
  54. package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/PLUGIN.md +4 -0
  55. package/payload/server/{chunk-5ADBNEJA.js → chunk-2WAXM5N2.js} +111 -111
  56. package/payload/server/maxy-edge.js +1 -1
  57. package/payload/server/public/activity.html +5 -5
  58. package/payload/server/public/agents.html +4 -4
  59. package/payload/server/public/assets/{AdminLoginScreens-D5KOQA8S.js → AdminLoginScreens-DKeCt1uf.js} +1 -1
  60. package/payload/server/public/assets/{AdminShell-C8wazpnz.js → AdminShell-HeELCCjJ.js} +1 -1
  61. package/payload/server/public/assets/{activity-CFqkDhTs.js → activity-BEIDAxDA.js} +1 -1
  62. package/payload/server/public/assets/admin-C0-WU1sf.js +1 -0
  63. package/payload/server/public/assets/agents-DjUvMFTj.js +1 -0
  64. package/payload/server/public/assets/{browser-T_5xbnJh.js → browser-GkVTGqlb.js} +1 -1
  65. package/payload/server/public/assets/{calendar-CKjuiG8c.js → calendar-CSqJeFEg.js} +1 -1
  66. package/payload/server/public/assets/chat-C0Ps84U5.js +1 -0
  67. package/payload/server/public/assets/chevron-left-DQ2uNlu9.js +1 -0
  68. package/payload/server/public/assets/chevron-right-C4QCiRGD.js +1 -0
  69. package/payload/server/public/assets/clock-BEeQ7JxX.js +1 -0
  70. package/payload/server/public/assets/data-CGuNfym7.js +1 -0
  71. package/payload/server/public/assets/{file-text-c6cA4Evd.js → file-text-DiscM2pP.js} +1 -1
  72. package/payload/server/public/assets/{graph-DIk-o45J.js → graph-CRP5DM8C.js} +1 -1
  73. package/payload/server/public/assets/{graph-labels-5bjxhC_p.js → graph-labels-Du-6KoBE.js} +1 -1
  74. package/payload/server/public/assets/{maximize-2-BCQkFb1_.js → maximize-2-D-H_uIV-.js} +1 -1
  75. package/payload/server/public/assets/{operator-0UnNo2ms.js → operator-D5rA76YY.js} +1 -1
  76. package/payload/server/public/assets/page-CHK1415C.js +1 -0
  77. package/payload/server/public/assets/{page-HVtUfub2.js → page-DZE3H_n4.js} +4 -4
  78. package/payload/server/public/assets/{public-CKEYyIPn.js → public-DFHXVkdD.js} +1 -1
  79. package/payload/server/public/assets/{rotate-ccw-dTpO5dhL.js → rotate-ccw-CesEJsuO.js} +1 -1
  80. package/payload/server/public/assets/{routines-C6P1-Dto.js → routines-DYMYY_r8.js} +1 -1
  81. package/payload/server/public/assets/{skills-ClrpVqlz.js → skills-67jtJ16D.js} +1 -1
  82. package/payload/server/public/assets/{tasks-C8SNhSFC.js → tasks-D2C2a-uk.js} +1 -1
  83. package/payload/server/public/assets/{time-entry-format-CCO6N6XZ.js → time-entry-format-BixTGQAd.js} +1 -1
  84. package/payload/server/public/assets/{triangle-alert-CHjdKpag.js → triangle-alert-Co9RWb9q.js} +1 -1
  85. package/payload/server/public/assets/{useCopyFeedback-BTUu3-l8.js → useCopyFeedback-7zwf9_w4.js} +1 -1
  86. package/payload/server/public/assets/useSubAccountSwitcher-Bg72hdQ8.js +9 -0
  87. package/payload/server/public/assets/useSubAccountSwitcher-DE4v5_Fz.css +1 -0
  88. package/payload/server/public/assets/{useVoiceRecorder-BeNqxmm2.js → useVoiceRecorder-CZDDiwMD.js} +1 -1
  89. package/payload/server/public/assets/{wrench-CeNnc7oO.js → wrench-CjQqdj4P.js} +1 -1
  90. package/payload/server/public/brand/fonts/inter-400.woff2 +0 -0
  91. package/payload/server/public/brand/fonts/inter-500.woff2 +0 -0
  92. package/payload/server/public/brand/fonts/inter-600.woff2 +0 -0
  93. package/payload/server/public/brand/fonts/inter-700.woff2 +0 -0
  94. package/payload/server/public/brand/fonts/inter-800.woff2 +0 -0
  95. package/payload/server/public/brand-defaults.css +83 -5
  96. package/payload/server/public/browser.html +4 -4
  97. package/payload/server/public/calendar.html +7 -7
  98. package/payload/server/public/chat.html +13 -13
  99. package/payload/server/public/data.html +11 -11
  100. package/payload/server/public/graph.html +9 -9
  101. package/payload/server/public/index.html +14 -14
  102. package/payload/server/public/operator.html +14 -14
  103. package/payload/server/public/public.html +13 -13
  104. package/payload/server/public/routines.html +6 -6
  105. package/payload/server/public/skills.html +5 -5
  106. package/payload/server/public/tasks.html +6 -6
  107. package/payload/server/server.js +1584 -837
  108. package/payload/server/public/assets/admin-B-EGUWS4.js +0 -1
  109. package/payload/server/public/assets/agents-DPulfmrd.js +0 -1
  110. package/payload/server/public/assets/chat-DWLYAA70.js +0 -1
  111. package/payload/server/public/assets/chevron-left-DBMCTKlT.js +0 -1
  112. package/payload/server/public/assets/chevron-right-SBAr6zzc.js +0 -1
  113. package/payload/server/public/assets/clock-kkzt7Scf.js +0 -1
  114. package/payload/server/public/assets/data-CV8N9qcy.js +0 -1
  115. package/payload/server/public/assets/page-JJa2xdzf.js +0 -1
  116. package/payload/server/public/assets/useSubAccountSwitcher-DSW9vDi2.js +0 -9
  117. 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
- const item = line.match(/^\s+-\s+(.+?)\s*$/);
92
- if (item) hidden.push(item[1]);
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 `- <x>` entries
40
- // under a bare `hidden:` block. Both contribute names a specialist may list in
41
- // its `tools:` frontmatter, so both belong to the live set.
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
- local _seed_dirs=(projects contacts documents url-get output generated extracted uploads)
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.
@@ -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. */
@@ -1 +1 @@
1
- {"version":3,"file":"account-dir-schema-reconcile.d.ts","sourceRoot":"","sources":["../src/account-dir-schema-reconcile.ts"],"names":[],"mappings":"AAsFA,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,CAAC,CAAC,EAAE,eAAe,EAAE,WAAW,SAAI,EAAE,aAAa,SAAI,GAAG,MAAM,CAGlG;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;AAyBD;;;;;;;;;;;;;iCAaiC;AACjC,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CA+CtD;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"}
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"}
@@ -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. */