@rubytech/create-sitedesk-code 0.1.513 → 0.1.515
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 +1 -1
- package/payload/platform/docs/superpowers/plans/2026-07-27-task-2016-disabled-agent-routing.md +624 -0
- package/payload/platform/docs/superpowers/plans/2026-07-27-task-2052-account-machinery-write-fence.md +250 -0
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2016-disabled-agent-routing-design.md +139 -0
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2028-declared-file-write-deny-design.md +2 -1
- package/payload/platform/docs/superpowers/specs/2026-07-27-task-2052-account-machinery-write-fence-design.md +150 -0
- package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh +41 -0
- package/payload/platform/plugins/admin/hooks/fs-schema-guard-bash-post.sh +8 -0
- package/payload/platform/plugins/admin/hooks/fs-schema-guard.sh +40 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js +14 -16
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/specialist-registry.d.ts +14 -0
- package/payload/platform/plugins/admin/mcp/dist/specialist-registry.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/specialist-registry.js +57 -0
- package/payload/platform/plugins/admin/mcp/dist/specialist-registry.js.map +1 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +15 -3
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
- package/payload/platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs +1 -1
- package/payload/platform/plugins/docs/references/admin-ui.md +14 -2
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +3 -3
- package/payload/platform/scripts/__tests__/agents-md-bootstrap.test.sh +47 -0
- package/payload/platform/scripts/lib/agents-md-bootstrap.sh +58 -1
- package/payload/platform/services/claude-session-manager/dist/config.d.ts +48 -12
- package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/config.js +65 -15
- package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +7 -4
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.js +17 -2
- package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.js.map +1 -1
- package/payload/platform/templates/account-schema/SCHEMA.md +17 -0
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/{AdminLoginScreens-bksN0XPe.js → AdminLoginScreens-Cs1jSnOF.js} +1 -1
- package/payload/server/public/assets/AdminLoginScreens-Cs1jSnOF.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-Cs1jSnOF.js.gz +0 -0
- package/payload/server/public/assets/{AdminShell-BMPEWNZk.js → AdminShell-DkrURZnF.js} +1 -1
- package/payload/server/public/assets/AdminShell-DkrURZnF.js.br +0 -0
- package/payload/server/public/assets/AdminShell-DkrURZnF.js.gz +0 -0
- package/payload/server/public/assets/{activity-ByWYF5dC.js → activity-BgZISMuE.js} +1 -1
- package/payload/server/public/assets/activity-BgZISMuE.js.br +0 -0
- package/payload/server/public/assets/activity-BgZISMuE.js.gz +0 -0
- package/payload/server/public/assets/{admin-6QN-k3zQ.js → admin-DjFysc3-.js} +1 -1
- package/payload/server/public/assets/admin-DjFysc3-.js.br +0 -0
- package/payload/server/public/assets/admin-DjFysc3-.js.gz +0 -0
- package/payload/server/public/assets/{agents-C0rTfrYd.js → agents-Bx-MSZW6.js} +1 -1
- package/payload/server/public/assets/agents-Bx-MSZW6.js.br +0 -0
- package/payload/server/public/assets/agents-Bx-MSZW6.js.gz +0 -0
- package/payload/server/public/assets/{browser-4tTaem8N.js → browser-1I7syhHu.js} +1 -1
- package/payload/server/public/assets/browser-1I7syhHu.js.br +0 -0
- package/payload/server/public/assets/browser-1I7syhHu.js.gz +0 -0
- package/payload/server/public/assets/{calendar-BEjnkKG1.js → calendar-D_04JIo1.js} +1 -1
- package/payload/server/public/assets/calendar-D_04JIo1.js.br +0 -0
- package/payload/server/public/assets/calendar-D_04JIo1.js.gz +0 -0
- package/payload/server/public/assets/chat-BFH3FM5a.js +1 -0
- package/payload/server/public/assets/chat-BFH3FM5a.js.br +0 -0
- package/payload/server/public/assets/chat-BFH3FM5a.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-Ux9-qLkk.js +1 -0
- package/payload/server/public/assets/chevron-right-BwIuCfyG.js +1 -0
- package/payload/server/public/assets/chevron-right-BwIuCfyG.js.br +0 -0
- package/payload/server/public/assets/clock-DhJdZRhC.js +1 -0
- package/payload/server/public/assets/clock-DhJdZRhC.js.br +0 -0
- package/payload/server/public/assets/data-ChY_uSIk.js +1 -0
- package/payload/server/public/assets/data-ChY_uSIk.js.br +0 -0
- package/payload/server/public/assets/data-ChY_uSIk.js.gz +0 -0
- package/payload/server/public/assets/{file-text-DMKw3nIk.js → file-text-DkvKqHMx.js} +1 -1
- package/payload/server/public/assets/file-text-DkvKqHMx.js.br +0 -0
- package/payload/server/public/assets/file-text-DkvKqHMx.js.gz +0 -0
- package/payload/server/public/assets/{graph-C3vW8lvw.js → graph-CCZhrwZX.js} +1 -1
- package/payload/server/public/assets/graph-CCZhrwZX.js.br +0 -0
- package/payload/server/public/assets/graph-CCZhrwZX.js.gz +0 -0
- package/payload/server/public/assets/{graph-labels-BZgd0L6n.js → graph-labels-BJ7puQkX.js} +1 -1
- package/payload/server/public/assets/graph-labels-BJ7puQkX.js.br +0 -0
- package/payload/server/public/assets/graph-labels-BJ7puQkX.js.gz +0 -0
- package/payload/server/public/assets/{maximize-2-la3IBA3M.js → maximize-2-Xdj804U1.js} +1 -1
- package/payload/server/public/assets/maximize-2-Xdj804U1.js.br +0 -0
- package/payload/server/public/assets/maximize-2-Xdj804U1.js.gz +0 -0
- package/payload/server/public/assets/{operator-D9NgVL9_.js → operator-BkfNpBs7.js} +1 -1
- package/payload/server/public/assets/operator-BkfNpBs7.js.br +0 -0
- package/payload/server/public/assets/operator-BkfNpBs7.js.gz +0 -0
- package/payload/server/public/assets/{page-DC11gesX.js → page-BoYc9c4a.js} +1 -1
- package/payload/server/public/assets/page-BoYc9c4a.js.br +0 -0
- package/payload/server/public/assets/page-BoYc9c4a.js.gz +0 -0
- package/payload/server/public/assets/{page-BWFHRIAH.js → page-D5n8ShE7.js} +1 -1
- package/payload/server/public/assets/page-D5n8ShE7.js.br +0 -0
- package/payload/server/public/assets/page-D5n8ShE7.js.gz +0 -0
- package/payload/server/public/assets/{public-DQbbLHQN.js → public-CX5XSrxD.js} +1 -1
- package/payload/server/public/assets/public-CX5XSrxD.js.br +0 -0
- package/payload/server/public/assets/public-CX5XSrxD.js.gz +0 -0
- package/payload/server/public/assets/{rotate-ccw-OQD5si8N.js → rotate-ccw-dh_OhL23.js} +1 -1
- package/payload/server/public/assets/rotate-ccw-dh_OhL23.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-dh_OhL23.js.gz +0 -0
- package/payload/server/public/assets/{routines-asrBGZUR.js → routines-bb7dxcrr.js} +1 -1
- package/payload/server/public/assets/routines-bb7dxcrr.js.br +0 -0
- package/payload/server/public/assets/routines-bb7dxcrr.js.gz +0 -0
- package/payload/server/public/assets/{skills-D4ECwoxa.js → skills-TeMwxXmJ.js} +1 -1
- package/payload/server/public/assets/skills-TeMwxXmJ.js.br +0 -0
- package/payload/server/public/assets/skills-TeMwxXmJ.js.gz +0 -0
- package/payload/server/public/assets/{tasks-U3QdbUsO.js → tasks-BqBBJlkg.js} +1 -1
- package/payload/server/public/assets/tasks-BqBBJlkg.js.br +0 -0
- package/payload/server/public/assets/tasks-BqBBJlkg.js.gz +0 -0
- package/payload/server/public/assets/{time-entry-format-BgdTKgYr.js → time-entry-format-Cy_T-wiD.js} +1 -1
- package/payload/server/public/assets/time-entry-format-Cy_T-wiD.js.br +3 -0
- package/payload/server/public/assets/time-entry-format-Cy_T-wiD.js.gz +0 -0
- package/payload/server/public/assets/{triangle-alert-Ck_3VhT9.js → triangle-alert-D-bSbPnX.js} +1 -1
- package/payload/server/public/assets/triangle-alert-D-bSbPnX.js.br +1 -0
- package/payload/server/public/assets/triangle-alert-D-bSbPnX.js.gz +0 -0
- package/payload/server/public/assets/{useCopyFeedback-BdUwyNpa.js → useCopyFeedback-L7L55nFl.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-L7L55nFl.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-L7L55nFl.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BYl2c7xA.css +1 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BYl2c7xA.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BYl2c7xA.css.gz +0 -0
- package/payload/server/public/assets/{useVoiceRecorder-ChnBrzbZ.js → useVoiceRecorder-MuZ7lpet.js} +1 -1
- package/payload/server/public/assets/useVoiceRecorder-MuZ7lpet.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-MuZ7lpet.js.gz +0 -0
- package/payload/server/public/assets/{wrench-Cpee3C3J.js → wrench-DUG1SZQB.js} +1 -1
- package/payload/server/public/assets/wrench-DUG1SZQB.js.br +0 -0
- package/payload/server/public/assets/wrench-DUG1SZQB.js.gz +0 -0
- package/payload/server/public/brand-defaults.css +2 -0
- 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 +138 -19
- package/payload/server/public/assets/AdminLoginScreens-bksN0XPe.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-bksN0XPe.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-BMPEWNZk.js.br +0 -0
- package/payload/server/public/assets/AdminShell-BMPEWNZk.js.gz +0 -0
- package/payload/server/public/assets/activity-ByWYF5dC.js.br +0 -0
- package/payload/server/public/assets/activity-ByWYF5dC.js.gz +0 -0
- package/payload/server/public/assets/admin-6QN-k3zQ.js.br +0 -0
- package/payload/server/public/assets/admin-6QN-k3zQ.js.gz +0 -0
- package/payload/server/public/assets/agents-C0rTfrYd.js.br +0 -0
- package/payload/server/public/assets/agents-C0rTfrYd.js.gz +0 -0
- package/payload/server/public/assets/browser-4tTaem8N.js.br +0 -0
- package/payload/server/public/assets/browser-4tTaem8N.js.gz +0 -0
- package/payload/server/public/assets/calendar-BEjnkKG1.js.br +0 -0
- package/payload/server/public/assets/calendar-BEjnkKG1.js.gz +0 -0
- package/payload/server/public/assets/chat-DBgzjDIE.js +0 -1
- package/payload/server/public/assets/chat-DBgzjDIE.js.br +0 -0
- package/payload/server/public/assets/chat-DBgzjDIE.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-DqYv3oFh.js +0 -1
- package/payload/server/public/assets/chevron-right-CQfPGsFb.js +0 -1
- package/payload/server/public/assets/chevron-right-CQfPGsFb.js.br +0 -0
- package/payload/server/public/assets/clock-Dn6FHB51.js +0 -1
- package/payload/server/public/assets/clock-Dn6FHB51.js.br +0 -0
- package/payload/server/public/assets/clock-Dn6FHB51.js.gz +0 -0
- package/payload/server/public/assets/data-D_e6Vdfd.js +0 -1
- package/payload/server/public/assets/data-D_e6Vdfd.js.br +0 -0
- package/payload/server/public/assets/data-D_e6Vdfd.js.gz +0 -0
- package/payload/server/public/assets/file-text-DMKw3nIk.js.br +0 -0
- package/payload/server/public/assets/file-text-DMKw3nIk.js.gz +0 -0
- package/payload/server/public/assets/graph-C3vW8lvw.js.br +0 -0
- package/payload/server/public/assets/graph-C3vW8lvw.js.gz +0 -0
- package/payload/server/public/assets/graph-labels-BZgd0L6n.js.br +0 -0
- package/payload/server/public/assets/graph-labels-BZgd0L6n.js.gz +0 -0
- package/payload/server/public/assets/maximize-2-la3IBA3M.js.br +0 -0
- package/payload/server/public/assets/maximize-2-la3IBA3M.js.gz +0 -0
- package/payload/server/public/assets/operator-D9NgVL9_.js.br +0 -0
- package/payload/server/public/assets/operator-D9NgVL9_.js.gz +0 -0
- package/payload/server/public/assets/page-BWFHRIAH.js.br +0 -0
- package/payload/server/public/assets/page-BWFHRIAH.js.gz +0 -0
- package/payload/server/public/assets/page-DC11gesX.js.br +0 -0
- package/payload/server/public/assets/page-DC11gesX.js.gz +0 -0
- package/payload/server/public/assets/public-DQbbLHQN.js.br +0 -0
- package/payload/server/public/assets/public-DQbbLHQN.js.gz +0 -0
- package/payload/server/public/assets/rotate-ccw-OQD5si8N.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-OQD5si8N.js.gz +0 -0
- package/payload/server/public/assets/routines-asrBGZUR.js.br +0 -0
- package/payload/server/public/assets/routines-asrBGZUR.js.gz +0 -0
- package/payload/server/public/assets/skills-D4ECwoxa.js.br +0 -0
- package/payload/server/public/assets/skills-D4ECwoxa.js.gz +0 -0
- package/payload/server/public/assets/tasks-U3QdbUsO.js.br +0 -0
- package/payload/server/public/assets/tasks-U3QdbUsO.js.gz +0 -0
- package/payload/server/public/assets/time-entry-format-BgdTKgYr.js.br +0 -0
- package/payload/server/public/assets/time-entry-format-BgdTKgYr.js.gz +0 -0
- package/payload/server/public/assets/triangle-alert-Ck_3VhT9.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-Ck_3VhT9.js.gz +0 -0
- package/payload/server/public/assets/useCopyFeedback-BdUwyNpa.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-BdUwyNpa.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DOWQXDT8.css +0 -1
- package/payload/server/public/assets/useSubAccountSwitcher-DOWQXDT8.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DOWQXDT8.css.gz +0 -0
- package/payload/server/public/assets/useVoiceRecorder-ChnBrzbZ.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-ChnBrzbZ.js.gz +0 -0
- package/payload/server/public/assets/wrench-Cpee3C3J.js.br +0 -2
- package/payload/server/public/assets/wrench-Cpee3C3J.js.gz +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-CJKjEPNN.js → useSubAccountSwitcher-DH0P6QMV.js} +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-CJKjEPNN.js.br → useSubAccountSwitcher-DH0P6QMV.js.br} +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-CJKjEPNN.js.gz → useSubAccountSwitcher-DH0P6QMV.js.gz} +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface SpecialistRegistryReport {
|
|
2
|
+
/** One ` <name>: <status>` line per AGENTS.md entry, in file order. */
|
|
3
|
+
lines: string[];
|
|
4
|
+
registered: number;
|
|
5
|
+
missing: number;
|
|
6
|
+
disabled: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function classifySpecialists(opts: {
|
|
9
|
+
agentsMd: string;
|
|
10
|
+
specialistsAgentsDir: string;
|
|
11
|
+
accountDir: string;
|
|
12
|
+
chromeUp: boolean;
|
|
13
|
+
}): SpecialistRegistryReport;
|
|
14
|
+
//# sourceMappingURL=specialist-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"specialist-registry.d.ts","sourceRoot":"","sources":["../src/specialist-registry.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,wBAAwB;IACvC,wEAAwE;IACxE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAkBD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;CACnB,GAAG,wBAAwB,CA4B3B"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Task 2016 — the specialist half of `system-status`.
|
|
2
|
+
//
|
|
3
|
+
// Disabling an agent MOVES its file out of `specialists/agents/` (Task 1996),
|
|
4
|
+
// so a walk of AGENTS.md against that directory sees an absent file and, before
|
|
5
|
+
// this module, called it `missing file` — an operator action rendered as an
|
|
6
|
+
// install fault. The disabled store is what tells the two apart.
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
/** Basenames the operator switched off. An unreadable store names nothing: a
|
|
10
|
+
* corrupt file is not evidence that an agent was disabled, and the absent file
|
|
11
|
+
* stays `missing file`, which is the honest answer when the store cannot be
|
|
12
|
+
* read. */
|
|
13
|
+
function readDisabledStore(accountDir) {
|
|
14
|
+
const p = resolve(accountDir, "agents-disabled.json");
|
|
15
|
+
if (!existsSync(p))
|
|
16
|
+
return new Set();
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(readFileSync(p, "utf-8"));
|
|
19
|
+
if (!Array.isArray(parsed.disabled))
|
|
20
|
+
return new Set();
|
|
21
|
+
return new Set(parsed.disabled.filter((x) => typeof x === "string"));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return new Set();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function classifySpecialists(opts) {
|
|
28
|
+
const entries = opts.agentsMd.match(/^- \*\*specialists:([^*]+)\*\*/gm) ?? [];
|
|
29
|
+
const disabled = readDisabledStore(opts.accountDir);
|
|
30
|
+
const lines = [];
|
|
31
|
+
let missingCount = 0;
|
|
32
|
+
let disabledCount = 0;
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const nameMatch = entry.match(/specialists:([^*]+)/);
|
|
35
|
+
if (!nameMatch)
|
|
36
|
+
continue;
|
|
37
|
+
const name = nameMatch[1];
|
|
38
|
+
let status;
|
|
39
|
+
if (existsSync(join(opts.specialistsAgentsDir, `${name}.md`))) {
|
|
40
|
+
status =
|
|
41
|
+
name === "personal-assistant" && !opts.chromeUp
|
|
42
|
+
? "degraded — chrome not running"
|
|
43
|
+
: "ok";
|
|
44
|
+
}
|
|
45
|
+
else if (disabled.has(`${name}.md`)) {
|
|
46
|
+
status = "disabled (operator)";
|
|
47
|
+
disabledCount++;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
status = "missing file";
|
|
51
|
+
missingCount++;
|
|
52
|
+
}
|
|
53
|
+
lines.push(` ${name}: ${status}`);
|
|
54
|
+
}
|
|
55
|
+
return { lines, registered: lines.length, missing: missingCount, disabled: disabledCount };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=specialist-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"specialist-registry.js","sourceRoot":"","sources":["../src/specialist-registry.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,4EAA4E;AAC5E,iEAAiE;AACjE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAU1C;;;YAGY;AACZ,SAAS,iBAAiB,CAAC,UAAkB;IAC3C,MAAM,CAAC,GAAG,OAAO,CAAC,UAAU,EAAE,sBAAsB,CAAC,CAAC;IACtD,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,GAAG,EAAE,CAAC;IACrC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC,CAA2B,CAAC;QAC9E,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,GAAG,EAAE,CAAC;QACtD,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC;IACpF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,GAAG,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,IAKnC;IACC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,kCAAkC,CAAC,IAAI,EAAE,CAAC;IAC9E,MAAM,QAAQ,GAAG,iBAAiB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACpD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,IAAI,aAAa,GAAG,CAAC,CAAC;IAEtB,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;QACrD,IAAI,CAAC,SAAS;YAAE,SAAS;QACzB,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;QAC1B,IAAI,MAAc,CAAC;QACnB,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,GAAG,IAAI,KAAK,CAAC,CAAC,EAAE,CAAC;YAC9D,MAAM;gBACJ,IAAI,KAAK,oBAAoB,IAAI,CAAC,IAAI,CAAC,QAAQ;oBAC7C,CAAC,CAAC,+BAA+B;oBACjC,CAAC,CAAC,IAAI,CAAC;QACb,CAAC;aAAM,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,IAAI,KAAK,CAAC,EAAE,CAAC;YACtC,MAAM,GAAG,qBAAqB,CAAC;YAC/B,aAAa,EAAE,CAAC;QAClB,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,cAAc,CAAC;YACxB,YAAY,EAAE,CAAC;QACjB,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,KAAK,MAAM,EAAE,CAAC,CAAC;IACrC,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC;AAC7F,CAAC"}
|
|
@@ -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:414b3a1a40f8b871635c0746cd30d4ea4203fd9baa8f4727c435c75c01f9bc2a
|
|
5
5
|
brand: sitedesk-code
|
|
6
6
|
product-name: SiteDesk
|
|
7
7
|
---
|
|
@@ -3176,6 +3176,18 @@ and removes anything named in it, so an upgrade does not silently re-deliver a
|
|
|
3176
3176
|
disabled agent. An unreadable store withholds nothing and says so: a corrupt
|
|
3177
3177
|
file is not evidence that an agent was disabled.
|
|
3178
3178
|
|
|
3179
|
+
Disabling also reconciles the admin's routing prose. `agents/admin/AGENTS.md`
|
|
3180
|
+
carries one `- **specialists:<name>**:` line per dispatchable specialist, and
|
|
3181
|
+
`agents-md-bootstrap.sh` used to only ever append, so a disabled agent stayed
|
|
3182
|
+
advertised and the admin dispatched to a file that was no longer there. The
|
|
3183
|
+
script now withholds a line for every name in the store, and both routes exec it
|
|
3184
|
+
after the move, so the prose is right immediately rather than after the next
|
|
3185
|
+
provisioning run. It emits `[agents-md] op=specialist-lines account=<id8>
|
|
3186
|
+
kept=<n> withheld=<comma-list|none|store-unreadable>`. `system-status` reads the
|
|
3187
|
+
same store, so a disabled agent reports `disabled (operator)` rather than
|
|
3188
|
+
`missing file`, and the header counts the two separately
|
|
3189
|
+
(`<n> registered, missing=<x> disabled=<y>`).
|
|
3190
|
+
|
|
3179
3191
|
**Account scope.** The route (`server/routes/admin/agents.ts`) never infers an
|
|
3180
3192
|
account from device state. The two reads resolve from the caller's admin session
|
|
3181
3193
|
(`requireAdminSession` + the shared `accountDirForSession` in
|
|
@@ -3191,8 +3203,8 @@ explicit `?plugin=` parameter, not inferred from `--` in the slug.
|
|
|
3191
3203
|
|-------|-----------|
|
|
3192
3204
|
| `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`, `riskiestTool`, `unresolved` and `disabled`. Returns `{agents, accountId, skipped, specialistsSkipped, shippedSkipped}`. 401 when the session maps to no account. |
|
|
3193
3205
|
| `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, riskiestTool, 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. |
|
|
3206
|
+
| `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. Returns `routingReconciled`, which is `false` when the AGENTS.md reconcile threw: the move and the store write already happened, so the request is not failed, and the caller is told rather than left assuming the prose is correct. Same `accountId` contract as delete. |
|
|
3207
|
+
| `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. Carries the same `routingReconciled` field; the restored file is what puts the routing line back. |
|
|
3196
3208
|
| `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. |
|
|
3197
3209
|
| `POST /api/admin/agents/:slug/project?accountId=` | Re-projects the named account's on-disk agent into the graph. Same `accountId` contract as delete. |
|
|
3198
3210
|
|
|
@@ -9,6 +9,18 @@ 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-27 (0.1.515)
|
|
13
|
+
|
|
14
|
+
- Disabling an agent now takes effect straight away: it is dropped from the routing table, reported as disabled rather than missing, and the agents file is reconciled on both disable and enable.
|
|
15
|
+
- Scheduled WhatsApp messages and live incoming messages now work out which account they belong to through the same code, so a scheduled send cannot land on a different account than a live reply would.
|
|
16
|
+
- When an account folder cannot be read at startup, the log now reports the actual error and names the files that failed to parse, instead of reporting that there are no accounts.
|
|
17
|
+
|
|
18
|
+
## 2026-07-27 (0.1.514)
|
|
19
|
+
|
|
20
|
+
- Three delete and danger buttons had no visible hover state, because their hover colour resolved to white. They now have a real hover step on the colour ramp.
|
|
21
|
+
- The assistant can no longer write to your account's own configuration and machinery files, which are now fenced off at the write guard.
|
|
22
|
+
- WhatsApp escalation checks now compare against the account a message was actually routed to, rather than a folder name, so an escalation is judged against the right account.
|
|
23
|
+
|
|
12
24
|
## 2026-07-27 (0.1.513)
|
|
13
25
|
|
|
14
26
|
- Fixed sign-in for sub-accounts: the browser session bridge now checks that you belong to the account you asked for, instead of only ever accepting the first one it found.
|
|
@@ -78,7 +78,7 @@ export function resolveExposedDirs(schemaText, exposeFolders = []) {
|
|
|
78
78
|
collisions: /** @type {string[]} */ ([]),
|
|
79
79
|
exposed: /** @type {string[]} */ ([]),
|
|
80
80
|
}
|
|
81
|
-
// Fail CLOSED. fs-schema-guard.sh:
|
|
81
|
+
// Fail CLOSED. fs-schema-guard.sh:83-86 fails open on a missing schema so an
|
|
82
82
|
// unseeded legacy account is not write-blocked; a read surface facing a
|
|
83
83
|
// client must do the opposite. Do not "fix" this to match the guard.
|
|
84
84
|
if (typeof schemaText !== 'string' || schemaText.length === 0) return empty
|
|
@@ -223,6 +223,18 @@ and removes anything named in it, so an upgrade does not silently re-deliver a
|
|
|
223
223
|
disabled agent. An unreadable store withholds nothing and says so: a corrupt
|
|
224
224
|
file is not evidence that an agent was disabled.
|
|
225
225
|
|
|
226
|
+
Disabling also reconciles the admin's routing prose. `agents/admin/AGENTS.md`
|
|
227
|
+
carries one `- **specialists:<name>**:` line per dispatchable specialist, and
|
|
228
|
+
`agents-md-bootstrap.sh` used to only ever append, so a disabled agent stayed
|
|
229
|
+
advertised and the admin dispatched to a file that was no longer there. The
|
|
230
|
+
script now withholds a line for every name in the store, and both routes exec it
|
|
231
|
+
after the move, so the prose is right immediately rather than after the next
|
|
232
|
+
provisioning run. It emits `[agents-md] op=specialist-lines account=<id8>
|
|
233
|
+
kept=<n> withheld=<comma-list|none|store-unreadable>`. `system-status` reads the
|
|
234
|
+
same store, so a disabled agent reports `disabled (operator)` rather than
|
|
235
|
+
`missing file`, and the header counts the two separately
|
|
236
|
+
(`<n> registered, missing=<x> disabled=<y>`).
|
|
237
|
+
|
|
226
238
|
**Account scope.** The route (`server/routes/admin/agents.ts`) never infers an
|
|
227
239
|
account from device state. The two reads resolve from the caller's admin session
|
|
228
240
|
(`requireAdminSession` + the shared `accountDirForSession` in
|
|
@@ -238,8 +250,8 @@ explicit `?plugin=` parameter, not inferred from `--` in the slug.
|
|
|
238
250
|
|-------|-----------|
|
|
239
251
|
| `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`, `riskiestTool`, `unresolved` and `disabled`. Returns `{agents, accountId, skipped, specialistsSkipped, shippedSkipped}`. 401 when the session maps to no account. |
|
|
240
252
|
| `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, riskiestTool, 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. |
|
|
253
|
+
| `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. Returns `routingReconciled`, which is `false` when the AGENTS.md reconcile threw: the move and the store write already happened, so the request is not failed, and the caller is told rather than left assuming the prose is correct. Same `accountId` contract as delete. |
|
|
254
|
+
| `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. Carries the same `routingReconciled` field; the restored file is what puts the routing line back. |
|
|
243
255
|
| `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. |
|
|
244
256
|
| `POST /api/admin/agents/:slug/project?accountId=` | Re-projects the named account's on-disk agent into the graph. Same `accountId` contract as delete. |
|
|
245
257
|
|
|
@@ -114,7 +114,7 @@ The platform enforces this at multiple levels:
|
|
|
114
114
|
|------|-----------|------|
|
|
115
115
|
| **Self phone** | The WhatsApp account's own phone number (the phone that scanned the QR code). Self-chat only. | `phonesMatch(senderPhone, selfPhone)` in `access-control.ts` |
|
|
116
116
|
| **Admin phones** | Phones listed in `adminPhones` in `account.json`. The user's personal phone is auto-registered on QR link; additional phones are added via `whatsapp-config action: "add-admin-phone"`. Their admin session is scoped to the **house** account. `adminPhones` is authoritative only on the account that owns the WhatsApp paired socket (the house): `add-admin-phone` is refused on any other account, and a list left on a non-socket (sub-)account is inert and purged at boot with `[admin-identity] op=purge-nonsocket-adminphone`. To answer "is this phone a manager?" read the house account's `adminPhones` + `accountManagers` only — never a sub-account's list. | `isAdminPhone()` in `access-control.ts` — iterates `adminPhones` |
|
|
117
|
-
| **Account managers** | Phones in the `accountManagers` map (`account.json`, phone → sub-account binding), added via `whatsapp-config action: "add-account-manager"`. Scoped to the **bound sub-account**, not the house. Disjoint from `adminPhones` — a phone is a house admin or a sub-account manager, never both. Each binding has a **mode**: `active` (default) is a full admin session that can reply and manage the sub-account; `passive` is a constrained requirement-intake spawn (only `work-create`, no reply). | `managedAccountFor()` in `access-control.ts` resolves the sub-account + mode **once**; `checkDmAccess` returns `effectiveAccountId` + `passive`, and `ensureChannelSession` consumes them without re-resolving |
|
|
117
|
+
| **Account managers** | Phones in the `accountManagers` map (`account.json`, phone → sub-account binding), added via `whatsapp-config action: "add-account-manager"`. Scoped to the **bound sub-account**, not the house. Disjoint from `adminPhones` — a phone is a house admin or a sub-account manager, never both. Each binding has a **mode**: `active` (default) is a full admin session that can reply and manage the sub-account; `passive` is a constrained requirement-intake spawn (only `work-create`, no reply). Like `adminPhones`, the map is authoritative only on the account that owns the paired socket: `add-account-manager` is refused on any other account with `reason: "not-socket-account"` (a binding that also names the house reports `house-account-binding` instead — the value check runs first), and a map left on a non-socket account is inert and purged at boot with `[admin-identity] op=purge-nonsocket-accountmanager`. That boot purge stands down, logging `resolver disagreement`, when the account the manager loads its config from is not the `role:"house"` account, because there the "non-socket" map is the live one. | `managedAccountFor()` in `access-control.ts` resolves the sub-account + mode **once**; `checkDmAccess` returns `effectiveAccountId` + `passive`, and `ensureChannelSession` consumes them without re-resolving |
|
|
118
118
|
| **Public/unknown** | Any phone that is not self, not in `adminPhones`, and not in `accountManagers`. Subject to DM policy gating. | Everything else in `checkDmAccess()` |
|
|
119
119
|
|
|
120
120
|
**Critical distinction:** The *self phone* is the paired device's number (the WhatsApp account SiteDesk controls). The *admin phone* is the user's personal phone (the phone they message *from*). These are typically different numbers. If the user's personal phone is not in `adminPhones` or `accountManagers`, their messages route as public — not admin.
|
|
@@ -137,9 +137,9 @@ A passive-bound phone that DMs a requirement gets exactly one `:Task` filed unde
|
|
|
137
137
|
|
|
138
138
|
**Recall surfaces the attachment, not just the text.** A recalled message that carried media now includes an `attachment` reference on its read-tool row (`whatsapp-messages` and both modes of `whatsapp-conversation-graph-state`): `attachmentId`, `filename`, `mimeType`, a `sizeBytes` (bytes, as stored — the line renders it as KB/MB), an absolute on-disk `path` to the stored bytes, and a `readableTextPath` when an extracted-text sibling exists. The reference resolves against the same store account the route read the thread from (the house account when projecting a sub-account), so the path always points at the house-stored bytes, never the sub-account's own empty uploads dir. The read-tool output renders it as an indented `[attachment: <name> (<mime>, <size>); Read: <path>]` line, and the agent reads that path directly (an image renders when Read). Inbound WhatsApp media runs best-effort text extraction (the earlier `skipDocToText` opt-out was retired): a convertible container (xlsx, docx, ods, and the rest of `CONVERTIBLE_DOC_MIME`) gets an `.extracted.txt` sibling, so `readableTextPath` is populated for those; an image or other non-convertible type has no sibling and surfaces the bytes `path` alone. Observable on each read route's result line as `withAttachments=<n>`; a record that names an attachment whose bytes are missing under the store account logs `[whatsapp-read-tool] op=attachment-miss attachmentId=<8> storeAccount=<id>`, which separates a genuine house-store gap from a sub-account mis-resolution.
|
|
139
139
|
|
|
140
|
-
**Single source of truth + fail-closed.** The effective account is resolved **exactly once**, in the gate: `checkDmAccess` returns `effectiveAccountId` (the house account for owner/`adminPhones`/public, the bound sub-account for a manager), and that value threads through the inbound payload → gateway → `ensureChannelSession`, which spawns into it **without re-reading** the `accountManagers` map. There is no second resolution and no `?? accountId` house fallback: if a manager's bound sub-account is not a valid account, the inbound is **rejected** (no session spawned, no reply), never routed to the house. This closes the escalation where a divergence between two independent map reads handed a scoped manager a house-owner admin session. Observable signals: `op=account-manager-route … effectiveAccount=… source=gate` on a routed manager inbound; `op=account-manager-reject … reason=unresolved-effective-account` on the fail-closed drop; a standing `op=escalation-tripwire` belt that can only fire if a future change reintroduces the divergence.
|
|
140
|
+
**Single source of truth + fail-closed.** The effective account is resolved **exactly once**, in the gate: `checkDmAccess` returns `effectiveAccountId` (the house account for owner/`adminPhones`/public, the bound sub-account for a manager), and that value threads through the inbound payload → gateway → `ensureChannelSession`, which spawns into it **without re-reading** the `accountManagers` map. There is no second resolution and no `?? accountId` house fallback: if a manager's bound sub-account is not a valid account, the inbound is **rejected** (no session spawned, no reply), never routed to the house. This closes the escalation where a divergence between two independent map reads handed a scoped manager a house-owner admin session. Observable signals: `op=account-manager-route … effectiveAccount=… source=gate` on a routed manager inbound; `op=account-manager-reject … reason=unresolved-effective-account` on the fail-closed drop; a standing `op=escalation-tripwire` belt that can only fire if a future change reintroduces the divergence. The belt compares the manager's effective account against the **house** UUID, not against the account inbound persists to — on an install that sets `channelRoutingAccountId` those differ, and a manager bound to the routing target is a legitimate binding that must admit. The binding write path now refuses a house-account binding at write time, so no new one can be created; a binding written before that refusal existed is still on disk and is what the belt catches.
|
|
141
141
|
|
|
142
|
-
**The scheduler path fails closed too.** A scheduled dispatch (`POST /api/channel/schedule-inject`) reaches the same spawn machine without going through the gate, resolving its own effective account via `effectiveAccountFor`. That resolver carries the same single-source + fail-closed shape: a non-manager destination scopes to the
|
|
142
|
+
**The scheduler path fails closed too, and resolves the same account inbound does.** A scheduled dispatch (`POST /api/channel/schedule-inject`) reaches the same spawn machine without going through the gate, resolving its own effective account via `effectiveAccountFor`. That resolver carries the same single-source + fail-closed shape: a non-manager destination scopes to the **channel account**, a valid manager scopes to the bound sub-account, and a manager whose bound sub-account is not a valid account resolves to nothing — the route **rejects** (`op=schedule-account-manager-reject … reason=unresolved-effective-account`, HTTP 403, no spawn, no reply), never routing to the house. There is no `?? accountId` fallback on this path either. The channel account is the `channelRoutingAccountId` target when the house names a valid one and the house itself otherwise, which is exactly what a live inbound scopes to — the session id hashes the account, so a scheduled firing to a phone and that phone's own reply must resolve the same one or they become two sessions for one person. The scheduled resolver is not handed the house at all, so it cannot return it as a scope. One window is still open: the live path resolves the channel account once when the connection starts, so a repoint made while the connection is up is seen by the scheduled path first. Telegram scheduled dispatch is house-only by construction (no account-manager routing), so there is nothing to fail closed there, and Telegram scopes to the house rather than the channel account on both of its paths.
|
|
143
143
|
|
|
144
144
|
**Schedule-time destination validation is a distinct, earlier layer.** The `effectiveAccountFor` resolution above runs at *fire* time to scope the spawn. Separately, when a schedule is *created or updated* (and re-checked every heartbeat by the standing audit), the scheduler validates the WhatsApp `agentDispatch` destination against the same house authority. It reads the authoritative `adminPhones` + `accountManagers` from the socket-owning **house** account — never the event's own `account.json`, whose manager map is inert — via the one shared `validateAgentDestination`. The destination's registered account (the house for an admin, the bound sub-account for a manager) must equal the account that created the schedule; the house/owner may target any registered admin or manager, a sub-account is locked to its own manager(s). A refusal names the true cause: `not-registered` (in no house list) or `cross-account destinationAccount=… scheduleAccount=…` (registered to a different account). Create-time and fire-time emit the identical reason codes, so a schedule that passes creation but later drifts is explained rather than silently dropped. This closes the bug where the validator read a sub-account's own (empty) manager map and refused a genuinely registered manager whose binding lives on the house.
|
|
145
145
|
|
|
@@ -102,6 +102,53 @@ assert_eq "$AGENTS_LINES" "$CORE_FILES" "doctrine:agents-md-line-count-equals-co
|
|
|
102
102
|
assert_not_contains "$TMP/agents/admin/AGENTS.md" "premium--special" "doctrine:premium-plugin-agent-skipped"
|
|
103
103
|
rm -rf "$TMP"
|
|
104
104
|
|
|
105
|
+
# Case 5 (Task 2016) — a name in the disabled store gets no routing line.
|
|
106
|
+
TMP=$(make_account)
|
|
107
|
+
bash "$BOOTSTRAP" "$TMP" >/dev/null
|
|
108
|
+
printf '{"disabled":["beta-specialist.md"]}\n' > "$TMP/agents-disabled.json"
|
|
109
|
+
OUT=$(bash "$BOOTSTRAP" "$TMP")
|
|
110
|
+
assert_not_contains "$TMP/agents/admin/AGENTS.md" "- **specialists:beta-specialist**: " "withhold:beta-line-gone"
|
|
111
|
+
assert_contains "$TMP/agents/admin/AGENTS.md" "- **specialists:alpha-specialist**: " "withhold:alpha-kept"
|
|
112
|
+
assert_contains "$TMP/agents/admin/AGENTS.md" "- **specialists:gamma-specialist**: " "withhold:gamma-kept"
|
|
113
|
+
assert_matches "$OUT" "op=specialist-lines account=[^ ]+ kept=2 withheld=beta-specialist" "withhold:log-line"
|
|
114
|
+
|
|
115
|
+
# Case 6 (Task 2016) — clearing the store restores the line on the next run.
|
|
116
|
+
printf '{"disabled":[]}\n' > "$TMP/agents-disabled.json"
|
|
117
|
+
OUT=$(bash "$BOOTSTRAP" "$TMP")
|
|
118
|
+
assert_contains "$TMP/agents/admin/AGENTS.md" "- **specialists:beta-specialist**: " "restore:beta-back"
|
|
119
|
+
assert_matches "$OUT" "op=specialist-lines account=[^ ]+ kept=3 withheld=none" "restore:log-line"
|
|
120
|
+
rm -rf "$TMP"
|
|
121
|
+
|
|
122
|
+
# Case 7 (Task 2016) — a malformed store removes nothing and says so. A corrupt
|
|
123
|
+
# file is not evidence that an agent was disabled.
|
|
124
|
+
TMP=$(make_account)
|
|
125
|
+
bash "$BOOTSTRAP" "$TMP" >/dev/null
|
|
126
|
+
printf '{ not json' > "$TMP/agents-disabled.json"
|
|
127
|
+
OUT=$(bash "$BOOTSTRAP" "$TMP")
|
|
128
|
+
assert_contains "$TMP/agents/admin/AGENTS.md" "- **specialists:alpha-specialist**: " "badstore:alpha-kept"
|
|
129
|
+
assert_contains "$TMP/agents/admin/AGENTS.md" "- **specialists:beta-specialist**: " "badstore:beta-kept"
|
|
130
|
+
assert_matches "$OUT" "op=specialist-lines account=[^ ]+ kept=3 withheld=store-unreadable" "badstore:log-line"
|
|
131
|
+
rm -rf "$TMP"
|
|
132
|
+
|
|
133
|
+
# Case 8 (Task 2016) — a store entry that breaks the `[a-z][a-z0-9-]*` name
|
|
134
|
+
# convention is ignored, never interpolated into the matcher. `.*` would
|
|
135
|
+
# otherwise match every specialist line and delete the whole routing table.
|
|
136
|
+
TMP=$(make_account)
|
|
137
|
+
bash "$BOOTSTRAP" "$TMP" >/dev/null
|
|
138
|
+
printf '{"disabled":[".*.md"]}\n' > "$TMP/agents-disabled.json"
|
|
139
|
+
OUT=$(bash "$BOOTSTRAP" "$TMP")
|
|
140
|
+
assert_contains "$TMP/agents/admin/AGENTS.md" "- **specialists:alpha-specialist**: " "badname:alpha-kept"
|
|
141
|
+
assert_contains "$TMP/agents/admin/AGENTS.md" "- **specialists:beta-specialist**: " "badname:beta-kept"
|
|
142
|
+
assert_contains "$TMP/agents/admin/AGENTS.md" "- **specialists:gamma-specialist**: " "badname:gamma-kept"
|
|
143
|
+
assert_matches "$OUT" "op=specialist-lines account=[^ ]+ kept=3 withheld=none" "badname:log-line"
|
|
144
|
+
rm -rf "$TMP"
|
|
145
|
+
|
|
146
|
+
# Case 9 (Task 2016) — no store on disk is the cold-install path.
|
|
147
|
+
TMP=$(make_account)
|
|
148
|
+
OUT=$(bash "$BOOTSTRAP" "$TMP")
|
|
149
|
+
assert_matches "$OUT" "op=specialist-lines account=[^ ]+ kept=3 withheld=none" "nostore:log-line"
|
|
150
|
+
rm -rf "$TMP"
|
|
151
|
+
|
|
105
152
|
echo
|
|
106
153
|
echo "PASS: $PASS, FAIL: $FAIL"
|
|
107
154
|
if [ "$FAIL" -gt 0 ]; then
|
|
@@ -10,9 +10,19 @@
|
|
|
10
10
|
# matches `^- **specialists:<name>**:` are preserved verbatim (operator-tuned
|
|
11
11
|
# lines never get clobbered); missing entries are appended.
|
|
12
12
|
#
|
|
13
|
-
#
|
|
13
|
+
# A name in <account_dir>/agents-disabled.json (Task 2016) gets no routing line:
|
|
14
|
+
# an existing one is removed and the walk below skips it. The store is the
|
|
15
|
+
# operator's intent, so this holds even in the drift state where the file is
|
|
16
|
+
# still live — the standing agent-parity check is what reports that drift. An
|
|
17
|
+
# unreadable store withholds nothing, matching provision-account-dir.sh: a
|
|
18
|
+
# corrupt file is not evidence that an agent was disabled. A store entry that
|
|
19
|
+
# breaks the name convention below is ignored rather than interpolated: `.*`
|
|
20
|
+
# would otherwise match every routing line and take the whole table with it.
|
|
21
|
+
#
|
|
22
|
+
# Emits two stdout observability lines, two-space indented to match the
|
|
14
23
|
# surrounding setup-account.sh log style:
|
|
15
24
|
# " [admin-bootstrap] AGENTS.md specialists=<n> appended=<m> existing=<k>"
|
|
25
|
+
# " [agents-md] op=specialist-lines account=<id8> kept=<n> withheld=<comma-list|none|store-unreadable>"
|
|
16
26
|
#
|
|
17
27
|
# Names must follow the `[a-z][a-z0-9-]*` convention so direct interpolation
|
|
18
28
|
# in the regex matcher is safe.
|
|
@@ -27,6 +37,48 @@ if [ ! -f "$AGENTS_MD" ]; then
|
|
|
27
37
|
printf '# Installed Roles\n\nDispatch via the Agent tool with `subagent_type: "specialists:{name}"`.\n\n' > "$AGENTS_MD"
|
|
28
38
|
fi
|
|
29
39
|
|
|
40
|
+
# --- Disabled-agent reconcile (Task 2016) ------------------------------------
|
|
41
|
+
_store="$ACCOUNT_DIR/agents-disabled.json"
|
|
42
|
+
_withheld_names=""
|
|
43
|
+
_withheld_field="none"
|
|
44
|
+
if [ -f "$_store" ]; then
|
|
45
|
+
if _withheld_names=$(python3 -c '
|
|
46
|
+
import json, re, sys
|
|
47
|
+
try:
|
|
48
|
+
parsed = json.load(open(sys.argv[1]))
|
|
49
|
+
except Exception:
|
|
50
|
+
raise SystemExit(1)
|
|
51
|
+
names = parsed.get("disabled")
|
|
52
|
+
if not isinstance(names, list):
|
|
53
|
+
raise SystemExit(1)
|
|
54
|
+
for n in sorted(names):
|
|
55
|
+
if isinstance(n, str) and n.endswith(".md") and re.fullmatch(r"[a-z][a-z0-9-]*", n[:-3]):
|
|
56
|
+
print(n[:-3])
|
|
57
|
+
' "$_store" 2>/dev/null); then
|
|
58
|
+
if [ -n "$_withheld_names" ]; then
|
|
59
|
+
_withheld_field=$(echo "$_withheld_names" | paste -sd, -)
|
|
60
|
+
fi
|
|
61
|
+
else
|
|
62
|
+
_withheld_names=""
|
|
63
|
+
_withheld_field="store-unreadable"
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
_is_withheld() {
|
|
68
|
+
[ -n "$_withheld_names" ] || return 1
|
|
69
|
+
printf '%s\n' "$_withheld_names" | grep -qxF -- "$1"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if [ -n "$_withheld_names" ]; then
|
|
73
|
+
while IFS= read -r _w; do
|
|
74
|
+
[ -n "$_w" ] || continue
|
|
75
|
+
if grep -qE "^- \*\*specialists:${_w}\*\*:" "$AGENTS_MD"; then
|
|
76
|
+
grep -vE "^- \*\*specialists:${_w}\*\*:" "$AGENTS_MD" > "$AGENTS_MD.tmp" || true
|
|
77
|
+
mv "$AGENTS_MD.tmp" "$AGENTS_MD"
|
|
78
|
+
fi
|
|
79
|
+
done <<< "$_withheld_names"
|
|
80
|
+
fi
|
|
81
|
+
|
|
30
82
|
_specialists=0
|
|
31
83
|
_appended=0
|
|
32
84
|
_existing=0
|
|
@@ -38,6 +90,7 @@ for specialist in "$ACCOUNT_DIR/specialists/agents/"*.md; do
|
|
|
38
90
|
_name=$(sed -n 's/^name: *//p' "$specialist" | head -1)
|
|
39
91
|
_desc=$(sed -n 's/^description: *"*//p' "$specialist" | sed 's/"$//' | head -1)
|
|
40
92
|
[ -z "$_name" ] && continue
|
|
93
|
+
if _is_withheld "$_name"; then continue; fi
|
|
41
94
|
_specialists=$((_specialists + 1))
|
|
42
95
|
if grep -qE "^- \*\*specialists:${_name}\*\*:" "$AGENTS_MD"; then
|
|
43
96
|
_existing=$((_existing + 1))
|
|
@@ -47,3 +100,7 @@ for specialist in "$ACCOUNT_DIR/specialists/agents/"*.md; do
|
|
|
47
100
|
_appended=$((_appended + 1))
|
|
48
101
|
done
|
|
49
102
|
echo " [admin-bootstrap] AGENTS.md specialists=$_specialists appended=$_appended existing=$_existing"
|
|
103
|
+
|
|
104
|
+
_kept=$(grep -cE '^- \*\*specialists:' "$AGENTS_MD" || true)
|
|
105
|
+
_id8=$(basename "$ACCOUNT_DIR" | cut -c1-8)
|
|
106
|
+
echo " [agents-md] op=specialist-lines account=$_id8 kept=$_kept withheld=$_withheld_field"
|
|
@@ -74,12 +74,24 @@ export type AccountDirDiagnosis = {
|
|
|
74
74
|
kind: 'resolved';
|
|
75
75
|
dir: string;
|
|
76
76
|
}
|
|
77
|
-
/** No directory under `accountsRoot` holds an `account.json
|
|
78
|
-
*
|
|
77
|
+
/** No directory under `accountsRoot` holds an `account.json`, and the root is
|
|
78
|
+
* either absent (`ENOENT`) or present and holding none. A genuinely empty
|
|
79
|
+
* registry, which is the installer's job to fill. `ENOENT` also covers a
|
|
80
|
+
* symlink at `data/accounts` pointing nowhere, which reads as absent. */
|
|
79
81
|
| {
|
|
80
82
|
kind: 'no-accounts';
|
|
81
83
|
accountsRoot: string;
|
|
82
84
|
}
|
|
85
|
+
/** `accountsRoot` could not be enumerated: `readdirSync` failed with a code
|
|
86
|
+
* other than `ENOENT`. Task 2031 — the registry is unknown, not empty, so
|
|
87
|
+
* its count is not zero and the installer remedy does not apply. An ownership
|
|
88
|
+
* or permission mismatch on `data/accounts` is the routine outcome of moving
|
|
89
|
+
* an install between machines. */
|
|
90
|
+
| {
|
|
91
|
+
kind: 'accounts-root-unreadable';
|
|
92
|
+
accountsRoot: string;
|
|
93
|
+
errno: string;
|
|
94
|
+
}
|
|
83
95
|
/** No account carries `role:"house"` and more than one candidate exists, so
|
|
84
96
|
* the pre-migration single-account tolerance cannot apply. */
|
|
85
97
|
| {
|
|
@@ -97,8 +109,9 @@ export type AccountDirDiagnosis = {
|
|
|
97
109
|
* managed-service model: the single `role:"house"` account, or the sole
|
|
98
110
|
* account in the pre-migration window (before `setup-account.sh` stamps the
|
|
99
111
|
* role). Mirrors the installer's `resolveInstallAccountId` so the runtime reads
|
|
100
|
-
* the same account the installer wrote. Reports no accounts
|
|
101
|
-
* drift conditions (zero-house with >1
|
|
112
|
+
* the same account the installer wrote. Reports no accounts, an unreadable
|
|
113
|
+
* accounts root, and the two genuine drift conditions (zero-house with >1
|
|
114
|
+
* candidate, multiple houses) as distinct
|
|
102
115
|
* kinds; the caller surfaces `account-dir-unresolved` naming the kind and
|
|
103
116
|
* refuses to start, and the boot census (`[account-registry] op=census`) makes
|
|
104
117
|
* the drift visible in journalctl.
|
|
@@ -114,17 +127,18 @@ export declare function diagnoseAccountDir(platformRoot: string): AccountDirDiag
|
|
|
114
127
|
export declare function resolveAccountDir(platformRoot: string): string | null;
|
|
115
128
|
/** The `account-dir-unresolved` boot-refusal message for a non-resolving
|
|
116
129
|
* diagnosis. Every branch keeps the `account-dir-unresolved` token so existing
|
|
117
|
-
* greps still hit, and carries `reason=<kind>` so the
|
|
130
|
+
* greps still hit, and carries `reason=<kind>` so the four failures are
|
|
118
131
|
* distinguishable from the log line alone. Only `no-accounts` names the
|
|
119
132
|
* installer: on the two drift branches re-running it adds another account and
|
|
120
|
-
* deepens the fault
|
|
121
|
-
*
|
|
133
|
+
* deepens the fault, and on an unreadable root it cannot repair the read.
|
|
134
|
+
* Throws on a resolved diagnosis — there is no failure to describe, and
|
|
135
|
+
* silently returning prose would let a caller print it. */
|
|
122
136
|
export declare function formatAccountDirUnresolved(d: AccountDirDiagnosis): string;
|
|
123
|
-
/** Registry census — house / client / total counts of the
|
|
124
|
-
* `accountsRoot`. A dir counts when it holds
|
|
125
|
-
* `clients` counts `role:"client"`, and an account with any other or
|
|
126
|
-
* role (the pre-migration unlabelled window) is neither house nor
|
|
127
|
-
* still counted in `total`. Reads roles fresh — house designation is
|
|
137
|
+
/** Registry census — house / client / total counts of the accounts under
|
|
138
|
+
* `accountsRoot`. A dir counts when it holds an `account.json`, parseable or
|
|
139
|
+
* not; `clients` counts `role:"client"`, and an account with any other or
|
|
140
|
+
* absent role (the pre-migration unlabelled window) is neither house nor
|
|
141
|
+
* client but still counted in `total`. Reads roles fresh — house designation is
|
|
128
142
|
* runtime-mutable via the lifecycle tools. Mirrors `resolveAccountDir`'s inline
|
|
129
143
|
* scan (the manager has no rootDir-clean import path for the account-enumeration
|
|
130
144
|
* lib). A missing accounts root reads as an empty registry, not a throw. */
|
|
@@ -132,12 +146,34 @@ export interface RegistryCensus {
|
|
|
132
146
|
houses: number;
|
|
133
147
|
clients: number;
|
|
134
148
|
total: number;
|
|
149
|
+
/** Task 2031 — the `account.json` files whose role could not be read, each
|
|
150
|
+
* with the reason. Counted in `total`, the same way a role-less `{}` already
|
|
151
|
+
* is, so this census and `diagnoseAccountDir` agree on how many accounts
|
|
152
|
+
* exist. `detail` carries the actual error rather than assuming one, so the
|
|
153
|
+
* log never claims a file's JSON is malformed when the file could not be
|
|
154
|
+
* opened at all. */
|
|
155
|
+
unreadable: Array<{
|
|
156
|
+
path: string;
|
|
157
|
+
detail: string;
|
|
158
|
+
}>;
|
|
159
|
+
/** Task 2031 — the errno when `accountsRoot` itself could not be enumerated,
|
|
160
|
+
* else null. `ENOENT` is not one: an absent root is an empty registry, so
|
|
161
|
+
* its zeros are facts. For any other code the counts are all zero and are
|
|
162
|
+
* NOT facts, and `formatRegistryCensus` renders the failure in their place. */
|
|
163
|
+
rootErrno: string | null;
|
|
135
164
|
}
|
|
136
165
|
export declare function censusAccountsRoot(accountsRoot: string): RegistryCensus;
|
|
137
166
|
/** The standing census log line body. `houses != 1` is the drift signature the
|
|
138
167
|
* boot emitter checks. Pure formatter so the emitter and its test share one
|
|
139
168
|
* wording. */
|
|
140
169
|
export declare function formatRegistryCensus(c: RegistryCensus): string;
|
|
170
|
+
/** The `op=census-drift` FATAL body, or null when there is no drift to report.
|
|
171
|
+
* Pure so the boot emitter and its test share one rule. `houses` is a fact only
|
|
172
|
+
* when the root was readable: on a read failure every count is zero, and
|
|
173
|
+
* FATAL-logging "exactly one house is required" against those zeros would
|
|
174
|
+
* assert a house count nobody measured (Task 2031). That case is already
|
|
175
|
+
* reported by `formatRegistryCensus` on the `op=census` line. */
|
|
176
|
+
export declare function formatCensusDrift(c: RegistryCensus, accountsRoot: string): string | null;
|
|
141
177
|
/** True iff `accountId` is the install's house account, by the same resolution
|
|
142
178
|
* the boot path uses (single role:"house", or the sole pre-migration account).
|
|
143
179
|
* Returns false on drift (zero-house-with->1-candidate, or multiple houses),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,yBAAyB,CAAA;AAGhC,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD;;4DAEwD;IACxD,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;CACjC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB;;;;0BAIsB;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB;;4EAEwE;IACxE,aAAa,EAAE,MAAM,CAAA;IACrB;;;gEAG4D;IAC5D,eAAe,EAAE,MAAM,CAAA;IACvB;;2EAEuE;IACvE,WAAW,EAAE,MAAM,CAAA;IACnB;;0EAEsE;IACtE,iBAAiB,EAAE,MAAM,CAAA;IACzB,IAAI,EAAE,cAAc,CAAA;IACpB;;;;;;;8BAO0B;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB;;;qDAGiD;IACjD,iBAAiB,EAAE,iBAAiB,CAAA;IACpC;;gFAE4E;IAC5E,gBAAgB,EAAE,MAAM,CAAA;IACxB;;;6CAGyC;IACzC,SAAS,EAAE,MAAM,CAAA;CAClB;AAyJD;;;;;;;iCAOiC;AACjC,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AACnC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|