@rubytech/create-maxy-code 0.1.74 → 0.1.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/mcp/dist/index.js +111 -7
  3. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  4. package/payload/platform/plugins/contacts/mcp/dist/index.js +36 -5
  5. package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
  6. package/payload/platform/plugins/email/mcp/dist/index.js +34 -3
  7. package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
  8. package/payload/platform/plugins/memory/mcp/dist/index.js +78 -3
  9. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  10. package/payload/platform/plugins/outlook/mcp/dist/index.js +37 -5
  11. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  12. package/payload/platform/plugins/replicate/mcp/dist/index.js +27 -6
  13. package/payload/platform/plugins/replicate/mcp/dist/index.js.map +1 -1
  14. package/payload/platform/plugins/scheduling/mcp/dist/index.js +32 -3
  15. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  16. package/payload/platform/plugins/tasks/mcp/dist/index.js +44 -3
  17. package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
  18. package/payload/platform/plugins/waitlist/mcp/dist/index.js +30 -3
  19. package/payload/platform/plugins/waitlist/mcp/dist/index.js.map +1 -1
  20. package/payload/platform/plugins/workflows/mcp/dist/index.js +32 -3
  21. package/payload/platform/plugins/workflows/mcp/dist/index.js.map +1 -1
  22. package/payload/platform/templates/agents/admin/IDENTITY.md +2 -2
  23. package/payload/platform/templates/agents/admin/SOUL.md +0 -38
  24. package/payload/server/public/assets/{admin-DcqVybWZ.js → admin-Bn3SKHdv.js} +1 -1
  25. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-C6vU0NST.js → architectureDiagram-Q4EWVU46-VMeuB5OP.js} +1 -1
  26. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-CVaR8-w2.js → blockDiagram-DXYQGD6D-B7mq0Me0.js} +1 -1
  27. package/payload/server/public/assets/{brand-1AQwHifp.css → brand-CZQNuuLo.css} +1 -1
  28. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-DnFHlz0P.js → c4Diagram-AHTNJAMY-Btu6ejD1.js} +1 -1
  29. package/payload/server/public/assets/channel-C_bSLH4h.js +1 -0
  30. package/payload/server/public/assets/{chunk-336JU56O-B6a2-iht.js → chunk-336JU56O-DAL_d_Xa.js} +2 -2
  31. package/payload/server/public/assets/{chunk-426QAEUC-DxIW6EIp.js → chunk-426QAEUC-DPD9ot_N.js} +1 -1
  32. package/payload/server/public/assets/{chunk-4TB4RGXK-Bd_K_Vm8.js → chunk-4TB4RGXK-BexM9T-f.js} +1 -1
  33. package/payload/server/public/assets/{chunk-5FUZZQ4R-BrcNLOWO.js → chunk-5FUZZQ4R-EwDdji3T.js} +1 -1
  34. package/payload/server/public/assets/{chunk-5PVQY5BW-VlQyk4ta.js → chunk-5PVQY5BW-BP9uoVws.js} +1 -1
  35. package/payload/server/public/assets/{chunk-EDXVE4YY-BR-zR5uE.js → chunk-EDXVE4YY-DSFmUMEW.js} +1 -1
  36. package/payload/server/public/assets/{chunk-ENJZ2VHE-SZeptTG4.js → chunk-ENJZ2VHE-IVAy2Qk9.js} +1 -1
  37. package/payload/server/public/assets/{chunk-ICPOFSXX-CtefGo4C.js → chunk-ICPOFSXX-DkUupPox.js} +1 -1
  38. package/payload/server/public/assets/{chunk-OYMX7WX6-BozNOEbo.js → chunk-OYMX7WX6-BPnzam8G.js} +1 -1
  39. package/payload/server/public/assets/{chunk-U2HBQHQK-D96MbvO_.js → chunk-U2HBQHQK-xYNchOVT.js} +1 -1
  40. package/payload/server/public/assets/{chunk-X2U36JSP-gwEYUG8y.js → chunk-X2U36JSP-C1V_V8v8.js} +1 -1
  41. package/payload/server/public/assets/{chunk-YZCP3GAM-DDIFkTtt.js → chunk-YZCP3GAM-CUibW87w.js} +1 -1
  42. package/payload/server/public/assets/{chunk-ZZ45TVLE-C75vepuo.js → chunk-ZZ45TVLE-HkKx4xBt.js} +1 -1
  43. package/payload/server/public/assets/classDiagram-6PBFFD2Q-B6oW43xg.js +1 -0
  44. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-D0237d4h.js +1 -0
  45. package/payload/server/public/assets/clone-AMRCmnen.js +1 -0
  46. package/payload/server/public/assets/{dagre-DwC2Bsrq.js → dagre-DIaJ9oMM.js} +1 -1
  47. package/payload/server/public/assets/{dagre-KV5264BT-DuukLv5C.js → dagre-KV5264BT-Oyox5ACd.js} +1 -1
  48. package/payload/server/public/assets/data-Mm3pTL1b.js +1 -0
  49. package/payload/server/public/assets/{device-url-actions-DDsULUXs.js → device-url-actions-BjDrEVoY.js} +1 -1
  50. package/payload/server/public/assets/{diagram-5BDNPKRD-DQWQd3WX.js → diagram-5BDNPKRD-DoGhkzBd.js} +1 -1
  51. package/payload/server/public/assets/{diagram-G4DWMVQ6-DbS_Ih5q.js → diagram-G4DWMVQ6-Dc3Ix-Dh.js} +1 -1
  52. package/payload/server/public/assets/{diagram-MMDJMWI5-BFDfdYgV.js → diagram-MMDJMWI5-CFM9_Ggj.js} +1 -1
  53. package/payload/server/public/assets/{diagram-TYMM5635-Dp48BsHY.js → diagram-TYMM5635-nMWgzxC3.js} +1 -1
  54. package/payload/server/public/assets/{erDiagram-SMLLAGMA-CKA6q8Cy.js → erDiagram-SMLLAGMA-DcQWiGgF.js} +1 -1
  55. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-BVva2cwM.js → flowDiagram-DWJPFMVM-CcX5waal.js} +1 -1
  56. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-BuzV7Yj-.js → ganttDiagram-T4ZO3ILL-BTQWoD8W.js} +1 -1
  57. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-B188dI5e.js → gitGraphDiagram-UUTBAWPF-BHZcRznS.js} +1 -1
  58. package/payload/server/public/assets/graph-Cd4jsDi3.js +1 -0
  59. package/payload/server/public/assets/{graph-labels-Cp9MdDRy.js → graph-labels-CqsRzbp0.js} +1 -1
  60. package/payload/server/public/assets/{graphlib-D-7uYmP9.js → graphlib-BpUb9z9y.js} +1 -1
  61. package/payload/server/public/assets/{infoDiagram-42DDH7IO-CLvYxjeZ.js → infoDiagram-42DDH7IO-CBoiO8yF.js} +1 -1
  62. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-B9F97qbc.js → ishikawaDiagram-UXIWVN3A-mTDI_0As.js} +1 -1
  63. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-CWg6xF0d.js → journeyDiagram-VCZTEJTY-CcuU_0rp.js} +1 -1
  64. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-CUM2Zro1.js → kanban-definition-6JOO6SKY-BWA7Do5Y.js} +1 -1
  65. package/payload/server/public/assets/{line-B_dtxHKd.js → line-BlGptbDR.js} +1 -1
  66. package/payload/server/public/assets/{mermaid-parser.core-q8nXkWvq.js → mermaid-parser.core-hslUiqJ-.js} +1 -1
  67. package/payload/server/public/assets/{mermaid.core-BIZvK8d3.js → mermaid.core-DWU_5Faf.js} +3 -3
  68. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-DOl7v8YY.js → mindmap-definition-QFDTVHPH-B3_sQr-2.js} +1 -1
  69. package/payload/server/public/assets/{page-HcHeXd21.js → page-eQPbgLXP.js} +1 -1
  70. package/payload/server/public/assets/{page-Ybq9fvmk.js → page-lPEIoiHs.js} +1 -1
  71. package/payload/server/public/assets/{pieDiagram-DEJITSTG-CVFDHS9S.js → pieDiagram-DEJITSTG-02u2dI6Z.js} +1 -1
  72. package/payload/server/public/assets/{public-B_FEURBo.js → public-j7U67c6E.js} +3 -3
  73. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-DIx-sbIM.js → quadrantDiagram-34T5L4WZ-BfgiMQiy.js} +1 -1
  74. package/payload/server/public/assets/{requirementDiagram-MS252O5E-C9yrlgJP.js → requirementDiagram-MS252O5E-JKZoibIp.js} +1 -1
  75. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-Dn2R2x6-.js → sankeyDiagram-XADWPNL6-N1iaY19u.js} +1 -1
  76. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-C9QMqZZw.js → sequenceDiagram-FGHM5R23-DjU3oQ0_.js} +1 -1
  77. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DPzHXWge.js → stateDiagram-FHFEXIEX-BuFcldiV.js} +1 -1
  78. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-rZLYem3J.js +1 -0
  79. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-gRq4Cvlk.js → timeline-definition-GMOUNBTQ-3vztYjFm.js} +1 -1
  80. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-CJGA-zAB.js → vennDiagram-DHZGUBPP-Zdv9WVqJ.js} +1 -1
  81. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BFS9eK0s.js → wardleyDiagram-NUSXRM2D-CkTAZI7f.js} +1 -1
  82. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-D5Kqs-jt.js → xychartDiagram-5P7HB3ND-CgMCxpms.js} +1 -1
  83. package/payload/server/public/data.html +5 -5
  84. package/payload/server/public/graph.html +5 -5
  85. package/payload/server/public/index.html +7 -7
  86. package/payload/server/public/public.html +4 -4
  87. package/payload/server/public/assets/channel-Ccfjb07z.js +0 -1
  88. package/payload/server/public/assets/classDiagram-6PBFFD2Q-Ci-ABevb.js +0 -1
  89. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BDQ3yWGx.js +0 -1
  90. package/payload/server/public/assets/clone-CuYGwTRT.js +0 -1
  91. package/payload/server/public/assets/data-DbNK3_l-.js +0 -1
  92. package/payload/server/public/assets/graph-yTqw4J01.js +0 -1
  93. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CcDXqSsh.js +0 -1
  94. /package/payload/server/public/assets/{brand-lfvUn_r6.js → brand-BnMilSyt.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.74",
3
+ "version": "0.1.75",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -25,17 +25,42 @@ const server = new McpServer({
25
25
  version: "0.1.0",
26
26
  });
27
27
  const PLATFORM_ROOT = process.env.PLATFORM_ROOT ?? resolve(import.meta.dirname, "../../../..");
28
- const ACCOUNT_ID = process.env.ACCOUNT_ID;
29
- if (!ACCOUNT_ID) {
30
- throw new Error("ACCOUNT_ID environment variable is required");
28
+ // Plugin-system boot tolerance — see Task 202.
29
+ // Module-init must never throw; plugin-system spawn enumerates tools/list
30
+ // before any per-account env is set. Per-call refusal preserves account
31
+ // isolation in metadata-only mode.
32
+ const ACCOUNT_ID = process.env.ACCOUNT_ID ?? null;
33
+ process.stderr.write(`[admin] boot accountId=${ACCOUNT_ID ?? "null"}\n`);
34
+ function refuseNoAccount(toolName) {
35
+ process.stderr.write(`[admin] tool=${toolName} refuse reason=no-account-context\n`);
36
+ return {
37
+ content: [
38
+ {
39
+ type: "text",
40
+ text: `${toolName} cannot execute: this MCP instance was launched without ACCOUNT_ID (plugin-system metadata-only mode). Per-account admin tools require ACCOUNT_ID at spawn.`,
41
+ },
42
+ ],
43
+ isError: true,
44
+ };
31
45
  }
32
46
  // Task 142 — `PLATFORM_PORT` is the internal loopback port, stamped on
33
47
  // the claude-session-manager systemd unit and forwarded to this MCP
34
48
  // process via the per-spawn `maxy-mcp-*.json` env block written by
35
49
  // `pty-spawner.writePerSpawnMcpConfig`. `MAXY_BRAND_PORT` is the brand's
36
50
  // public UI port, used only for display-surface `.local` URLs.
37
- const PLATFORM_PORT = String(requirePortEnv("PLATFORM_PORT", { tag: "admin:module-init" }));
38
- const MAXY_BRAND_PORT = String(requirePortEnv("MAXY_BRAND_PORT", { tag: "admin:module-init" }));
51
+ // Ports are best-effort at boot: plugin-system metadata-only spawn has no
52
+ // port env, so we capture the loud-fail message instead of crashing. Real
53
+ // per-account spawns supply both; any handler reaching the port-using code
54
+ // paths is gated upstream by the accountId refusal.
55
+ let PLATFORM_PORT = "";
56
+ let MAXY_BRAND_PORT = "";
57
+ try {
58
+ PLATFORM_PORT = String(requirePortEnv("PLATFORM_PORT", { tag: "admin:module-init" }));
59
+ MAXY_BRAND_PORT = String(requirePortEnv("MAXY_BRAND_PORT", { tag: "admin:module-init" }));
60
+ }
61
+ catch (err) {
62
+ process.stderr.write(`[admin] boot port-env unavailable — ${err.message}\n`);
63
+ }
39
64
  // Brand-aware config — reads configDir, productName, and commercialMode from
40
65
  // brand.json stamped at install time. commercialMode gates the
41
66
  // entitlement verifier: false (default) preserves personal-mode installs;
@@ -114,6 +139,9 @@ async function currentEntitlement() {
114
139
  const REMOTE_PASSWORD_FILE = resolve(CONFIG_DIR, ".remote-password");
115
140
  // Resolve account directory
116
141
  function getAccountDir() {
142
+ if (!ACCOUNT_ID) {
143
+ throw new Error("getAccountDir() called without ACCOUNT_ID — handler guard missing");
144
+ }
117
145
  const dir = resolve(PLATFORM_ROOT, "..", "data/accounts", ACCOUNT_ID);
118
146
  if (!existsSync(dir)) {
119
147
  throw new Error(`Account directory not found: ${dir}`);
@@ -220,6 +248,8 @@ function checkPort(port, timeoutMs = 1000) {
220
248
  });
221
249
  }
222
250
  eagerTool(server, "system-status", "Check health of all Maxy platform services: Neo4j, Ollama, Cloudflare tunnel, crontab (Maxy cron entries), VNC, Chrome (CDP), specialist agents, and deployed brand identity.", {}, async () => {
251
+ if (!ACCOUNT_ID)
252
+ return refuseNoAccount("system-status");
223
253
  const checks = {};
224
254
  try {
225
255
  // NEO4J_URI must be explicit. The outer try/catch surfaces the
@@ -407,6 +437,8 @@ eagerTool(server, "system-status", "Check health of all Maxy platform services:
407
437
  return { content: [{ type: "text", text: formatted }] };
408
438
  });
409
439
  eagerTool(server, "public-hostname", "Resolve this account's canonical public hostname. Reads cloudflared ingress + alias-domains.json — the same files the platform server trusts to route. Returns a single deterministic answer; use this immediately after publish-site to construct the full URL.", {}, async () => {
440
+ if (!ACCOUNT_ID)
441
+ return refuseNoAccount("public-hostname");
410
442
  const TAG = "[admin:public-hostname]";
411
443
  try {
412
444
  const result = resolvePublicHostname(CONFIG_DIR);
@@ -437,6 +469,8 @@ eagerTool(server, "public-hostname", "Resolve this account's canonical public ho
437
469
  }
438
470
  });
439
471
  eagerTool(server, "remote-auth-status", "Check whether the remote access password is configured. When not configured, emits a device-bound URL affordance (maxy-device-url fenced block) pointing at the password setup page — this URL opens on the device's own screen when the operator clicks it. The agent never constructs the password file path or runs shell commands — this tool is the single authority.", {}, async () => {
472
+ if (!ACCOUNT_ID)
473
+ return refuseNoAccount("remote-auth-status");
440
474
  const TAG = "[remote-auth-status]";
441
475
  // Task 142: `.local` URL is a display surface — read the brand's
442
476
  // public port. The loopback fetch reads PLATFORM_PORT (internal).
@@ -476,6 +510,8 @@ eagerTool(server, "remote-auth-status", "Check whether the remote access passwor
476
510
  }
477
511
  });
478
512
  eagerTool(server, "brand-settings", "Read the brand/styling configuration (name, tagline, colours, fonts, plugin sets). Reads from config/brand.json (stamped by the bundler at install time).", {}, async () => {
513
+ if (!ACCOUNT_ID)
514
+ return refuseNoAccount("brand-settings");
479
515
  try {
480
516
  const brandPath = resolve(PLATFORM_ROOT, "config", "brand.json");
481
517
  if (!existsSync(brandPath)) {
@@ -492,6 +528,8 @@ eagerTool(server, "brand-settings", "Read the brand/styling configuration (name,
492
528
  }
493
529
  });
494
530
  eagerTool(server, "account-manage", "Read the account configuration (tier, domains, settings).", {}, async () => {
531
+ if (!ACCOUNT_ID)
532
+ return refuseNoAccount("account-manage");
495
533
  try {
496
534
  const config = await readAccountConfig();
497
535
  return {
@@ -509,6 +547,8 @@ eagerTool(server, "account-update", "Update a user-configurable setting in accou
509
547
  field: z.enum(["outputStyle", "thinkingView", "effort", "adminModel", "publicModel", "defaultAgent"]),
510
548
  value: z.string(),
511
549
  }, async ({ field, value }) => {
550
+ if (!ACCOUNT_ID)
551
+ return refuseNoAccount("account-update");
512
552
  const VALID = {
513
553
  outputStyle: ["default", "explanatory"],
514
554
  thinkingView: ["default", "expanded", "collapsed"],
@@ -588,6 +628,8 @@ server.tool("plugin-toggle-enabled", "Enable or disable a plugin in this account
588
628
  pluginName: z.string().describe("Plugin slug (lowercase a-z0-9-)."),
589
629
  action: z.enum(["enable", "disable"]).describe("enable adds to enabledPlugins; disable removes."),
590
630
  }, async ({ pluginName, action }) => {
631
+ if (!ACCOUNT_ID)
632
+ return refuseNoAccount("plugin-toggle-enabled");
591
633
  const TAG = "[admin:plugin-toggle-enabled]";
592
634
  if (!VALID_PLUGIN_NAME.test(pluginName)) {
593
635
  return { content: [{ type: "text", text: `${TAG} Invalid plugin name "${pluginName}". Must match ${VALID_PLUGIN_NAME}.` }], isError: true };
@@ -632,6 +674,8 @@ eagerTool(server, "admin-add", "Add a new admin user to this account. Creates a
632
674
  name: z.string().describe("Display name for the new admin (stored on the AdminUser node in Neo4j)."),
633
675
  pin: z.string().optional().describe("Optional PIN (minimum 4 digits). If omitted, a unique 4-digit PIN is generated."),
634
676
  }, async ({ name, pin: rawPin }) => {
677
+ if (!ACCOUNT_ID)
678
+ return refuseNoAccount("admin-add");
635
679
  const TAG = "[admin]";
636
680
  if (!name.trim()) {
637
681
  return { content: [{ type: "text", text: `${TAG} Name is required.` }], isError: true };
@@ -818,6 +862,8 @@ eagerTool(server, "admin-add", "Add a new admin user to this account. Creates a
818
862
  eagerTool(server, "admin-remove", "Remove an admin from this account. Removes them from the account's admins list (account.json) and deletes the ADMIN_OF relationship in Neo4j. Does NOT remove the device-level user entry (they may admin other accounts). Cannot remove the last admin on the account.", {
819
863
  userId: z.string().describe("The userId of the admin to remove (use admin-list to find userIds)"),
820
864
  }, async ({ userId }) => {
865
+ if (!ACCOUNT_ID)
866
+ return refuseNoAccount("admin-remove");
821
867
  const TAG = "[admin]";
822
868
  const callerUserId = process.env.USER_ID;
823
869
  // Read account.json and validate
@@ -892,6 +938,8 @@ eagerTool(server, "admin-remove", "Remove an admin from this account. Removes th
892
938
  };
893
939
  });
894
940
  eagerTool(server, "admin-list", "List all admins for this account with their names and roles.", {}, async () => {
941
+ if (!ACCOUNT_ID)
942
+ return refuseNoAccount("admin-list");
895
943
  const TAG = "[admin]";
896
944
  let admins;
897
945
  try {
@@ -937,6 +985,8 @@ eagerTool(server, "admin-update-pin", "Update an existing admin user's PIN. Defa
937
985
  userId: z.string().optional().describe("The userId of the admin whose PIN to update. Defaults to the caller (the admin invoking this tool)."),
938
986
  newPin: z.string().describe("The new PIN. Minimum 4 digits, no upper bound."),
939
987
  }, async ({ userId: targetUserId, newPin }) => {
988
+ if (!ACCOUNT_ID)
989
+ return refuseNoAccount("admin-update-pin");
940
990
  const TAG = "[admin-update-pin]";
941
991
  const callerUserId = process.env.USER_ID;
942
992
  const userId = targetUserId ?? callerUserId;
@@ -994,6 +1044,8 @@ server.tool("agent-image", "Upload, update, or remove an agent's image. action=s
994
1044
  filePath: z.string().optional().describe("Absolute path to the image file (required for action=set)"),
995
1045
  imageShape: z.enum(["circle", "rounded"]).optional().describe("Image shape: circle for avatars, rounded for logos (required for action=set)"),
996
1046
  }, async ({ agentSlug, action, filePath, imageShape }) => {
1047
+ if (!ACCOUNT_ID)
1048
+ return refuseNoAccount("agent-image");
997
1049
  const VALID_IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"];
998
1050
  try {
999
1051
  const accountDir = getAccountDir();
@@ -1104,6 +1156,8 @@ server.tool("agent-image", "Upload, update, or remove an agent's image. action=s
1104
1156
  server.tool("agent-config-read", "Read the full config.json for a specific public agent by slug. Returns the complete configuration including slug, displayName, model, plugins, status, liveMemory, and any other fields.", {
1105
1157
  slug: z.string().describe("Agent slug (directory name under agents/)"),
1106
1158
  }, async ({ slug }) => {
1159
+ if (!ACCOUNT_ID)
1160
+ return refuseNoAccount("agent-config-read");
1107
1161
  const TAG = "[admin:agent-config-read]";
1108
1162
  // Path traversal guard
1109
1163
  if (slug.includes("/") || slug.includes("\\") || slug.includes("..")) {
@@ -1138,6 +1192,8 @@ server.tool("agent-config-read", "Read the full config.json for a specific publi
1138
1192
  }
1139
1193
  });
1140
1194
  server.tool("agent-list", "List all public (non-admin) agents with their full configuration: slug, displayName, model, plugins, status, liveMemory, and whether each is the account's default agent.", {}, async () => {
1195
+ if (!ACCOUNT_ID)
1196
+ return refuseNoAccount("agent-list");
1141
1197
  const TAG = "[admin:agent-list]";
1142
1198
  try {
1143
1199
  const accountDir = getAccountDir();
@@ -1202,6 +1258,8 @@ eagerTool(server, "logs-read", "Read recent logs. Stream logs (type=agent-stream
1202
1258
  sessionKey: z.string().optional(),
1203
1259
  conversationId: z.string().optional(),
1204
1260
  }, async ({ type, lines = 50, sessionKey, conversationId }) => {
1261
+ if (!ACCOUNT_ID)
1262
+ return refuseNoAccount("logs-read");
1205
1263
  try {
1206
1264
  const LOG_DIR = resolve(getAccountDir(), "logs");
1207
1265
  // sessionKey is the single identifier on disk.
@@ -1418,6 +1476,8 @@ eagerTool(server, "store-skill", "Save an operator-authored skill on disk as par
1418
1476
  content: z.string().describe("Reference file body content."),
1419
1477
  })).default([]).describe("Optional reference files written under skills/<skillName>/references/."),
1420
1478
  }, async ({ pluginName, skillName, description, publicEmbed, body, references }) => {
1479
+ if (!ACCOUNT_ID)
1480
+ return refuseNoAccount("store-skill");
1421
1481
  const KEBAB_RE = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
1422
1482
  const REF_RE = /^[a-z0-9-]+\.md$/;
1423
1483
  if (!KEBAB_RE.test(pluginName)) {
@@ -1531,6 +1591,8 @@ eagerTool(server, "plugin-read", "Read a plugin definition (PLUGIN.md) or one of
1531
1591
  pluginName: z.string().describe("Name of the plugin directory (e.g. 'sales', 'business-assistant')"),
1532
1592
  file: z.string().optional().describe("Specific file to read (e.g. 'references/pricing.md'). Defaults to PLUGIN.md."),
1533
1593
  }, async ({ pluginName, file }) => {
1594
+ if (!ACCOUNT_ID)
1595
+ return refuseNoAccount("plugin-read");
1534
1596
  try {
1535
1597
  const resolvedFile = file ?? "PLUGIN.md";
1536
1598
  const pluginPath = resolve(PLATFORM_ROOT, "plugins", pluginName, resolvedFile);
@@ -1614,6 +1676,8 @@ eagerTool(server, "plugin-read", "Read a plugin definition (PLUGIN.md) or one of
1614
1676
  server.tool("skill-find", "Find which plugin owns a skill by name. Walks plugins/*/skills/<skillName>/SKILL.md and returns the owning plugin (unique), every candidate (ambiguous), or not-found. For the common resolve-and-read case, prefer `skill-load skillName=<name>` — it returns the SKILL.md body in the same call.", {
1615
1677
  skillName: z.string().regex(/^[a-z0-9][a-z0-9-]*$/, "skillName must be lowercase kebab-case (a-z, 0-9, -)"),
1616
1678
  }, async ({ skillName }) => {
1679
+ if (!ACCOUNT_ID)
1680
+ return refuseNoAccount("skill-find");
1617
1681
  const start = Date.now();
1618
1682
  const result = findSkillOwners(PLATFORM_ROOT, skillName);
1619
1683
  const ms = Date.now() - start;
@@ -1657,6 +1721,8 @@ server.tool("skill-find", "Find which plugin owns a skill by name. Walks plugins
1657
1721
  eagerTool(server, "skill-load", "Load a plugin skill's SKILL.md body by skill name. One call: resolves the owning plugin and reads the body, with brand placeholders substituted. Returns the SKILL.md body when exactly one plugin owns the skill, a candidate list when multiple plugins own it (ambiguous), or an error when no plugin owns it.", {
1658
1722
  skillName: z.string().regex(/^[a-z0-9][a-z0-9-]*$/, "skillName must be lowercase kebab-case (a-z, 0-9, -)"),
1659
1723
  }, async ({ skillName }) => {
1724
+ if (!ACCOUNT_ID)
1725
+ return refuseNoAccount("skill-load");
1660
1726
  const start = Date.now();
1661
1727
  try {
1662
1728
  const outcome = await loadSkill(PLATFORM_ROOT, skillName);
@@ -1721,14 +1787,24 @@ eagerTool(server, "skill-load", "Load a plugin skill's SKILL.md body by skill na
1721
1787
  // already carry the rows; no new conversations write to it.
1722
1788
  eagerTool(server, "session-reset", "Reset the current session. Compacts conversation history to memory, clears the visible conversation, " +
1723
1789
  "and starts a fresh session with a new greeting. Call when the user asks to start a new session, " +
1724
- "clear the conversation, or start fresh.", {}, async () => ({ content: [{ type: "text", text: "reset" }] }));
1790
+ "clear the conversation, or start fresh.", {}, async () => {
1791
+ if (!ACCOUNT_ID)
1792
+ return refuseNoAccount("session-reset");
1793
+ return { content: [{ type: "text", text: "reset" }] };
1794
+ });
1725
1795
  eagerTool(server, "session-resume", "Resume a previous session. Loads the selected session's message history into the chat timeline " +
1726
1796
  "and routes new messages to that conversation. Call after the user selects a session from the " +
1727
- "session-list results. Pass the conversationId from the selected session.", { conversationId: z.string().uuid().describe("The conversationId of the session to resume") }, async () => ({ content: [{ type: "text", text: "resumed" }] }));
1797
+ "session-list results. Pass the conversationId from the selected session.", { conversationId: z.string().uuid().describe("The conversationId of the session to resume") }, async () => {
1798
+ if (!ACCOUNT_ID)
1799
+ return refuseNoAccount("session-resume");
1800
+ return { content: [{ type: "text", text: "resumed" }] };
1801
+ });
1728
1802
  server.tool("remote-auth-set-password", "Set the remote access password. Hashes with scrypt and writes to the platform's brand-specific config directory with mode 0600. " +
1729
1803
  "Validates strength (8+ chars, digit, special character, no spaces anywhere). " +
1730
1804
  "Protects the admin interface when exposed over the tunnel — has no effect on the public endpoint or the tunnel itself. " +
1731
1805
  "Set before `setup-tunnel.sh` — the script's post-restart verification curls the admin hostname and fails if remote-auth is not configured.", { password: z.string() }, async ({ password }) => {
1806
+ if (!ACCOUNT_ID)
1807
+ return refuseNoAccount("remote-auth-set-password");
1732
1808
  // Validate strength — same rules as the web server's validatePasswordStrength
1733
1809
  const checks = [
1734
1810
  { label: "at least 8 characters", met: password.length >= 8 },
@@ -1777,6 +1853,8 @@ eagerTool(server, "qr-generate", "Generate a QR code from text or a URL. Returns
1777
1853
  .describe("File path to save the PNG. If omitted, returns a data URI."),
1778
1854
  width: z.number().optional().describe("Width in pixels (default 300)"),
1779
1855
  }, async ({ data, outputPath, width }) => {
1856
+ if (!ACCOUNT_ID)
1857
+ return refuseNoAccount("qr-generate");
1780
1858
  try {
1781
1859
  const options = { width: width ?? 300, margin: 2 };
1782
1860
  if (outputPath) {
@@ -1831,6 +1909,8 @@ eagerTool(server, "wifi", "Manage WiFi connections on this device. Actions: " +
1831
1909
  ssid: z.string().optional().describe("Network name — required for connect and forget actions"),
1832
1910
  password: z.string().optional().describe("Network password — required for connect action (WPA/WPA2/WPA3 PSK)"),
1833
1911
  }, async ({ action, ssid, password }) => {
1912
+ if (!ACCOUNT_ID)
1913
+ return refuseNoAccount("wifi");
1834
1914
  /** Parse nmcli terse output into rows of colon-separated fields.
1835
1915
  * nmcli -t escapes literal colons in values as \: — split on
1836
1916
  * unescaped colons only, then unescape the fields. */
@@ -2084,6 +2164,8 @@ eagerTool(server, "wifi", "Manage WiFi connections on this device. Actions: " +
2084
2164
  server.tool("premium-list", "List available premium plugins and their delivery status. Returns structured data " +
2085
2165
  "for each premium plugin in the staging area: name, type (standalone/bundle), description, " +
2086
2166
  "purchase status, and delivery status (per sub-plugin for bundles).", {}, async () => {
2167
+ if (!ACCOUNT_ID)
2168
+ return refuseNoAccount("premium-list");
2087
2169
  try {
2088
2170
  if (!existsSync(STAGING_ROOT)) {
2089
2171
  return {
@@ -2158,6 +2240,8 @@ server.tool("premium-deliver", "Deliver a purchased premium plugin. Copies plugi
2158
2240
  "status and available templates.", {
2159
2241
  pluginName: z.string().describe("Name of the premium plugin to deliver (e.g. 'real-agent', 'teaching')"),
2160
2242
  }, async ({ pluginName }) => {
2243
+ if (!ACCOUNT_ID)
2244
+ return refuseNoAccount("premium-deliver");
2161
2245
  const TAG = "[premium-deliver]";
2162
2246
  // --- Validate plugin name ---
2163
2247
  if (!pluginName || !VALID_PLUGIN_NAME.test(pluginName)) {
@@ -2404,6 +2488,8 @@ eagerTool(server, "file-attach", "Attach a file from the account directory for d
2404
2488
  "and size on screen.", {
2405
2489
  filePath: z.string().describe("Absolute path to the file to attach"),
2406
2490
  }, async ({ filePath }) => {
2491
+ if (!ACCOUNT_ID)
2492
+ return refuseNoAccount("file-attach");
2407
2493
  const TAG = "[file-attach]";
2408
2494
  if (!filePath || !filePath.trim()) {
2409
2495
  return {
@@ -2572,6 +2658,13 @@ async function persistApprovalToolCall(opts) {
2572
2658
  console.error(`[approval] ToolCall skipped (no conversationId): tool=${opts.toolName} state=${opts.approvalState}`);
2573
2659
  return;
2574
2660
  }
2661
+ if (!ACCOUNT_ID) {
2662
+ // Boot-tolerance refusal handlers gate every caller, so reaching here is
2663
+ // a guard regression rather than a steady-state condition. Throw loud so
2664
+ // the missing handler-level guard is identified, not silently default to
2665
+ // empty-string accountId (which would mint orphan ToolCall nodes).
2666
+ throw new Error(`[approval] persistApprovalToolCall reached without ACCOUNT_ID — handler guard missing for tool=${opts.toolName}`);
2667
+ }
2575
2668
  const session = getSession();
2576
2669
  try {
2577
2670
  const optionalFields = [
@@ -2642,6 +2735,9 @@ async function dispatchApprovedAction(plugin, tool, args, timeoutMs = 30_000) {
2642
2735
  env[k] = v;
2643
2736
  }
2644
2737
  env.PLATFORM_ROOT = PLATFORM_ROOT;
2738
+ if (!ACCOUNT_ID) {
2739
+ throw new Error("dispatchApprovedAction called without ACCOUNT_ID — handler guard missing");
2740
+ }
2645
2741
  env.ACCOUNT_ID = ACCOUNT_ID;
2646
2742
  const transport = new StdioClientTransport({
2647
2743
  command: process.execPath,
@@ -2690,6 +2786,8 @@ async function dispatchApprovedAction(plugin, tool, args, timeoutMs = 30_000) {
2690
2786
  eagerTool(server, "action-pending", "List actions that are queued for human approval. Returns each pending action's ID, " +
2691
2787
  "tool name, input summary, and when it was queued. Use this to review what the agent " +
2692
2788
  "wanted to do before approving or rejecting.", {}, async () => {
2789
+ if (!ACCOUNT_ID)
2790
+ return refuseNoAccount("action-pending");
2693
2791
  const actions = readPendingActions();
2694
2792
  if (actions.length === 0) {
2695
2793
  return {
@@ -2710,6 +2808,8 @@ eagerTool(server, "action-approve", "Approve a pending action and execute it imm
2710
2808
  actionId: z.string().describe("The action ID to approve (from action-pending)"),
2711
2809
  conversationId: z.string().optional().describe("Current conversation ID for audit trail linking"),
2712
2810
  }, async ({ actionId, conversationId }) => {
2811
+ if (!ACCOUNT_ID)
2812
+ return refuseNoAccount("action-approve");
2713
2813
  const action = readPendingAction(actionId);
2714
2814
  if (!action) {
2715
2815
  return {
@@ -2780,6 +2880,8 @@ eagerTool(server, "action-reject", "Reject a pending action. The action is not e
2780
2880
  reason: z.string().optional().describe("Optional reason for rejection"),
2781
2881
  conversationId: z.string().optional().describe("Current conversation ID for audit trail linking"),
2782
2882
  }, async ({ actionId, reason, conversationId }) => {
2883
+ if (!ACCOUNT_ID)
2884
+ return refuseNoAccount("action-reject");
2783
2885
  const action = readPendingAction(actionId);
2784
2886
  if (!action) {
2785
2887
  return {
@@ -2822,6 +2924,8 @@ eagerTool(server, "action-edit", "Edit a pending action's input and then execute
2822
2924
  editedInput: z.string().describe("The modified tool input as a JSON string"),
2823
2925
  conversationId: z.string().optional().describe("Current conversation ID for audit trail linking"),
2824
2926
  }, async ({ actionId, editedInput, conversationId }) => {
2927
+ if (!ACCOUNT_ID)
2928
+ return refuseNoAccount("action-edit");
2825
2929
  const action = readPendingAction(actionId);
2826
2930
  if (!action) {
2827
2931
  return {