@rubytech/create-sitedesk-code 0.1.507 → 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 +12 -0
- package/payload/platform/plugins/admin/PLUGIN.md +10 -5
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +91 -13
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +7 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +90 -12
- package/payload/platform/plugins/memory/PLUGIN.md +6 -3
- package/payload/platform/scripts/__tests__/provision-honours-disabled-agents.test.sh +84 -0
- package/payload/platform/scripts/check-risk-class.mjs +7 -2
- package/payload/platform/scripts/check-specialist-tool-surface.mjs +5 -2
- package/payload/platform/scripts/lib/canonical-tool-names.mjs +7 -4
- package/payload/platform/scripts/lib/provision-account-dir.sh +43 -0
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts +1 -1
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +56 -3
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/tool-surface.d.ts +19 -5
- package/payload/platform/services/claude-session-manager/dist/tool-surface.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/tool-surface.js +50 -11
- package/payload/platform/services/claude-session-manager/dist/tool-surface.js.map +1 -1
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/{AdminLoginScreens-YT7jsh_H.js → AdminLoginScreens-DKeCt1uf.js} +1 -1
- package/payload/server/public/assets/{AdminShell-Ckht9hmU.js → AdminShell-HeELCCjJ.js} +1 -1
- package/payload/server/public/assets/{activity-DH3Wgc3O.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-Bym8QhfA.js → browser-GkVTGqlb.js} +1 -1
- package/payload/server/public/assets/{calendar-CFnufJOy.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-zch0E1GJ.js → file-text-DiscM2pP.js} +1 -1
- package/payload/server/public/assets/{graph-BavHLzpV.js → graph-CRP5DM8C.js} +1 -1
- package/payload/server/public/assets/{graph-labels-CBwh4g-6.js → graph-labels-Du-6KoBE.js} +1 -1
- package/payload/server/public/assets/{maximize-2-bTn_Ozpi.js → maximize-2-D-H_uIV-.js} +1 -1
- package/payload/server/public/assets/{operator-a4IOwf62.js → operator-D5rA76YY.js} +1 -1
- package/payload/server/public/assets/{page-3T2mE63b.js → page-CHK1415C.js} +1 -1
- package/payload/server/public/assets/{page-D6M1CmPe.js → page-DZE3H_n4.js} +1 -1
- package/payload/server/public/assets/{public-B3y_YU1G.js → public-DFHXVkdD.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-BGvzNB1b.js → rotate-ccw-CesEJsuO.js} +1 -1
- package/payload/server/public/assets/{routines-C7Dr16am.js → routines-DYMYY_r8.js} +1 -1
- package/payload/server/public/assets/{skills-DFeb3XO3.js → skills-67jtJ16D.js} +1 -1
- package/payload/server/public/assets/{tasks-CSfnCKJe.js → tasks-D2C2a-uk.js} +1 -1
- package/payload/server/public/assets/{time-entry-format-BgZqOOqG.js → time-entry-format-BixTGQAd.js} +1 -1
- package/payload/server/public/assets/{triangle-alert-DBJ7xz_t.js → triangle-alert-Co9RWb9q.js} +1 -1
- package/payload/server/public/assets/{useCopyFeedback-BFOujMl-.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-BP6CpYQD.js → useVoiceRecorder-CZDDiwMD.js} +1 -1
- package/payload/server/public/assets/{wrench-DdaRhfzS.js → wrench-CjQqdj4P.js} +1 -1
- package/payload/server/public/brand/fonts/inter-600.woff2 +0 -0
- package/payload/server/public/brand/fonts/inter-700.woff2 +0 -0
- package/payload/server/public/brand/fonts/inter-800.woff2 +0 -0
- package/payload/server/public/brand-defaults.css +22 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +7 -7
- package/payload/server/public/chat.html +13 -13
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +9 -9
- package/payload/server/public/index.html +14 -14
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +13 -13
- package/payload/server/public/routines.html +6 -6
- package/payload/server/public/skills.html +5 -5
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +882 -251
- package/payload/server/public/assets/admin-DetvulAu.js +0 -1
- package/payload/server/public/assets/agents-X_IvDHLZ.js +0 -1
- package/payload/server/public/assets/chat-wrckyIpw.js +0 -1
- package/payload/server/public/assets/chevron-left-Dtk3jEK5.js +0 -1
- package/payload/server/public/assets/chevron-right-Dih6IZ8B.js +0 -1
- package/payload/server/public/assets/clock-iJCrrmWM.js +0 -1
- package/payload/server/public/assets/data-D76--TYs.js +0 -1
- package/payload/server/public/assets/useSubAccountSwitcher-0PfRrN6i.js +0 -9
- package/payload/server/public/assets/useSubAccountSwitcher-BkQ5tA8A.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rubytech/create-sitedesk-code",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.508",
|
|
4
4
|
"description": "Install SiteDesk — automated back office for independent building contractors",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-sitedesk-code": "./dist/index.js"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"build": "tsc",
|
|
14
14
|
"bundle": "node scripts/bundle.js",
|
|
15
15
|
"test": "npm run build && node --test 'dist/__tests__/*.test.js'",
|
|
16
|
-
"prepublishOnly": "bash ../../platform/scripts/verify-skill-tool-surface.sh && node ../../platform/scripts/check-plugin-tools-mcp-consistency.mjs && node ../../platform/scripts/check-risk-class.mjs && node ../../platform/scripts/check-specialist-tool-surface.mjs && node ../../platform/scripts/check-agent-contract.mjs && node ../../platform/scripts/check-routing-prose-bash.mjs && node ../../platform/scripts/check-no-task-id-leaks.mjs && node ../../platform/scripts/check-no-esm-require.mjs && node ../../platform/scripts/check-no-raw-mcp-registrations.mjs && node ../../platform/scripts/check-skill-load-coverage.mjs && node ../../platform/scripts/check-architecture-skill-no-drift.mjs && node ../../platform/ui/scripts/check-route-wiring.mjs && node ../../platform/ui/scripts/check-edge-admin-routes.mjs && npm run build && node --test 'dist/__tests__/*.test.js' && chmod +x dist/index.js && npm run bundle && bash ../../platform/scripts/smoke-boot-services.sh && node ../../platform/ui/scripts/check-bundle-node-imports.mjs --dir=./payload/server/public/assets"
|
|
16
|
+
"prepublishOnly": "bash ../../platform/scripts/verify-skill-tool-surface.sh && node ../../platform/scripts/check-plugin-tools-mcp-consistency.mjs && node ../../platform/scripts/check-risk-class.mjs && node ../../platform/scripts/check-specialist-tool-surface.mjs && node ../../platform/scripts/check-agent-contract.mjs && node ../../platform/scripts/check-routing-prose-bash.mjs && node ../../platform/scripts/check-no-task-id-leaks.mjs && node ../../platform/scripts/check-no-esm-require.mjs && node ../../platform/scripts/check-no-raw-mcp-registrations.mjs && node ../../platform/scripts/check-skill-load-coverage.mjs && node ../../platform/scripts/check-architecture-skill-no-drift.mjs && node ../../platform/ui/scripts/check-brand-fonts.mjs && node ../../platform/ui/scripts/check-route-wiring.mjs && node ../../platform/ui/scripts/check-edge-admin-routes.mjs && npm run build && node --test 'dist/__tests__/*.test.js' && chmod +x dist/index.js && npm run bundle && bash ../../platform/scripts/smoke-boot-services.sh && node ../../platform/ui/scripts/check-bundle-node-imports.mjs --dir=./payload/server/public/assets"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
@@ -140,6 +140,18 @@
|
|
|
140
140
|
{
|
|
141
141
|
"weight": 500,
|
|
142
142
|
"file": "inter-500.woff2"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"weight": 600,
|
|
146
|
+
"file": "inter-600.woff2"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"weight": 700,
|
|
150
|
+
"file": "inter-700.woff2"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"weight": 800,
|
|
154
|
+
"file": "inter-800.woff2"
|
|
143
155
|
}
|
|
144
156
|
]
|
|
145
157
|
},
|
|
@@ -148,11 +148,16 @@ tools:
|
|
|
148
148
|
adminAllowlist: true
|
|
149
149
|
riskClass: write_local
|
|
150
150
|
hidden:
|
|
151
|
-
- remote-auth-status
|
|
152
|
-
|
|
153
|
-
- remote-auth-
|
|
154
|
-
|
|
155
|
-
-
|
|
151
|
+
- name: remote-auth-status
|
|
152
|
+
riskClass: read
|
|
153
|
+
- name: remote-auth-set-password
|
|
154
|
+
riskClass: write_local
|
|
155
|
+
- name: remote-auth-revoke-password
|
|
156
|
+
riskClass: write_local
|
|
157
|
+
- name: qr-generate
|
|
158
|
+
riskClass: write_local
|
|
159
|
+
- name: wifi
|
|
160
|
+
riskClass: exec
|
|
156
161
|
always: true
|
|
157
162
|
embed: false
|
|
158
163
|
mcp:
|
|
@@ -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:
|
|
4
|
+
content-hash: sha256:b15d0e491d957f75523ecf7056f8004bafcdf448767c0c3dd64b40a73926b58b
|
|
5
5
|
brand: sitedesk-code
|
|
6
6
|
product-name: SiteDesk
|
|
7
7
|
---
|
|
@@ -3104,8 +3104,11 @@ not-found|rm-error>`.
|
|
|
3104
3104
|
|
|
3105
3105
|
### Agents
|
|
3106
3106
|
|
|
3107
|
-
The Agents surface (`/agents`) is the admin view of the account's agents
|
|
3108
|
-
|
|
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.
|
|
3109
3112
|
|
|
3110
3113
|
- **Public agents** — the per-agent directories under `<accountDir>/agents/<slug>/`,
|
|
3111
3114
|
each carrying a `config.json` (`displayName`, `status`, `model`,
|
|
@@ -3116,19 +3119,63 @@ kinds, each card tagged `Public` or `Specialist`:
|
|
|
3116
3119
|
to the public URL (computed client-side: an `admin.` host maps to the `public.`
|
|
3117
3120
|
host), plus a **delete** behind a confirm that runs the loud-fail `DELETE`
|
|
3118
3121
|
(graph projection cleanup precedes file removal).
|
|
3119
|
-
- **User-created specialists** — the agent files under
|
|
3122
|
+
- **User-created specialists** (`origin: 'specialist'`) — the agent files under
|
|
3120
3123
|
`<accountDir>/plugins/<plugin>/agents/<plugin>--<name>.md` (written by
|
|
3121
|
-
`agent-builder`).
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
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.
|
|
3128
3137
|
|
|
3129
3138
|
The former "Public" flyout toggle in the account menu was removed; this page is
|
|
3130
3139
|
the sole agent surface.
|
|
3131
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
|
+
|
|
3132
3179
|
**Account scope.** The route (`server/routes/admin/agents.ts`) never infers an
|
|
3133
3180
|
account from device state. The two reads resolve from the caller's admin session
|
|
3134
3181
|
(`requireAdminSession` + the shared `accountDirForSession` in
|
|
@@ -3142,8 +3189,10 @@ explicit `?plugin=` parameter, not inferred from `--` in the slug.
|
|
|
3142
3189
|
|
|
3143
3190
|
| Route | Behaviour |
|
|
3144
3191
|
|-------|-----------|
|
|
3145
|
-
| `GET /api/admin/agents` | Session-scoped. Lists the session account's public `agents/*/` dirs (never the `admin` agent)
|
|
3146
|
-
| `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=`,
|
|
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. |
|
|
3147
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. |
|
|
3148
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. |
|
|
3149
3198
|
|
|
@@ -3170,6 +3219,35 @@ Agents page survived undiagnosed on a multi-account install.
|
|
|
3170
3219
|
- `op=delete accountId=<id8> slug=<…> outcome=<ok|failed>` with
|
|
3171
3220
|
`reason=<graph-cleanup-failed|rm-error>` on failure (files preserved on a
|
|
3172
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.
|
|
3173
3251
|
|
|
3174
3252
|
### Graph
|
|
3175
3253
|
|
|
@@ -9,6 +9,13 @@ 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
|
+
|
|
12
19
|
## 2026-07-25 (0.1.507)
|
|
13
20
|
|
|
14
21
|
- Chat now updates the moment a message or permission prompt arrives, instead of checking every couple of seconds.
|
|
@@ -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
|
|
155
|
-
|
|
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`).
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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)
|
|
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=`,
|
|
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
|
|
|
@@ -168,9 +168,12 @@ tools:
|
|
|
168
168
|
adminAllowlist: true
|
|
169
169
|
riskClass: read
|
|
170
170
|
hidden:
|
|
171
|
-
- session-compact
|
|
172
|
-
|
|
173
|
-
-
|
|
171
|
+
- name: session-compact
|
|
172
|
+
riskClass: write_local
|
|
173
|
+
- name: session-compact-status
|
|
174
|
+
riskClass: read
|
|
175
|
+
- name: image-fetch
|
|
176
|
+
riskClass: read
|
|
174
177
|
skills:
|
|
175
178
|
- skills/conversational-memory/SKILL.md
|
|
176
179
|
- skills/preference-audit/SKILL.md
|
|
@@ -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
|
|
@@ -92,13 +92,18 @@ function collectPluginDirs() {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/** Independent of tool-surface.ts on purpose: a gate that shares the parser it
|
|
95
|
-
* is checking cannot detect that parser drifting.
|
|
95
|
+
* is checking cannot detect that parser drifting.
|
|
96
|
+
*
|
|
97
|
+
* Task 1998 — `hidden:` entries carry the same name+riskClass shape as
|
|
98
|
+
* `tools:` entries and are served by the same MCP server, so a hidden tool
|
|
99
|
+
* can contradict its own name exactly as a visible one can. Scanning only
|
|
100
|
+
* `tools:` would leave eight declarations outside the gate. */
|
|
96
101
|
function declaredTools(manifestPath) {
|
|
97
102
|
const out = []
|
|
98
103
|
let current = null
|
|
99
104
|
let inTools = false
|
|
100
105
|
for (const line of readFileSync(manifestPath, 'utf8').split(/\r?\n/)) {
|
|
101
|
-
if (/^tools:\s*$/.test(line)) { inTools = true; continue }
|
|
106
|
+
if (/^(tools|hidden):\s*$/.test(line)) { inTools = true; current = null; continue }
|
|
102
107
|
if (inTools && /^[A-Za-z_]/.test(line)) inTools = false
|
|
103
108
|
if (!inTools) continue
|
|
104
109
|
const name = line.match(/^\s+-\s+name:\s*(.+?)\s*$/)
|
|
@@ -88,8 +88,11 @@ function parsePluginManifest(rawManifest) {
|
|
|
88
88
|
if (nameLine) { tools.push(nameLine[1]); continue; }
|
|
89
89
|
}
|
|
90
90
|
if (mode === 'hidden') {
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
// Task 1998 — hidden entries carry the same `- name:` + `riskClass:`
|
|
92
|
+
// shape as tools entries. Names only here; the gate checks membership
|
|
93
|
+
// of the `all` surface, not consequence.
|
|
94
|
+
const nameLine = line.match(/^\s+-\s+name:\s*(.+?)\s*$/);
|
|
95
|
+
if (nameLine) hidden.push(nameLine[1]);
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
98
|
return { tools, hidden, hasTools };
|
|
@@ -36,9 +36,12 @@ function extractFrontmatter(md) {
|
|
|
36
36
|
return md.slice(3, end)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// Collect `- name: <x>` entries under a bare `tools:` block and
|
|
40
|
-
//
|
|
41
|
-
//
|
|
39
|
+
// Collect `- name: <x>` entries under a bare `tools:` block and under a bare
|
|
40
|
+
// `hidden:` block. Both contribute names a specialist may list in its `tools:`
|
|
41
|
+
// frontmatter, so both belong to the live set. Task 1998 gave the hidden block
|
|
42
|
+
// the same name+riskClass shape as tools, so the two branches now read alike;
|
|
43
|
+
// this parser stays separate from tool-surface.ts on purpose, so a drift in one
|
|
44
|
+
// shows up as a diff rather than as agreement between a parser and itself.
|
|
42
45
|
function parseToolNames(frontmatter) {
|
|
43
46
|
const lines = frontmatter.split('\n')
|
|
44
47
|
const names = []
|
|
@@ -57,7 +60,7 @@ function parseToolNames(frontmatter) {
|
|
|
57
60
|
const m = line.match(/^\s+-\s+name:\s*(.+?)\s*$/)
|
|
58
61
|
if (m) names.push(m[1])
|
|
59
62
|
} else if (mode === 'hidden') {
|
|
60
|
-
const m = line.match(/^\s+-\s+(.+?)\s*$/)
|
|
63
|
+
const m = line.match(/^\s+-\s+name:\s*(.+?)\s*$/)
|
|
61
64
|
if (m) names.push(m[1])
|
|
62
65
|
}
|
|
63
66
|
}
|
|
@@ -251,6 +251,49 @@ SETTINGS_EOF
|
|
|
251
251
|
[ -f "$specialist" ] && cp "$specialist" "$ACCOUNT_DIR/specialists/agents/$(basename "$specialist")"
|
|
252
252
|
done
|
|
253
253
|
|
|
254
|
+
# Task 1996 — the copy loop above re-delivers every core specialist, which
|
|
255
|
+
# would silently undo a disable on the next publish. specialists/agents/ is
|
|
256
|
+
# the dispatchable set (spawn-context.ts reads it), so an agent the operator
|
|
257
|
+
# switched off is removed again here. The quarantined copy under
|
|
258
|
+
# specialists/agents-disabled/ is never touched: it is the operator's own
|
|
259
|
+
# file, and for a premium or edited agent it is the only copy.
|
|
260
|
+
#
|
|
261
|
+
# An unreadable store withholds NOTHING. A corrupt file is not evidence that
|
|
262
|
+
# an agent was disabled, and withholding the whole set on a parse error would
|
|
263
|
+
# turn a bad byte into a fleet-wide loss of specialists.
|
|
264
|
+
if [ -f "$ACCOUNT_DIR/agents-disabled.json" ]; then
|
|
265
|
+
local _disabled_names _withheld=0
|
|
266
|
+
if _disabled_names="$(python3 -c '
|
|
267
|
+
import json, sys
|
|
268
|
+
with open(sys.argv[1]) as fh:
|
|
269
|
+
names = json.load(fh)["disabled"]
|
|
270
|
+
if not isinstance(names, list):
|
|
271
|
+
raise SystemExit(1)
|
|
272
|
+
for n in names:
|
|
273
|
+
if isinstance(n, str) and n.endswith(".md") and "/" not in n and ".." not in n:
|
|
274
|
+
print(n)
|
|
275
|
+
' "$ACCOUNT_DIR/agents-disabled.json" 2>/dev/null)"; then
|
|
276
|
+
local _name
|
|
277
|
+
while IFS= read -r _name; do
|
|
278
|
+
[ -n "$_name" ] || continue
|
|
279
|
+
if [ -e "$ACCOUNT_DIR/specialists/agents/$_name" ]; then
|
|
280
|
+
# MOVE, never delete. In the drift state the standing parity check
|
|
281
|
+
# exists to catch (store names it, file is live, quarantine empty) a
|
|
282
|
+
# delete would destroy the only copy of a premium `--` agent or an
|
|
283
|
+
# operator-edited override: the replace loop above skips `*--*` and
|
|
284
|
+
# the copy loop only restores templates, so nothing re-delivers it.
|
|
285
|
+
mkdir -p "$ACCOUNT_DIR/specialists/agents-disabled"
|
|
286
|
+
mv -f "$ACCOUNT_DIR/specialists/agents/$_name" \
|
|
287
|
+
"$ACCOUNT_DIR/specialists/agents-disabled/$_name"
|
|
288
|
+
_withheld=$((_withheld + 1))
|
|
289
|
+
fi
|
|
290
|
+
done <<< "$_disabled_names"
|
|
291
|
+
echo " specialists agents-withheld=$_withheld (disabled by operator)"
|
|
292
|
+
else
|
|
293
|
+
echo " specialists agents-disabled-store-unreadable — withholding nothing"
|
|
294
|
+
fi
|
|
295
|
+
fi
|
|
296
|
+
|
|
254
297
|
rm -f "$ACCOUNT_DIR/.claude/agents/"*.md 2>/dev/null || true
|
|
255
298
|
|
|
256
299
|
# SOUL.md — user-controlled. Only create if missing. Never overwrite.
|
package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface ReconcileResult extends ReconcileCounts {
|
|
|
15
15
|
/** The single per-run summary line body. A non-zero count is the standing signal
|
|
16
16
|
* that an agent authored folders the hard guards could not prevent. Pure
|
|
17
17
|
* formatter so the emitter and its test share one wording. */
|
|
18
|
-
export declare function formatReconcileLine(c: ReconcileCounts, homeMissing?: number, homeUnaudited?: number): string;
|
|
18
|
+
export declare function formatReconcileLine(c: ReconcileCounts, homeMissing?: number, homeUnaudited?: number, agentDrift?: number): string;
|
|
19
19
|
/** Parse the fenced ```allowed-top-level block from an account's SCHEMA.md —
|
|
20
20
|
* the same structured set fs-schema-guard.sh parses with awk. Returns the
|
|
21
21
|
* entry names (blank lines dropped). An absent block yields an empty set. */
|
package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-dir-schema-reconcile.d.ts","sourceRoot":"","sources":["../src/account-dir-schema-reconcile.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"account-dir-schema-reconcile.d.ts","sourceRoot":"","sources":["../src/account-dir-schema-reconcile.ts"],"names":[],"mappings":"AA2FA,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD;;;;;0CAKsC;IACtC,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;+DAE+D;AAC/D,wBAAgB,mBAAmB,CACjC,CAAC,EAAE,eAAe,EAClB,WAAW,SAAI,EACf,aAAa,SAAI,EACjB,UAAU,SAAI,GACb,MAAM,CAGR;AAED;;8EAE8E;AAC9E,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAcpE;AA+ED;;;8EAG8E;AAC9E,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,CAoCpE;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAC/B;AA0DD;;;;;;;;;;;;;iCAaiC;AACjC,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CA8DtD;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,IAAI,IAAI,CAAA;IACb,IAAI,IAAI,IAAI,CAAA;CACb;AAED;oDACoD;AACpD,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,aAAa,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC3C,yBAAyB,CAiB3B"}
|