@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
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what SiteDesk ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:6333dcde1f052035148393349684b0d1311a2f645c990066c96672c5fa61c1ce
4
+ content-hash: sha256:b15d0e491d957f75523ecf7056f8004bafcdf448767c0c3dd64b40a73926b58b
5
5
  brand: sitedesk-code
6
6
  product-name: SiteDesk
7
7
  ---
@@ -474,6 +474,32 @@ Skill content, plugin manifests, agent templates, and reference files reference
474
474
 
475
475
  The runtime substitution happens at every read site that flows content into a system prompt or operator-visible UI: the admin agent's `plugin-read` tool (references + `PLUGIN.md`), the `skill-load` tool (SKILL.md by skill name — one-call resolver+reader, the canonical primitive for SKILL.md), the public agent's recursive plugin assembly, and `IDENTITY` / `SOUL` / `AGENTS` / `KNOWLEDGE` markdown reads. Missing or empty `productName` hard-fails — there is no fallback to a default brand string. See [.docs/agents.md](../../.docs/agents.md) § "Brand templating" for the full contract.
476
476
 
477
+ ## Tool risk classes (for plugin authors)
478
+
479
+ Every tool a plugin declares in its `PLUGIN.md` `tools:` block carries three mandatory
480
+ fields. `publicAllowlist` and `adminAllowlist` say who may call the tool. `riskClass`
481
+ says what happens when they do, and takes one of four values:
482
+
483
+ - `read` changes no state anywhere. A remote lookup counts as `read`; reaching the
484
+ network is not by itself consequential.
485
+ - `write_local` mutates account files or the graph on this device and transmits nothing.
486
+ - `exec` runs an operating-system command, or code supplied by the caller or the operator.
487
+ - `external` transmits to, or changes state on, a system outside this device, or delivers
488
+ a message to a person.
489
+
490
+ Leaving the field out is a refusal-to-boot condition, not a silent default. The platform
491
+ refuses to start rather than treat an undeclared consequence as harmless, because a tool
492
+ that quietly reads as `read` escapes every check downstream of it.
493
+
494
+ Declaring the class does not yet gate anything. It gives any consumer one honest answer
495
+ to the question "is this tool consequential?" without maintaining a list of tool names,
496
+ which is what the checks in `platform/plugins/admin/hooks/` have had to do until now.
497
+
498
+ A publish-time gate flags declarations that contradict what the tool's own name says it
499
+ does, and a boot line reports the census across all four classes. The classification
500
+ rule, the exception mechanism, and the diagnostic commands are in
501
+ `.docs/tool-risk-classes.md`.
502
+
477
503
  ## MCP Plugin Observability (for plugin authors)
478
504
 
479
505
  Every `console.error` line from a plugin's MCP server is captured to a per-server raw log. Capture is automatic and needs no import or call: the `mcp-spawn-tee` shim spawns every MCP server and mirrors its stderr to `mcp-<name>-<sessionId>.log` (or `mcp-<name>-nosession.log` for a spawn with no session). That file is keyed by session, which is the conversation, and that is where you grep. Prefix your diagnostics `[your-tool]` so a grep isolates one tool.
@@ -3078,8 +3104,11 @@ not-found|rm-error>`.
3078
3104
 
3079
3105
  ### Agents
3080
3106
 
3081
- The Agents surface (`/agents`) is the admin view of the account's agents, of two
3082
- kinds, each card tagged `Public` or `Specialist`:
3107
+ The Agents surface (`/agents`) is the admin view of the account's agents. Each
3108
+ card is tagged by **origin**, which says who put the agent there and therefore
3109
+ what the operator may do with it. Origin is a different question to `kind`, which
3110
+ says what shape the agent is: a shipped specialist and a user-created one are
3111
+ both `kind: 'specialist'` and differ only by origin.
3083
3112
 
3084
3113
  - **Public agents** — the per-agent directories under `<accountDir>/agents/<slug>/`,
3085
3114
  each carrying a `config.json` (`displayName`, `status`, `model`,
@@ -3090,19 +3119,63 @@ kinds, each card tagged `Public` or `Specialist`:
3090
3119
  to the public URL (computed client-side: an `admin.` host maps to the `public.`
3091
3120
  host), plus a **delete** behind a confirm that runs the loud-fail `DELETE`
3092
3121
  (graph projection cleanup precedes file removal).
3093
- - **User-created specialists** — the agent files under
3122
+ - **User-created specialists** (`origin: 'specialist'`) — the agent files under
3094
3123
  `<accountDir>/plugins/<plugin>/agents/<plugin>--<name>.md` (written by
3095
- `agent-builder`). `<accountDir>/plugins` is the user-created tree, so only
3096
- user-created specialists list here; shipped premium and core specialists (whose
3097
- plugins live under `PLATFORM_ROOT`) do not. The specialist modal shows the
3098
- model, tools, the owning plugin's skills, and the system-prompt body. Specialists
3099
- are authored, edited, and deleted through chat (`agent-builder` /
3100
- `specialist-management`), so the specialist modal offers no delete or open-agent
3101
- link.
3124
+ `agent-builder`). The specialist modal shows the model, tools, the owning
3125
+ plugin's skills, and the system-prompt body. Specialists are authored, edited,
3126
+ and deleted through chat (`agent-builder` / `specialist-management`), so the
3127
+ specialist modal offers no delete or open-agent link.
3128
+ - **Shipped specialists** (`origin: 'shipped'`) the union of
3129
+ `<accountDir>/specialists/agents/`, `<accountDir>/specialists/agents-disabled/`
3130
+ and `PLATFORM_ROOT/templates/specialists/agents/`, in that precedence order.
3131
+ `sidebar-artefacts.ts` reads the same union minus the quarantine directory, so
3132
+ a disabled agent still appears there through its bundled-template row. The
3133
+ route once walked only the account plugins tree, so the twelve
3134
+ highest-capability agents on the box were the ones the operator could not see.
3135
+ These are the only agents that can be disabled, and the route enforces that
3136
+ rather than relying on the UI to withhold the control.
3102
3137
 
3103
3138
  The former "Public" flyout toggle in the account menu was removed; this page is
3104
3139
  the sole agent surface.
3105
3140
 
3141
+ **Risk class.** Every classified card carries a green/amber/red badge derived
3142
+ from the agent's declared `tools:` line: green when every tool is `read`, amber
3143
+ when the worst is `write_local`, red on any `exec` or `external`, and red when
3144
+ any tool resolves to no class at all. Resolution has two sources, because
3145
+ frontmatter names tools in Claude Code's namespace: a plugin tool is a direct
3146
+ lookup in `ToolSurface.riskByTool`, which is keyed by the same
3147
+ canonical `mcp__plugin_<p>_<p>__<tool>` string frontmatter writes, and the
3148
+ eleven built-ins carry their own table in `server/lib/agent-risk.ts`. An empty
3149
+ tool list is red: no `tools:` line means the full surface, not none of it.
3150
+
3151
+ A public agent carries `risk: null` and shows no badge. Its tool surface
3152
+ resolves through `ADMIN_CORE_TOOLS` plus the plugin allowlists rather than a
3153
+ `tools:` line, and classifying its empty list would paint every public agent red
3154
+ on a premise nothing established.
3155
+
3156
+ The detail modal renders one chip per declared tool carrying **that tool's own**
3157
+ class, so a `read` tool inside a red agent still reads as harmless; a tool with
3158
+ no class is dashed and uncoloured rather than painted as if it were understood.
3159
+ Badge colours come from the shared semantic status set: `-solid` with white text
3160
+ for the badge, `-tint` with `-textOnTint` for a chip, never white.
3161
+
3162
+ **Disable.** `<accountDir>/specialists/agents/` is the live dispatchable set —
3163
+ `spawn-context.ts` reads it to build the spawn manifest — so disabling **moves**
3164
+ the agent file to `<accountDir>/specialists/agents-disabled/` and records the
3165
+ basename in a 0600 `<accountDir>/agents-disabled.json`. A flag alone would leave
3166
+ the agent running while the card claimed otherwise.
3167
+
3168
+ The file is moved, never deleted: a premium `--` agent and an operator-edited
3169
+ override each exist only in the account dir, so deleting one and later restoring
3170
+ from the bundled template would hand back a different agent than the operator
3171
+ switched off. A disabled agent stays listed, marked `Disabled`, because an agent
3172
+ that vanished from the surface that disabled it could never be switched back on.
3173
+
3174
+ `provision-account-dir.sh` re-reads the store after its core-specialist recopy
3175
+ and removes anything named in it, so an upgrade does not silently re-deliver a
3176
+ disabled agent. An unreadable store withholds nothing and says so: a corrupt
3177
+ file is not evidence that an agent was disabled.
3178
+
3106
3179
  **Account scope.** The route (`server/routes/admin/agents.ts`) never infers an
3107
3180
  account from device state. The two reads resolve from the caller's admin session
3108
3181
  (`requireAdminSession` + the shared `accountDirForSession` in
@@ -3116,8 +3189,10 @@ explicit `?plugin=` parameter, not inferred from `--` in the slug.
3116
3189
 
3117
3190
  | Route | Behaviour |
3118
3191
  |-------|-----------|
3119
- | `GET /api/admin/agents` | Session-scoped. Lists the session account's public `agents/*/` dirs (never the `admin` agent) plus its user-created specialists from `plugins/*/agents/*.md`, each row tagged `kind`. Returns `{agents, accountId, skipped, specialistsSkipped}``accountId` names the account resolved (the client sends it back on a delete); `skipped` counts public dirs whose `config.json` failed to parse; `specialistsSkipped` counts specialist `.md` files with no parseable `name`. 401 when the session maps to no account. |
3120
- | `GET /api/admin/agents/:slug` | Session-scoped. Without `?plugin=`, returns a public agent's config fields + four owned docs + a `present` map (a missing or unreadable doc is `''`/`present.<role>=false`, never a 500). With `?plugin=`, returns that plugin's specialist `{kind, model, tools, systemPrompt, skills}`. Unknown 404s, 401 as above. |
3192
+ | `GET /api/admin/agents` | Session-scoped. Lists the session account's public `agents/*/` dirs (never the `admin` agent), its user-created specialists from `plugins/*/agents/*.md`, and its shipped specialists, each row tagged `kind` and `origin` and carrying `risk`, `worstTool`, `unresolved` and `disabled`. Returns `{agents, accountId, skipped, specialistsSkipped, shippedSkipped}`. 401 when the session maps to no account. |
3193
+ | `GET /api/admin/agents/:slug` | Session-scoped. Without `?plugin=` or `?origin=`, returns a public agent's config fields + four owned docs + a `present` map (a missing or unreadable doc is `''`/`present.<role>=false`, never a 500). With `?plugin=`, that plugin's user-created specialist; with `?origin=shipped`, the shipped one. Both specialist shapes add `{risk, worstTool, unresolved, byTool, disabled}`, where `byTool` is one class per declared tool. Selection is explicit rather than inferred, because a premium file and a user-created specialist under a plugin of the same name produce the same slug. Unknown 404s, 401 as above. |
3194
+ | `POST /api/admin/agents/:slug/disable?accountId=` | Shipped agents only, enforced here: a slug present in none of the three shipped directories 404s. Moves the file from `specialists/agents/` to `specialists/agents-disabled/` and records the basename in the 0600 store. Returns `{ok, moved}`; `moved:false` means the agent existed only as a bundled template, so nothing was there to move and the store entry is what stops the next provisioning run delivering it. The store is read strictly before anything moves, so an unreadable store 500s with the file untouched rather than rewriting the file whole from an empty set and dropping every other disabled agent. Same `accountId` contract as delete. |
3195
+ | `POST /api/admin/agents/:slug/enable?accountId=` | Covers both of disable's outcomes. Returns `{ok, restored}`: a quarantined file moves back (`restored:true`); a bundled-only agent has nothing to move, so clearing the store entry is the whole job (`restored:false`), which is what stops provisioning withholding it. Treating that second case as "nothing to restore" made disable a one-way door. 404 only when neither directory nor store knows the agent. |
3121
3196
  | `DELETE /api/admin/agents/:slug?accountId=` | Public agents only, on the named validated account. Removes the dir after `deleteAgentProjection`; refuses the `admin` slug (403) and a missing/unknown `accountId` (400) with no write. Loud-fail: a graph-cleanup throw aborts the file removal. |
3122
3197
  | `POST /api/admin/agents/:slug/project?accountId=` | Re-projects the named account's on-disk agent into the graph. Same `accountId` contract as delete. |
3123
3198
 
@@ -3144,6 +3219,35 @@ Agents page survived undiagnosed on a multi-account install.
3144
3219
  - `op=delete accountId=<id8> slug=<…> outcome=<ok|failed>` with
3145
3220
  `reason=<graph-cleanup-failed|rm-error>` on failure (files preserved on a
3146
3221
  graph-cleanup throw).
3222
+ - `op=classify account=<id8> agent=<name> origin=<shipped|public|specialist>
3223
+ risk=<green|amber|red> worstTool=<tool> tools=<n> unresolved=<comma-list|none>
3224
+ disabled=<bool>` — one line per classified agent per listing, so it fires on
3225
+ every page load. `worstTool` makes a wrong class diagnosable without
3226
+ re-deriving it. **A non-empty `unresolved=` is the one to act on**: the agent
3227
+ is forced red, and the cause is that the built-in table or the registry
3228
+ mapping has drifted, not that the agent changed.
3229
+ - `op=risk-surface status=load-failed reason=<msg>` — the plugin registry did
3230
+ not parse, so every row is forced red and the listing returns
3231
+ `riskSurfaceFailed: true`. Without the forced red, every plugin tool would
3232
+ land in `unresolved` and a registry outage would read as agent drift across
3233
+ every card at once; without the response field, a red row would be
3234
+ indistinguishable from a genuinely dangerous agent, because nothing resolved
3235
+ so `unresolved` is empty and the per-tool explanation cannot fire.
3236
+ - `op=disabled-store status=parse-failed reason=<msg>` — the listing then treats
3237
+ every agent as enabled, which is what they actually are. A read surface shows
3238
+ the true dispatchable set rather than a comforting one.
3239
+ - `op=<disable|enable> accountId=<id8> slug=<…> outcome=<ok|failed>`, with
3240
+ `moved=<bool>` on disable and `reason=nothing-quarantined` on a failed enable.
3241
+
3242
+ **Disable is a no-event failure, so it needs a standing check.** If the store
3243
+ names an agent whose file is still in `specialists/agents/`, nothing throws, the
3244
+ card reads disabled, and the agent keeps being dispatched. The five-minute
3245
+ `account-dir-schema-reconcile` pass reports it as
3246
+ `[fs-reconcile] op=agent-parity account=<id8> disabled=<n> stillOnDisk=<list>`,
3247
+ fired only on disagreement. The per-cycle heartbeat is `agent-drift=0` on that
3248
+ pass's summary line, which is what makes the absence of a per-account line
3249
+ readable as "clean" rather than "never audited" — grep the summary line, not the
3250
+ per-account one, to confirm the check is running.
3147
3251
 
3148
3252
  ### Graph
3149
3253
 
@@ -3277,9 +3381,29 @@ the Web Share API and is disabled where unavailable. Signals:
3277
3381
  `[data-ui] op=mount header=operator home=absent`, `op=select-enter`,
3278
3382
  `op=select-exit reason=<cancel|action>`, `op=share supported=<bool> count=<n>`.
3279
3383
 
3384
+ **The dashboard has a surface ramp and a display type tier.** Every page sits on
3385
+ a page plane, cards rise onto a raised plane, and inputs and scroll regions
3386
+ recede onto an inset plane. Each brand chooses how the card separates: SiteDesk's
3387
+ card is a shade lighter than its page and casts no shadow, Real Agent's card and
3388
+ page are both white and a shadow does the work. The page header is an inverted
3389
+ band carrying the page title at the display size. Colours, radii, shadows and
3390
+ type weights all come from the brand's own tokens; nothing is hardcoded, and a
3391
+ token that resolves to nothing fails the build rather than painting invisibly.
3392
+
3393
+ **Folders open beside a rail on a wide screen.** At 1280px and above the folder
3394
+ view splits in two: a list of the account's folders on the left, the files on
3395
+ the right, with the folder you are in highlighted. Below that width the rail is
3396
+ hidden and the breadcrumb does the same job.
3397
+
3398
+ **Status colours are shared.** Success, warning, error and info each carry a
3399
+ solid fill, a pale tint and a text colour for that tint. Every pairing meets the
3400
+ WCAG AA contrast minimum, on every brand.
3401
+
3280
3402
  **`/data` opens on the account's own folders, as cards.** With no `#path=` in
3281
3403
  the URL the browser lands on **home**: the account's business folders (the
3282
- entity folders of its trade, plus `projects`, `contacts` and `documents`) shown
3404
+ entity folders of its trade, plus documents, projects, contacts, the work it
3405
+ has produced for you, anything a client has sent in, and any websites it
3406
+ has published) shown
3283
3407
  one per card, each listing the first 20 things inside it and a `+N more` row
3284
3408
  past that. Tapping a card opens that folder in the ordinary list or grid. There
3285
3409
  are never loose files at this level. Everything the platform and its plugins
@@ -9,6 +9,20 @@ Invoked by the admin agent directly.
9
9
 
10
10
  This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
11
11
 
12
+ ## 2026-07-25 (0.1.508)
13
+
14
+ - SiteDesk's screens now use a single, cleaner typeface throughout, and the old olive-green colour is gone.
15
+ - You can now disable an agent from the Agents page, and each one shows its risk level.
16
+ - Dashboard sections appear as soon as each one loads, instead of the whole page waiting on the slowest one.
17
+ - Real Agent's icons and imagery were redrawn to match its colour palette.
18
+
19
+ ## 2026-07-25 (0.1.507)
20
+
21
+ - Chat now updates the moment a message or permission prompt arrives, instead of checking every couple of seconds.
22
+ - The dashboard has a visual refresh: cards now fill in properly instead of looking transparent.
23
+ - The Data page has a new two-pane view with a folder rail on wider screens, and uploads/sites are now correctly grouped under Home.
24
+ - Every assistant tool now carries a declared risk level, checked automatically so it cannot silently drift.
25
+
12
26
  ## 2026-07-25 (0.1.506)
13
27
 
14
28
  - Fixed a bug where the Agents page and a few other admin screens could resolve the wrong account.
@@ -5,12 +5,15 @@ tools:
5
5
  - name: aeo-emit-jsonld
6
6
  publicAllowlist: false
7
7
  adminAllowlist: true
8
+ riskClass: write_local
8
9
  - name: aeo-write-llms-txt
9
10
  publicAllowlist: false
10
11
  adminAllowlist: true
12
+ riskClass: write_local
11
13
  - name: aeo-audit-page
12
14
  publicAllowlist: false
13
15
  adminAllowlist: true
16
+ riskClass: write_local
14
17
  skills:
15
18
  - skills/structured-answer/SKILL.md
16
19
  always: false
@@ -5,57 +5,75 @@ tools:
5
5
  - name: browser-render
6
6
  publicAllowlist: false
7
7
  adminAllowlist: true
8
+ riskClass: read
8
9
  - name: browser-navigate
9
10
  publicAllowlist: false
10
11
  adminAllowlist: true
12
+ riskClass: read
11
13
  - name: browser-snapshot
12
14
  publicAllowlist: false
13
15
  adminAllowlist: true
16
+ riskClass: read
14
17
  - name: browser-click
15
18
  publicAllowlist: false
16
19
  adminAllowlist: true
20
+ riskClass: external
17
21
  - name: browser-fill
18
22
  publicAllowlist: false
19
23
  adminAllowlist: true
24
+ riskClass: external
20
25
  - name: browser-fill-form
21
26
  publicAllowlist: false
22
27
  adminAllowlist: true
28
+ riskClass: external
23
29
  - name: browser-type
24
30
  publicAllowlist: false
25
31
  adminAllowlist: true
32
+ riskClass: external
26
33
  - name: browser-press-key
27
34
  publicAllowlist: false
28
35
  adminAllowlist: true
36
+ riskClass: external
29
37
  - name: browser-hover
30
38
  publicAllowlist: false
31
39
  adminAllowlist: true
40
+ riskClass: read
32
41
  - name: browser-select-option
33
42
  publicAllowlist: false
34
43
  adminAllowlist: true
44
+ riskClass: external
35
45
  - name: browser-wait-for
36
46
  publicAllowlist: false
37
47
  adminAllowlist: true
48
+ riskClass: read
38
49
  - name: browser-handle-dialog
39
50
  publicAllowlist: false
40
51
  adminAllowlist: true
52
+ riskClass: external
41
53
  - name: browser-evaluate
42
54
  publicAllowlist: false
43
55
  adminAllowlist: true
56
+ riskClass: exec
44
57
  - name: browser-console-messages
45
58
  publicAllowlist: false
46
59
  adminAllowlist: true
60
+ riskClass: read
47
61
  - name: browser-tabs
48
62
  publicAllowlist: false
49
63
  adminAllowlist: true
64
+ riskClass: read
50
65
  - name: browser-pdf-save
51
66
  publicAllowlist: false
52
67
  adminAllowlist: true
68
+ riskClass: write_local
53
69
  - name: browser-screenshot
54
70
  publicAllowlist: false
55
71
  adminAllowlist: true
72
+ riskClass: write_local
56
73
  - name: browser-resize
57
74
  publicAllowlist: false
58
75
  adminAllowlist: true
76
+ riskClass: read
59
77
  always: true
60
78
  embed: false
61
79
  metadata: {"platform":{}}
@@ -5,15 +5,19 @@ tools:
5
5
  - name: connector-register
6
6
  publicAllowlist: false
7
7
  adminAllowlist: true
8
+ riskClass: write_local
8
9
  - name: connector-call
9
10
  publicAllowlist: false
10
11
  adminAllowlist: true
12
+ riskClass: external
11
13
  - name: connector-list
12
14
  publicAllowlist: false
13
15
  adminAllowlist: true
16
+ riskClass: read
14
17
  - name: connector-deregister
15
18
  publicAllowlist: false
16
19
  adminAllowlist: true
20
+ riskClass: write_local
17
21
  metadata: {"platform":{"optional":true,"pluginKey":"connector"}}
18
22
  mcp:
19
23
  command: node
@@ -5,30 +5,39 @@ tools:
5
5
  - name: contact-create
6
6
  publicAllowlist: false
7
7
  adminAllowlist: false
8
+ riskClass: write_local
8
9
  - name: contact-lookup
9
10
  publicAllowlist: false
10
11
  adminAllowlist: false
12
+ riskClass: read
11
13
  - name: contact-update
12
14
  publicAllowlist: false
13
15
  adminAllowlist: false
16
+ riskClass: write_local
14
17
  - name: contact-delete
15
18
  publicAllowlist: false
16
19
  adminAllowlist: false
20
+ riskClass: write_local
17
21
  - name: contact-list
18
22
  publicAllowlist: false
19
23
  adminAllowlist: false
24
+ riskClass: read
20
25
  - name: contact-export
21
26
  publicAllowlist: false
22
27
  adminAllowlist: false
28
+ riskClass: write_local
23
29
  - name: contact-erase
24
30
  publicAllowlist: false
25
31
  adminAllowlist: false
32
+ riskClass: write_local
26
33
  - name: group-create
27
34
  publicAllowlist: false
28
35
  adminAllowlist: false
36
+ riskClass: write_local
29
37
  - name: group-manage
30
38
  publicAllowlist: false
31
39
  adminAllowlist: false
40
+ riskClass: write_local
32
41
  always: false
33
42
  embed: false
34
43
  metadata: {"platform":{}}
@@ -151,8 +151,11 @@ not-found|rm-error>`.
151
151
 
152
152
  ### Agents
153
153
 
154
- The Agents surface (`/agents`) is the admin view of the account's agents, of two
155
- kinds, each card tagged `Public` or `Specialist`:
154
+ The Agents surface (`/agents`) is the admin view of the account's agents. Each
155
+ card is tagged by **origin**, which says who put the agent there and therefore
156
+ what the operator may do with it. Origin is a different question to `kind`, which
157
+ says what shape the agent is: a shipped specialist and a user-created one are
158
+ both `kind: 'specialist'` and differ only by origin.
156
159
 
157
160
  - **Public agents** — the per-agent directories under `<accountDir>/agents/<slug>/`,
158
161
  each carrying a `config.json` (`displayName`, `status`, `model`,
@@ -163,19 +166,63 @@ kinds, each card tagged `Public` or `Specialist`:
163
166
  to the public URL (computed client-side: an `admin.` host maps to the `public.`
164
167
  host), plus a **delete** behind a confirm that runs the loud-fail `DELETE`
165
168
  (graph projection cleanup precedes file removal).
166
- - **User-created specialists** — the agent files under
169
+ - **User-created specialists** (`origin: 'specialist'`) — the agent files under
167
170
  `<accountDir>/plugins/<plugin>/agents/<plugin>--<name>.md` (written by
168
- `agent-builder`). `<accountDir>/plugins` is the user-created tree, so only
169
- user-created specialists list here; shipped premium and core specialists (whose
170
- plugins live under `PLATFORM_ROOT`) do not. The specialist modal shows the
171
- model, tools, the owning plugin's skills, and the system-prompt body. Specialists
172
- are authored, edited, and deleted through chat (`agent-builder` /
173
- `specialist-management`), so the specialist modal offers no delete or open-agent
174
- link.
171
+ `agent-builder`). The specialist modal shows the model, tools, the owning
172
+ plugin's skills, and the system-prompt body. Specialists are authored, edited,
173
+ and deleted through chat (`agent-builder` / `specialist-management`), so the
174
+ specialist modal offers no delete or open-agent link.
175
+ - **Shipped specialists** (`origin: 'shipped'`) the union of
176
+ `<accountDir>/specialists/agents/`, `<accountDir>/specialists/agents-disabled/`
177
+ and `PLATFORM_ROOT/templates/specialists/agents/`, in that precedence order.
178
+ `sidebar-artefacts.ts` reads the same union minus the quarantine directory, so
179
+ a disabled agent still appears there through its bundled-template row. The
180
+ route once walked only the account plugins tree, so the twelve
181
+ highest-capability agents on the box were the ones the operator could not see.
182
+ These are the only agents that can be disabled, and the route enforces that
183
+ rather than relying on the UI to withhold the control.
175
184
 
176
185
  The former "Public" flyout toggle in the account menu was removed; this page is
177
186
  the sole agent surface.
178
187
 
188
+ **Risk class.** Every classified card carries a green/amber/red badge derived
189
+ from the agent's declared `tools:` line: green when every tool is `read`, amber
190
+ when the worst is `write_local`, red on any `exec` or `external`, and red when
191
+ any tool resolves to no class at all. Resolution has two sources, because
192
+ frontmatter names tools in Claude Code's namespace: a plugin tool is a direct
193
+ lookup in `ToolSurface.riskByTool`, which is keyed by the same
194
+ canonical `mcp__plugin_<p>_<p>__<tool>` string frontmatter writes, and the
195
+ eleven built-ins carry their own table in `server/lib/agent-risk.ts`. An empty
196
+ tool list is red: no `tools:` line means the full surface, not none of it.
197
+
198
+ A public agent carries `risk: null` and shows no badge. Its tool surface
199
+ resolves through `ADMIN_CORE_TOOLS` plus the plugin allowlists rather than a
200
+ `tools:` line, and classifying its empty list would paint every public agent red
201
+ on a premise nothing established.
202
+
203
+ The detail modal renders one chip per declared tool carrying **that tool's own**
204
+ class, so a `read` tool inside a red agent still reads as harmless; a tool with
205
+ no class is dashed and uncoloured rather than painted as if it were understood.
206
+ Badge colours come from the shared semantic status set: `-solid` with white text
207
+ for the badge, `-tint` with `-textOnTint` for a chip, never white.
208
+
209
+ **Disable.** `<accountDir>/specialists/agents/` is the live dispatchable set —
210
+ `spawn-context.ts` reads it to build the spawn manifest — so disabling **moves**
211
+ the agent file to `<accountDir>/specialists/agents-disabled/` and records the
212
+ basename in a 0600 `<accountDir>/agents-disabled.json`. A flag alone would leave
213
+ the agent running while the card claimed otherwise.
214
+
215
+ The file is moved, never deleted: a premium `--` agent and an operator-edited
216
+ override each exist only in the account dir, so deleting one and later restoring
217
+ from the bundled template would hand back a different agent than the operator
218
+ switched off. A disabled agent stays listed, marked `Disabled`, because an agent
219
+ that vanished from the surface that disabled it could never be switched back on.
220
+
221
+ `provision-account-dir.sh` re-reads the store after its core-specialist recopy
222
+ and removes anything named in it, so an upgrade does not silently re-deliver a
223
+ disabled agent. An unreadable store withholds nothing and says so: a corrupt
224
+ file is not evidence that an agent was disabled.
225
+
179
226
  **Account scope.** The route (`server/routes/admin/agents.ts`) never infers an
180
227
  account from device state. The two reads resolve from the caller's admin session
181
228
  (`requireAdminSession` + the shared `accountDirForSession` in
@@ -189,8 +236,10 @@ explicit `?plugin=` parameter, not inferred from `--` in the slug.
189
236
 
190
237
  | Route | Behaviour |
191
238
  |-------|-----------|
192
- | `GET /api/admin/agents` | Session-scoped. Lists the session account's public `agents/*/` dirs (never the `admin` agent) plus its user-created specialists from `plugins/*/agents/*.md`, each row tagged `kind`. Returns `{agents, accountId, skipped, specialistsSkipped}``accountId` names the account resolved (the client sends it back on a delete); `skipped` counts public dirs whose `config.json` failed to parse; `specialistsSkipped` counts specialist `.md` files with no parseable `name`. 401 when the session maps to no account. |
193
- | `GET /api/admin/agents/:slug` | Session-scoped. Without `?plugin=`, returns a public agent's config fields + four owned docs + a `present` map (a missing or unreadable doc is `''`/`present.<role>=false`, never a 500). With `?plugin=`, returns that plugin's specialist `{kind, model, tools, systemPrompt, skills}`. Unknown 404s, 401 as above. |
239
+ | `GET /api/admin/agents` | Session-scoped. Lists the session account's public `agents/*/` dirs (never the `admin` agent), its user-created specialists from `plugins/*/agents/*.md`, and its shipped specialists, each row tagged `kind` and `origin` and carrying `risk`, `worstTool`, `unresolved` and `disabled`. Returns `{agents, accountId, skipped, specialistsSkipped, shippedSkipped}`. 401 when the session maps to no account. |
240
+ | `GET /api/admin/agents/:slug` | Session-scoped. Without `?plugin=` or `?origin=`, returns a public agent's config fields + four owned docs + a `present` map (a missing or unreadable doc is `''`/`present.<role>=false`, never a 500). With `?plugin=`, that plugin's user-created specialist; with `?origin=shipped`, the shipped one. Both specialist shapes add `{risk, worstTool, unresolved, byTool, disabled}`, where `byTool` is one class per declared tool. Selection is explicit rather than inferred, because a premium file and a user-created specialist under a plugin of the same name produce the same slug. Unknown 404s, 401 as above. |
241
+ | `POST /api/admin/agents/:slug/disable?accountId=` | Shipped agents only, enforced here: a slug present in none of the three shipped directories 404s. Moves the file from `specialists/agents/` to `specialists/agents-disabled/` and records the basename in the 0600 store. Returns `{ok, moved}`; `moved:false` means the agent existed only as a bundled template, so nothing was there to move and the store entry is what stops the next provisioning run delivering it. The store is read strictly before anything moves, so an unreadable store 500s with the file untouched rather than rewriting the file whole from an empty set and dropping every other disabled agent. Same `accountId` contract as delete. |
242
+ | `POST /api/admin/agents/:slug/enable?accountId=` | Covers both of disable's outcomes. Returns `{ok, restored}`: a quarantined file moves back (`restored:true`); a bundled-only agent has nothing to move, so clearing the store entry is the whole job (`restored:false`), which is what stops provisioning withholding it. Treating that second case as "nothing to restore" made disable a one-way door. 404 only when neither directory nor store knows the agent. |
194
243
  | `DELETE /api/admin/agents/:slug?accountId=` | Public agents only, on the named validated account. Removes the dir after `deleteAgentProjection`; refuses the `admin` slug (403) and a missing/unknown `accountId` (400) with no write. Loud-fail: a graph-cleanup throw aborts the file removal. |
195
244
  | `POST /api/admin/agents/:slug/project?accountId=` | Re-projects the named account's on-disk agent into the graph. Same `accountId` contract as delete. |
196
245
 
@@ -217,6 +266,35 @@ Agents page survived undiagnosed on a multi-account install.
217
266
  - `op=delete accountId=<id8> slug=<…> outcome=<ok|failed>` with
218
267
  `reason=<graph-cleanup-failed|rm-error>` on failure (files preserved on a
219
268
  graph-cleanup throw).
269
+ - `op=classify account=<id8> agent=<name> origin=<shipped|public|specialist>
270
+ risk=<green|amber|red> worstTool=<tool> tools=<n> unresolved=<comma-list|none>
271
+ disabled=<bool>` — one line per classified agent per listing, so it fires on
272
+ every page load. `worstTool` makes a wrong class diagnosable without
273
+ re-deriving it. **A non-empty `unresolved=` is the one to act on**: the agent
274
+ is forced red, and the cause is that the built-in table or the registry
275
+ mapping has drifted, not that the agent changed.
276
+ - `op=risk-surface status=load-failed reason=<msg>` — the plugin registry did
277
+ not parse, so every row is forced red and the listing returns
278
+ `riskSurfaceFailed: true`. Without the forced red, every plugin tool would
279
+ land in `unresolved` and a registry outage would read as agent drift across
280
+ every card at once; without the response field, a red row would be
281
+ indistinguishable from a genuinely dangerous agent, because nothing resolved
282
+ so `unresolved` is empty and the per-tool explanation cannot fire.
283
+ - `op=disabled-store status=parse-failed reason=<msg>` — the listing then treats
284
+ every agent as enabled, which is what they actually are. A read surface shows
285
+ the true dispatchable set rather than a comforting one.
286
+ - `op=<disable|enable> accountId=<id8> slug=<…> outcome=<ok|failed>`, with
287
+ `moved=<bool>` on disable and `reason=nothing-quarantined` on a failed enable.
288
+
289
+ **Disable is a no-event failure, so it needs a standing check.** If the store
290
+ names an agent whose file is still in `specialists/agents/`, nothing throws, the
291
+ card reads disabled, and the agent keeps being dispatched. The five-minute
292
+ `account-dir-schema-reconcile` pass reports it as
293
+ `[fs-reconcile] op=agent-parity account=<id8> disabled=<n> stillOnDisk=<list>`,
294
+ fired only on disagreement. The per-cycle heartbeat is `agent-drift=0` on that
295
+ pass's summary line, which is what makes the absence of a per-account line
296
+ readable as "clean" rather than "never audited" — grep the summary line, not the
297
+ per-account one, to confirm the check is running.
220
298
 
221
299
  ### Graph
222
300
 
@@ -350,9 +428,29 @@ the Web Share API and is disabled where unavailable. Signals:
350
428
  `[data-ui] op=mount header=operator home=absent`, `op=select-enter`,
351
429
  `op=select-exit reason=<cancel|action>`, `op=share supported=<bool> count=<n>`.
352
430
 
431
+ **The dashboard has a surface ramp and a display type tier.** Every page sits on
432
+ a page plane, cards rise onto a raised plane, and inputs and scroll regions
433
+ recede onto an inset plane. Each brand chooses how the card separates: SiteDesk's
434
+ card is a shade lighter than its page and casts no shadow, Real Agent's card and
435
+ page are both white and a shadow does the work. The page header is an inverted
436
+ band carrying the page title at the display size. Colours, radii, shadows and
437
+ type weights all come from the brand's own tokens; nothing is hardcoded, and a
438
+ token that resolves to nothing fails the build rather than painting invisibly.
439
+
440
+ **Folders open beside a rail on a wide screen.** At 1280px and above the folder
441
+ view splits in two: a list of the account's folders on the left, the files on
442
+ the right, with the folder you are in highlighted. Below that width the rail is
443
+ hidden and the breadcrumb does the same job.
444
+
445
+ **Status colours are shared.** Success, warning, error and info each carry a
446
+ solid fill, a pale tint and a text colour for that tint. Every pairing meets the
447
+ WCAG AA contrast minimum, on every brand.
448
+
353
449
  **`/data` opens on the account's own folders, as cards.** With no `#path=` in
354
450
  the URL the browser lands on **home**: the account's business folders (the
355
- entity folders of its trade, plus `projects`, `contacts` and `documents`) shown
451
+ entity folders of its trade, plus documents, projects, contacts, the work it
452
+ has produced for you, anything a client has sent in, and any websites it
453
+ has published) shown
356
454
  one per card, each listing the first 20 things inside it and a `+N more` row
357
455
  past that. Tapping a card opens that folder in the ordinary list or grid. There
358
456
  are never loose files at this level. Everything the platform and its plugins
@@ -150,6 +150,32 @@ Skill content, plugin manifests, agent templates, and reference files reference
150
150
 
151
151
  The runtime substitution happens at every read site that flows content into a system prompt or operator-visible UI: the admin agent's `plugin-read` tool (references + `PLUGIN.md`), the `skill-load` tool (SKILL.md by skill name — one-call resolver+reader, the canonical primitive for SKILL.md), the public agent's recursive plugin assembly, and `IDENTITY` / `SOUL` / `AGENTS` / `KNOWLEDGE` markdown reads. Missing or empty `productName` hard-fails — there is no fallback to a default brand string. See [.docs/agents.md](../../.docs/agents.md) § "Brand templating" for the full contract.
152
152
 
153
+ ## Tool risk classes (for plugin authors)
154
+
155
+ Every tool a plugin declares in its `PLUGIN.md` `tools:` block carries three mandatory
156
+ fields. `publicAllowlist` and `adminAllowlist` say who may call the tool. `riskClass`
157
+ says what happens when they do, and takes one of four values:
158
+
159
+ - `read` changes no state anywhere. A remote lookup counts as `read`; reaching the
160
+ network is not by itself consequential.
161
+ - `write_local` mutates account files or the graph on this device and transmits nothing.
162
+ - `exec` runs an operating-system command, or code supplied by the caller or the operator.
163
+ - `external` transmits to, or changes state on, a system outside this device, or delivers
164
+ a message to a person.
165
+
166
+ Leaving the field out is a refusal-to-boot condition, not a silent default. The platform
167
+ refuses to start rather than treat an undeclared consequence as harmless, because a tool
168
+ that quietly reads as `read` escapes every check downstream of it.
169
+
170
+ Declaring the class does not yet gate anything. It gives any consumer one honest answer
171
+ to the question "is this tool consequential?" without maintaining a list of tool names,
172
+ which is what the checks in `platform/plugins/admin/hooks/` have had to do until now.
173
+
174
+ A publish-time gate flags declarations that contradict what the tool's own name says it
175
+ does, and a boot line reports the census across all four classes. The classification
176
+ rule, the exception mechanism, and the diagnostic commands are in
177
+ `.docs/tool-risk-classes.md`.
178
+
153
179
  ## MCP Plugin Observability (for plugin authors)
154
180
 
155
181
  Every `console.error` line from a plugin's MCP server is captured to a per-server raw log. Capture is automatic and needs no import or call: the `mcp-spawn-tee` shim spawns every MCP server and mirrors its stderr to `mcp-<name>-<sessionId>.log` (or `mcp-<name>-nosession.log` for a spawn with no session). That file is keyed by session, which is the conversation, and that is where you grep. Prefix your diagnostics `[your-tool]` so a grep isolates one tool.