@rubytech/create-maxy-code 0.1.411 → 0.1.414

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 (162) hide show
  1. package/dist/__tests__/installer-cwd-pin-and-auth-verify.test.js +82 -0
  2. package/dist/index.js +44 -5
  3. package/package.json +1 -1
  4. package/payload/platform/lib/account-scope/dist/index.d.ts +27 -0
  5. package/payload/platform/lib/account-scope/dist/index.d.ts.map +1 -0
  6. package/payload/platform/lib/account-scope/dist/index.js +33 -0
  7. package/payload/platform/lib/account-scope/dist/index.js.map +1 -0
  8. package/payload/platform/lib/account-scope/src/index.ts +43 -0
  9. package/payload/platform/lib/account-scope/tsconfig.json +5 -0
  10. package/payload/platform/package.json +2 -2
  11. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  12. package/payload/platform/plugins/admin/hooks/lib/maxy-mcp-plugins.txt +1 -0
  13. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +37 -1
  14. package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +2 -0
  15. package/payload/platform/plugins/docs/references/cross-account-authority.md +25 -0
  16. package/payload/platform/plugins/docs/references/memory-guide.md +6 -0
  17. package/payload/platform/plugins/filesystem/.claude-plugin/plugin.json +21 -0
  18. package/payload/platform/plugins/filesystem/PLUGIN.md +63 -0
  19. package/payload/platform/plugins/filesystem/lib/mcp-spawn-tee/index.js +193 -0
  20. package/payload/platform/plugins/filesystem/lib/mcp-spawn-tee/package.json +3 -0
  21. package/payload/platform/plugins/filesystem/mcp/dist/index.d.ts +2 -0
  22. package/payload/platform/plugins/filesystem/mcp/dist/index.d.ts.map +1 -0
  23. package/payload/platform/plugins/filesystem/mcp/dist/index.js +91 -0
  24. package/payload/platform/plugins/filesystem/mcp/dist/index.js.map +1 -0
  25. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.d.ts +2 -0
  26. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.d.ts.map +1 -0
  27. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.js +89 -0
  28. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.js.map +1 -0
  29. package/payload/platform/plugins/filesystem/mcp/dist/lib/result.d.ts +12 -0
  30. package/payload/platform/plugins/filesystem/mcp/dist/lib/result.d.ts.map +1 -0
  31. package/payload/platform/plugins/filesystem/mcp/dist/lib/result.js +6 -0
  32. package/payload/platform/plugins/filesystem/mcp/dist/lib/result.js.map +1 -0
  33. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts +45 -0
  34. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts.map +1 -0
  35. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js +86 -0
  36. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js.map +1 -0
  37. package/payload/platform/plugins/filesystem/mcp/dist/lib/walk.d.ts +11 -0
  38. package/payload/platform/plugins/filesystem/mcp/dist/lib/walk.d.ts.map +1 -0
  39. package/payload/platform/plugins/filesystem/mcp/dist/lib/walk.js +64 -0
  40. package/payload/platform/plugins/filesystem/mcp/dist/lib/walk.js.map +1 -0
  41. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.d.ts +2 -0
  42. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.d.ts.map +1 -0
  43. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.js +94 -0
  44. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.js.map +1 -0
  45. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-edit.d.ts +8 -0
  46. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-edit.d.ts.map +1 -0
  47. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-edit.js +40 -0
  48. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-edit.js.map +1 -0
  49. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-glob.d.ts +6 -0
  50. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-glob.d.ts.map +1 -0
  51. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-glob.js +13 -0
  52. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-glob.js.map +1 -0
  53. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-grep.d.ts +7 -0
  54. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-grep.d.ts.map +1 -0
  55. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-grep.js +71 -0
  56. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-grep.js.map +1 -0
  57. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-list.d.ts +6 -0
  58. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-list.d.ts.map +1 -0
  59. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-list.js +26 -0
  60. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-list.js.map +1 -0
  61. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-read.d.ts +6 -0
  62. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-read.d.ts.map +1 -0
  63. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-read.js +28 -0
  64. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-read.js.map +1 -0
  65. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-write.d.ts +7 -0
  66. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-write.d.ts.map +1 -0
  67. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-write.js +25 -0
  68. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-write.js.map +1 -0
  69. package/payload/platform/plugins/filesystem/mcp/package.json +21 -0
  70. package/payload/platform/plugins/filesystem/mcp/vitest.config.ts +8 -0
  71. package/payload/platform/plugins/memory/PLUGIN.md +8 -0
  72. package/payload/platform/plugins/memory/mcp/dist/index-testable.d.ts +34 -0
  73. package/payload/platform/plugins/memory/mcp/dist/index-testable.d.ts.map +1 -0
  74. package/payload/platform/plugins/memory/mcp/dist/index-testable.js +60 -0
  75. package/payload/platform/plugins/memory/mcp/dist/index-testable.js.map +1 -0
  76. package/payload/platform/plugins/memory/mcp/dist/index.js +327 -146
  77. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  78. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-owner-userid.d.ts +15 -0
  79. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-owner-userid.d.ts.map +1 -0
  80. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-owner-userid.js +29 -0
  81. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-owner-userid.js.map +1 -0
  82. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.d.ts +2 -0
  83. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.d.ts.map +1 -0
  84. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.js +74 -0
  85. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.js.map +1 -0
  86. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-owner-userid.test.d.ts +2 -0
  87. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-owner-userid.test.d.ts.map +1 -0
  88. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-owner-userid.test.js +40 -0
  89. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-owner-userid.test.js.map +1 -0
  90. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.d.ts +2 -0
  91. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.d.ts.map +1 -0
  92. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.js +54 -0
  93. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.js.map +1 -0
  94. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts +11 -0
  95. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts.map +1 -1
  96. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +13 -1
  97. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
  98. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +2 -0
  99. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  100. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +7 -0
  101. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  102. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.d.ts.map +1 -1
  103. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.js +6 -1
  104. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.js.map +1 -1
  105. package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.d.ts +7 -1
  106. package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.d.ts.map +1 -1
  107. package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.js +7 -2
  108. package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.js.map +1 -1
  109. package/payload/platform/services/claude-session-manager/dist/config.d.ts +37 -0
  110. package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
  111. package/payload/platform/services/claude-session-manager/dist/config.js +34 -1
  112. package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
  113. package/payload/platform/services/claude-session-manager/dist/cross-account-census.d.ts +34 -0
  114. package/payload/platform/services/claude-session-manager/dist/cross-account-census.d.ts.map +1 -0
  115. package/payload/platform/services/claude-session-manager/dist/cross-account-census.js +98 -0
  116. package/payload/platform/services/claude-session-manager/dist/cross-account-census.js.map +1 -0
  117. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  118. package/payload/platform/services/claude-session-manager/dist/http-server.js +10 -0
  119. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  120. package/payload/platform/services/claude-session-manager/dist/index.js +17 -0
  121. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  122. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  123. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +25 -0
  124. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  125. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +7 -0
  126. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -1
  127. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +8 -0
  128. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -1
  129. package/payload/server/{chunk-ZYS5V5QG.js → chunk-3Z5WE53X.js} +4 -0
  130. package/payload/server/maxy-edge.js +1 -1
  131. package/payload/server/public/assets/{AdminLoginScreens-BRQgI2lW.js → AdminLoginScreens-CYPTMmTk.js} +1 -1
  132. package/payload/server/public/assets/{AdminShell-By9n5icG.js → AdminShell-CZH_EZIy.js} +1 -1
  133. package/payload/server/public/assets/{Checkbox-DjEWgYu1.js → Checkbox-B0pbYJDQ.js} +1 -1
  134. package/payload/server/public/assets/{Transcript-BejWvOaO.js → Transcript-CYaAPBFX.js} +1 -1
  135. package/payload/server/public/assets/{admin-BFEMryQm.js → admin-BKDMSRto.js} +1 -1
  136. package/payload/server/public/assets/{browser-CMJMEUz6.js → browser-CxeMDDZA.js} +1 -1
  137. package/payload/server/public/assets/{calendar-C6ZGlHEP.js → calendar-DiIiTY9-.js} +1 -1
  138. package/payload/server/public/assets/chat-C56Oqvt1.js +1 -0
  139. package/payload/server/public/assets/chevron-left-Da-iZGi_.js +1 -0
  140. package/payload/server/public/assets/data-ZUdT44Ba.js +1 -0
  141. package/payload/server/public/assets/{graph-DIrXtPLv.js → graph-DC3dwnMZ.js} +1 -1
  142. package/payload/server/public/assets/{graph-labels-BtPf1Jom.js → graph-labels-Csnovm_c.js} +1 -1
  143. package/payload/server/public/assets/{maximize-2-DeJ4K8kL.js → maximize-2-WRUJsg5n.js} +1 -1
  144. package/payload/server/public/assets/{operator-B8QhqzLI.js → operator-BpzeHTAw.js} +1 -1
  145. package/payload/server/public/assets/{page-iFOy0dFx.js → page-8jJOxx6y.js} +1 -1
  146. package/payload/server/public/assets/{page-Df6KYkf4.js → page-IQDLdebG.js} +1 -1
  147. package/payload/server/public/assets/{public-BrUIRQZd.js → public-Dqg7jwjq.js} +1 -1
  148. package/payload/server/public/assets/{rotate-ccw-CYfntpwu.js → rotate-ccw-BayMWwfc.js} +1 -1
  149. package/payload/server/public/assets/{useSubAccountSwitcher-Cvnc_fwQ.css → useSubAccountSwitcher-CRZfyJXR.css} +1 -1
  150. package/payload/server/public/browser.html +4 -4
  151. package/payload/server/public/calendar.html +5 -5
  152. package/payload/server/public/chat.html +9 -9
  153. package/payload/server/public/data.html +8 -8
  154. package/payload/server/public/graph.html +9 -9
  155. package/payload/server/public/index.html +11 -11
  156. package/payload/server/public/operator.html +11 -11
  157. package/payload/server/public/public.html +9 -9
  158. package/payload/server/server.js +72 -15
  159. package/payload/server/public/assets/chat-B7s62Nnp.js +0 -1
  160. package/payload/server/public/assets/chevron-left-Dfjcaiyr.js +0 -1
  161. package/payload/server/public/assets/data-741ZbBwR.js +0 -1
  162. /package/payload/server/public/assets/{useSubAccountSwitcher-BK_KfOLC.js → useSubAccountSwitcher-C9aLg8yI.js} +0 -0
@@ -41,6 +41,7 @@ import { profileRead } from "./tools/profile-read.js";
41
41
  import { memoryBrainCaptureRecent } from "./tools/memory-brain-capture-recent.js";
42
42
  import { profileUpdate } from "./tools/profile-update.js";
43
43
  import { profileDelete } from "./tools/profile-delete.js";
44
+ import { resolveOwnerUserId } from "./lib/resolve-owner-userid.js";
44
45
  import { graphPruneDenylistAdd } from "./tools/graph-prune-denylist-add.js";
45
46
  import { graphPruneDenylistList } from "./tools/graph-prune-denylist-list.js";
46
47
  import { graphPruneDenylistRemove } from "./tools/graph-prune-denylist-remove.js";
@@ -59,6 +60,10 @@ import { getSession, closeDriver } from "./lib/neo4j.js";
59
60
  import { embed } from "./lib/embeddings.js";
60
61
  import { notTrashed } from "../../../../lib/graph-trash/dist/index.js";
61
62
  import { lifelineTool } from "../../../../lib/mcp-lifeline/dist/index.js";
63
+ import { resolve } from "node:path";
64
+ import { enumerateValidAccountIds } from "../../../../lib/account-enumeration/dist/index.js";
65
+ import { readHouseAdminScope } from "../../../../lib/account-scope/dist/index.js";
66
+ import { makeResolveToolAccount } from "./index-testable.js";
62
67
  const server = new McpServer({
63
68
  name: "maxy-memory",
64
69
  version: "0.1.0",
@@ -74,17 +79,62 @@ const accountId = process.env.ACCOUNT_ID ?? null;
74
79
  // accountId`; `agentSlug` empty on a specialist spawn (provenance lost);
75
80
  // `agentScope` non-empty on the admin operator spawn (search wrongly scoped).
76
81
  process.stderr.write(`[memory] boot accountId=${accountId ?? "null"} sessionId=${process.env.SESSION_ID || "-"} agentSlug=${process.env.AGENT_SLUG || "-"} agentScope=${process.env.AGENT_SLUG_SCOPE || "-"}\n`);
77
- function refuseNoAccount(toolName) {
78
- process.stderr.write(`[memory] tool=${toolName} refuse reason=no-account-context\n`);
79
- return {
80
- content: [
81
- {
82
- type: "text",
83
- text: `${toolName} cannot execute: this MCP instance was launched without ACCOUNT_ID (plugin-system metadata-only mode). Per-account graph access requires ACCOUNT_ID at spawn.`,
82
+ // The no-account refusal (formerly `refuseNoAccount`) is now handled inside
83
+ // `resolveToolAccount` / `makeResolveToolAccount` (index-testable.ts), which
84
+ // preserves the same message when the boot ACCOUNT_ID is null. Task 1455.
85
+ // ---------------------------------------------------------------------------
86
+ // Cross-account authority (Task 1455). A house-scoped admin session may set an
87
+ // optional `targetAccountId` on any account-scoped tool to read/write a
88
+ // sub-account's subgraph; omitting it is byte-identical to own-account.
89
+ //
90
+ // `houseScope` rides the spawn env (Task 1440, `HOUSE_ADMIN_SCOPE`). `accountsDir`
91
+ // mirrors neo4j.ts's platform-root resolution (PLATFORM_ROOT env, else relative
92
+ // to the compiled file): from dist/index.js, `../../../..` is `platform/`, and
93
+ // accounts live at `<platform>/../data/accounts`. `enumerateValidAccountIds`
94
+ // returns [] (never throws) on a missing dir, so a misresolved path fails CLOSED
95
+ // — every target rejects invalid-target. The boot line makes that visible rather
96
+ // than silent: a zero `validAccounts` on a provisioned install is the alarm.
97
+ const houseScope = readHouseAdminScope();
98
+ const accountsDir = resolve(process.env.PLATFORM_ROOT ?? resolve(import.meta.dirname, "../../../.."), "..", "data/accounts");
99
+ process.stderr.write(`[xacct] boot houseScope=${houseScope ? "yes" : "no"} accountsDir=${accountsDir} validAccounts=${enumerateValidAccountIds(accountsDir).length}\n`);
100
+ const resolveToolAccount = makeResolveToolAccount({
101
+ accountId,
102
+ houseScope,
103
+ isValid: (id) => enumerateValidAccountIds(accountsDir).includes(id),
104
+ });
105
+ // Single source for the operator-facing targetAccountId description — the LLM
106
+ // reads this verbatim, so it must not drift across the 35 targetable tools.
107
+ const XACCT_DESC = "House-scoped admin only: operate on the named sub-account subgraph instead of your own. Only a house-scoped session may set this; a non-house session that sets it is rejected.";
108
+ // Boot account, captured before the tool handlers shadow `accountId` with the
109
+ // resolved (possibly cross-account) id. A resolved id != this is a cross-account
110
+ // call (the resolver only returns a target for a house-authorised session).
111
+ const BOOT_ACCOUNT_ID = accountId;
112
+ async function resolveProfileIdentity(toolName, crossAccount, resolvedAccountId, callerUserId) {
113
+ if (crossAccount) {
114
+ const owner = await resolveOwnerUserId(resolvedAccountId);
115
+ if (!owner) {
116
+ return {
117
+ reject: {
118
+ content: [{
119
+ type: "text",
120
+ text: `${toolName} failed: target account ${resolvedAccountId.slice(0, 8)}… has no owner AdminUser to attribute the profile to — its graph root is unprovisioned (see Task 1359 seedAccountGraphRoot).`,
121
+ }],
122
+ isError: true,
123
+ },
124
+ };
125
+ }
126
+ process.stderr.write(`[xacct] op=identity tool=${toolName} target=${resolvedAccountId.slice(0, 8)} resolved-userId=${owner.slice(0, 8)}\n`);
127
+ return { userId: owner };
128
+ }
129
+ if (!callerUserId) {
130
+ return {
131
+ reject: {
132
+ content: [{ type: "text", text: `${toolName} requires an authenticated admin session with userId` }],
133
+ isError: true,
84
134
  },
85
- ],
86
- isError: true,
87
- };
135
+ };
136
+ }
137
+ return { userId: callerUserId };
88
138
  }
89
139
  // Load the markdown schema sidecar once at startup. Every memory-write call
90
140
  // reads its required-property and synonym maps. If loading fails we throw —
@@ -242,9 +292,12 @@ eagerTool(server, "memory-search", "Search the knowledge graph using semantic ve
242
292
  .enum(["entity", "temporal", "event", "general", "none"])
243
293
  .optional()
244
294
  .describe("Hint that selects the per-class index mix (route-hint contract). Pass 'entity' for who/what-is-this lookups (vector-heavy, Person|Company|Concept filter), 'temporal' for when-did-this-happen (BM25-heavy over :Event with Timeline), 'event' for pure event listings, 'none' to skip graph lookup entirely (greetings/instructions). Omit or pass 'general' for the balanced default. Routing only takes effect when MAXY_GS_ROUTE is enabled; otherwise the value is recorded in the log line but does not change index selection."),
245
- }, async ({ query, labels, limit, expandHops, keywords, keywordMatch, agentSlug: toolAgentSlug, fields, retrievalClass }) => {
246
- if (!accountId)
247
- return refuseNoAccount("memory-search");
295
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
296
+ }, async ({ query, labels, limit, expandHops, keywords, keywordMatch, agentSlug: toolAgentSlug, fields, retrievalClass, targetAccountId }) => {
297
+ const scoped = resolveToolAccount("memory-search", targetAccountId);
298
+ if ("reject" in scoped)
299
+ return scoped.reject;
300
+ const accountId = scoped.accountId;
248
301
  // AGENT_SLUG_SCOPE takes precedence over the tool param (security: public
249
302
  // agent server-level enforcement). Empty for the admin operator, so admin
250
303
  // falls through to the optional tool param (default: unscoped).
@@ -358,9 +411,12 @@ eagerTool(server, "memory-lookup-by-name", "Deterministic by-name entity lookup.
358
411
  .number()
359
412
  .optional()
360
413
  .describe("Maximum number of matching nodes to return (default 25)."),
361
- }, async ({ name, labels, limit }) => {
362
- if (!accountId)
363
- return refuseNoAccount("memory-lookup-by-name");
414
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
415
+ }, async ({ name, labels, limit, targetAccountId }) => {
416
+ const scoped = resolveToolAccount("memory-lookup-by-name", targetAccountId);
417
+ if ("reject" in scoped)
418
+ return scoped.reject;
419
+ const accountId = scoped.accountId;
364
420
  const effectiveAgentSlug = envAgentScopeSlug;
365
421
  try {
366
422
  const hits = await memoryLookupByName({
@@ -425,9 +481,12 @@ lifelineTool(server, "conversation-archive-list-chunks", "Page through the :Sect
425
481
  .max(20)
426
482
  .optional()
427
483
  .describe("Chunks per page (1..20). Default 5."),
428
- }, async ({ archiveElementId, chunkOffset, chunkLimit }) => {
429
- if (!accountId)
430
- return refuseNoAccount("conversation-archive-list-chunks");
484
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
485
+ }, async ({ archiveElementId, chunkOffset, chunkLimit, targetAccountId }) => {
486
+ const scoped = resolveToolAccount("conversation-archive-list-chunks", targetAccountId);
487
+ if ("reject" in scoped)
488
+ return scoped.reject;
489
+ const accountId = scoped.accountId;
431
490
  try {
432
491
  const result = await conversationArchiveListChunks({
433
492
  accountId,
@@ -504,9 +563,12 @@ lifelineTool(server, "conversation-archive-derive-insights", "Convert one chunk'
504
563
  .describe("Claims the dispatched specialist emitted in-turn from the chunk body. Zero is valid — pass [] to record an empty-chunk pass."),
505
564
  inlineRewrite: z.boolean().optional().describe("When true, count mention-claim subjects in this chunk and return those reaching inlineRewriteThreshold in inlineRewriteSignals. The calling skill accumulates signals across chunks; when an entity's cumulative count hits threshold, resolve it via memory-search and dispatch the compiled-truth-rewriter specialist."),
506
565
  inlineRewriteThreshold: z.number().int().min(1).optional().describe("Minimum mention-claim count for a subject to appear in inlineRewriteSignals. Default: 2."),
507
- }, async ({ archiveElementId, chunkElementId, claims, inlineRewrite, inlineRewriteThreshold }) => {
508
- if (!accountId)
509
- return refuseNoAccount("conversation-archive-derive-insights");
566
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
567
+ }, async ({ archiveElementId, chunkElementId, claims, inlineRewrite, inlineRewriteThreshold, targetAccountId }) => {
568
+ const scoped = resolveToolAccount("conversation-archive-derive-insights", targetAccountId);
569
+ if ("reject" in scoped)
570
+ return scoped.reject;
571
+ const accountId = scoped.accountId;
510
572
  try {
511
573
  const result = await conversationArchiveDeriveInsights({
512
574
  accountId,
@@ -583,9 +645,12 @@ lifelineTool(server, "conversation-archive-enrich-rejection", "Record or undo a
583
645
  mode: z
584
646
  .enum(["record", "undo"])
585
647
  .describe("record = append rejection; undo = remove rejection (called after `wire`)."),
586
- }, async ({ archiveElementId, chunkElementId, kind, contentHash, mode }) => {
587
- if (!accountId)
588
- return refuseNoAccount("conversation-archive-enrich-rejection");
648
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
649
+ }, async ({ archiveElementId, chunkElementId, kind, contentHash, mode, targetAccountId }) => {
650
+ const scoped = resolveToolAccount("conversation-archive-enrich-rejection", targetAccountId);
651
+ if ("reject" in scoped)
652
+ return scoped.reject;
653
+ const accountId = scoped.accountId;
589
654
  try {
590
655
  const result = await conversationArchiveEnrichRejection({
591
656
  accountId,
@@ -629,9 +694,12 @@ eagerTool(server, "memory-report-read-latest", "Return the most recent :Report n
629
694
  .max(20)
630
695
  .optional()
631
696
  .describe("Maximum number of reports to return, newest first. Default 1."),
632
- }, async ({ keyword, limit }) => {
633
- if (!accountId)
634
- return refuseNoAccount("memory-report-read-latest");
697
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
698
+ }, async ({ keyword, limit, targetAccountId }) => {
699
+ const scoped = resolveToolAccount("memory-report-read-latest", targetAccountId);
700
+ if ("reject" in scoped)
701
+ return scoped.reject;
702
+ const accountId = scoped.accountId;
635
703
  try {
636
704
  const result = await memoryReportReadLatest({ accountId, keyword, limit });
637
705
  if (result.results.length === 0) {
@@ -686,9 +754,12 @@ eagerTool(server, "memory-report-list", "List :Report nodes, newest first, with
686
754
  .min(0)
687
755
  .optional()
688
756
  .describe("Zero-based offset for pagination. Default 0."),
689
- }, async ({ keyword, sourceWorkflow, limit, offset }) => {
690
- if (!accountId)
691
- return refuseNoAccount("memory-report-list");
757
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
758
+ }, async ({ keyword, sourceWorkflow, limit, offset, targetAccountId }) => {
759
+ const scoped = resolveToolAccount("memory-report-list", targetAccountId);
760
+ if ("reject" in scoped)
761
+ return scoped.reject;
762
+ const accountId = scoped.accountId;
692
763
  try {
693
764
  const result = await memoryReportList({
694
765
  accountId,
@@ -736,9 +807,12 @@ eagerTool(server, "memory-compiled-truth-history", "Return the audit history of
736
807
  nodeId: z
737
808
  .string()
738
809
  .describe("elementId of the :Person | :Organization | :Concept whose revision history is being read."),
739
- }, async ({ nodeId }) => {
740
- if (!accountId)
741
- return refuseNoAccount("memory-compiled-truth-history");
810
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
811
+ }, async ({ nodeId, targetAccountId }) => {
812
+ const scoped = resolveToolAccount("memory-compiled-truth-history", targetAccountId);
813
+ if ("reject" in scoped)
814
+ return scoped.reject;
815
+ const accountId = scoped.accountId;
742
816
  try {
743
817
  const result = await memoryCompiledTruthHistory({ accountId, nodeId });
744
818
  if (result.revisions.length === 0) {
@@ -793,9 +867,12 @@ if (!readOnly) {
793
867
  .string()
794
868
  .optional()
795
869
  .describe("Active Task elementId for autonomous-workflow writes. When set, an inbound :PRODUCED edge from this Task is composed into relationships before the write. Task must share the same accountId; mismatch is rejected. Omit for direct conversation asks — the platform composes the edge from the active conversation instead."),
796
- }, async ({ labels, properties, scope, relationships, producedByTaskId }) => {
797
- if (!accountId)
798
- return refuseNoAccount("memory-write");
870
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
871
+ }, async ({ labels, properties, scope, relationships, producedByTaskId, targetAccountId }) => {
872
+ const scoped = resolveToolAccount("memory-write", targetAccountId);
873
+ if ("reject" in scoped)
874
+ return scoped.reject;
875
+ const accountId = scoped.accountId;
799
876
  try {
800
877
  const result = await memoryWrite({
801
878
  labels,
@@ -853,9 +930,12 @@ if (!readOnly) {
853
930
  .string()
854
931
  .optional()
855
932
  .describe("ISO 8601 timestamp the report describes. Defaults to now(). Override only when persisting a backdated report."),
856
- }, async ({ title, body, keywords, sourceWorkflow, occurredAt }) => {
857
- if (!accountId)
858
- return refuseNoAccount("memory-report-write");
933
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
934
+ }, async ({ title, body, keywords, sourceWorkflow, occurredAt, targetAccountId }) => {
935
+ const scoped = resolveToolAccount("memory-report-write", targetAccountId);
936
+ if ("reject" in scoped)
937
+ return scoped.reject;
938
+ const accountId = scoped.accountId;
859
939
  try {
860
940
  const result = await memoryReportWrite({
861
941
  accountId,
@@ -896,9 +976,12 @@ if (!readOnly) {
896
976
  properties: z
897
977
  .record(z.string(), z.unknown())
898
978
  .describe("Properties to set or overwrite. Same restrictions as memory-update — embedding/accountId/createdAt are silently stripped; compiled-truth fields are refused unless the caller is the compiled-truth-rewriter specialist."),
899
- }, async ({ label, name, properties }) => {
900
- if (!accountId)
901
- return refuseNoAccount("memory-update-by-name");
979
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
980
+ }, async ({ label, name, properties, targetAccountId }) => {
981
+ const scoped = resolveToolAccount("memory-update-by-name", targetAccountId);
982
+ if ("reject" in scoped)
983
+ return scoped.reject;
984
+ const accountId = scoped.accountId;
902
985
  try {
903
986
  const result = await memoryUpdateByName({
904
987
  label,
@@ -937,9 +1020,12 @@ if (!readOnly) {
937
1020
  properties: z
938
1021
  .record(z.string(), z.unknown())
939
1022
  .describe("Properties to set or overwrite. Restricted keys (embedding, accountId, createdAt) are ignored. Compiled-truth fields are system-managed; only the compiled-truth-rewriter specialist may write them."),
940
- }, async ({ nodeId, properties }) => {
941
- if (!accountId)
942
- return refuseNoAccount("memory-update");
1023
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1024
+ }, async ({ nodeId, properties, targetAccountId }) => {
1025
+ const scoped = resolveToolAccount("memory-update", targetAccountId);
1026
+ if ("reject" in scoped)
1027
+ return scoped.reject;
1028
+ const accountId = scoped.accountId;
943
1029
  try {
944
1030
  const result = await memoryUpdate({
945
1031
  nodeId,
@@ -978,9 +1064,12 @@ if (!readOnly) {
978
1064
  toId: z.string().describe("elementId of the target node (from memory-search results)."),
979
1065
  relationshipType: z.string().describe("Relationship type label — e.g. RELATES_TO, MENTIONS, WORKED_WITH. Uppercase-coerced. Underscore-prefixed types (_SOFT_DELETE, etc.) are reserved and rejected."),
980
1066
  properties: z.record(z.string(), z.unknown()).optional().describe("Properties to set on the relationship on CREATE. Ignored on DELETE and on idempotent CREATE (MERGE-matched)."),
981
- }, async ({ action, fromId, toId, relationshipType, properties }) => {
982
- if (!accountId)
983
- return refuseNoAccount("memory-edge");
1067
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1068
+ }, async ({ action, fromId, toId, relationshipType, properties, targetAccountId }) => {
1069
+ const scoped = resolveToolAccount("memory-edge", targetAccountId);
1070
+ if ("reject" in scoped)
1071
+ return scoped.reject;
1072
+ const accountId = scoped.accountId;
984
1073
  try {
985
1074
  const result = await memoryEdge({
986
1075
  action,
@@ -1014,9 +1103,13 @@ if (!readOnly) {
1014
1103
  };
1015
1104
  }
1016
1105
  });
1017
- eagerTool(server, "session-compact-status", "Check whether a session compaction summary exists in the graph. Returns the most recent summary title, date, and briefing text. Use this at session start to determine whether a prior session was compacted and its context should be restored.", {}, async () => {
1018
- if (!accountId)
1019
- return refuseNoAccount("session-compact-status");
1106
+ eagerTool(server, "session-compact-status", "Check whether a session compaction summary exists in the graph. Returns the most recent summary title, date, and briefing text. Use this at session start to determine whether a prior session was compacted and its context should be restored.", {
1107
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1108
+ }, async ({ targetAccountId }) => {
1109
+ const scoped = resolveToolAccount("session-compact-status", targetAccountId);
1110
+ if ("reject" in scoped)
1111
+ return scoped.reject;
1112
+ const accountId = scoped.accountId;
1020
1113
  const session = getSession();
1021
1114
  try {
1022
1115
  const result = await session.run(`MATCH (n:CreativeWork {accountId: $accountId})
@@ -1043,8 +1136,10 @@ if (!readOnly) {
1043
1136
  eagerTool(server, "session-compact", "Write a session summary to the knowledge graph. Call this when compacting context — provide the full briefing and the tool handles labelling, structure, and embedding.", {
1044
1137
  content: z.string().describe("The session briefing — what was discussed, decisions made, work in progress, open questions, and anything learned about the user or their business"),
1045
1138
  }, async ({ content }) => {
1046
- if (!accountId)
1047
- return refuseNoAccount("session-compact");
1139
+ const scoped = resolveToolAccount("session-compact", undefined);
1140
+ if ("reject" in scoped)
1141
+ return scoped.reject;
1142
+ const accountId = scoped.accountId;
1048
1143
  try {
1049
1144
  // Write doctrine: session summaries link back to the
1050
1145
  // Conversation they summarise, so an operator searching a session's
@@ -1103,9 +1198,13 @@ if (!readOnly) {
1103
1198
  };
1104
1199
  }
1105
1200
  });
1106
- eagerTool(server, "memory-reindex", "Rebuild vector embeddings for all nodes missing embeddings. Run after seeding or bulk updates.", {}, async () => {
1107
- if (!accountId)
1108
- return refuseNoAccount("memory-reindex");
1201
+ eagerTool(server, "memory-reindex", "Rebuild vector embeddings for all nodes missing embeddings. Run after seeding or bulk updates.", {
1202
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1203
+ }, async ({ targetAccountId }) => {
1204
+ const scoped = resolveToolAccount("memory-reindex", targetAccountId);
1205
+ if ("reject" in scoped)
1206
+ return scoped.reject;
1207
+ const accountId = scoped.accountId;
1109
1208
  try {
1110
1209
  const result = await memoryReindex(accountId);
1111
1210
  return {
@@ -1140,8 +1239,10 @@ if (!readOnly) {
1140
1239
  mimeType: z.string().describe("MIME type of the file"),
1141
1240
  attachmentId: z.string().describe("UUID of the file attachment — used as cache key for memory-ingest"),
1142
1241
  }, async ({ storagePath, filename, mimeType, attachmentId }) => {
1143
- if (!accountId)
1144
- return refuseNoAccount("memory-ingest-extract");
1242
+ const scoped = resolveToolAccount("memory-ingest-extract", undefined);
1243
+ if ("reject" in scoped)
1244
+ return scoped.reject;
1245
+ const accountId = scoped.accountId;
1145
1246
  try {
1146
1247
  const result = await memoryIngestExtract({ storagePath, filename, mimeType, attachmentId });
1147
1248
  return {
@@ -1231,9 +1332,12 @@ if (!readOnly) {
1231
1332
  inlineRewrite: z.boolean().optional().describe("When true, after writing all sections and edges, run the inline compiled-truth reconciliation gate. Counts MENTIONS/REFERENCES edges from this document (KnowledgeDocument + its sections) to existing entities that carry compiledTruth. Entities with >= inlineRewriteThreshold such edges (default 2) and compiledTruthUpdatedAt older than 7 days are returned in inlineRewriteCandidates. Dispatch the compiled-truth-rewriter specialist for each before the skill returns. Disabled by default."),
1232
1333
  forceRewrite: z.boolean().optional().describe("When inlineRewrite is true, bypass the 7-day per-entity cooldown on compiledTruthUpdatedAt. All threshold-qualifying entities are returned as inlineRewriteCandidates regardless of when their truth was last rewritten."),
1233
1334
  inlineRewriteThreshold: z.number().int().min(1).optional().describe("Minimum MENTIONS/REFERENCES edge count from this source to an entity for it to qualify as an inline-rewrite candidate. Default: 2."),
1234
- }, async ({ attachmentId, conversationIdentity, source, archiveSha256, archiveSourceFile, participantElementIds, lastIngestedMessageHash, lastIngestedMessageAt, archiveTitle, cleanupPriorChunks, sessionIndex, documentSummary, anchorNodeId, anchorLabel, sections, documentEdges, orphanCandidates, scope, sourceUrl, sourceType, documentKeywords, userKeywords, sessionId: sessionIdOverride, inlineRewrite, forceRewrite, inlineRewriteThreshold }) => {
1235
- if (!accountId)
1236
- return refuseNoAccount("memory-ingest");
1335
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1336
+ }, async ({ attachmentId, conversationIdentity, source, archiveSha256, archiveSourceFile, participantElementIds, lastIngestedMessageHash, lastIngestedMessageAt, archiveTitle, cleanupPriorChunks, sessionIndex, documentSummary, anchorNodeId, anchorLabel, sections, documentEdges, orphanCandidates, scope, sourceUrl, sourceType, documentKeywords, userKeywords, sessionId: sessionIdOverride, inlineRewrite, forceRewrite, inlineRewriteThreshold, targetAccountId }) => {
1337
+ const scoped = resolveToolAccount("memory-ingest", targetAccountId);
1338
+ if ("reject" in scoped)
1339
+ return scoped.reject;
1340
+ const accountId = scoped.accountId;
1237
1341
  try {
1238
1342
  const result = await memoryIngest({
1239
1343
  accountId,
@@ -1296,9 +1400,12 @@ if (!readOnly) {
1296
1400
  });
1297
1401
  eagerTool(server, "kd-classify", "Operator-solicited reclassify for any :KnowledgeDocument that carries a `body` property (today: email-thread KDs; future: any thread-shaped source). Reads the KD by attachmentId, decides via a body-growth gate (proceed iff body grew ≥25% since the last classify, or first-ever classify), stages the body to a temp file under $ACCOUNT_DIR/tmp/, and returns a dispatch envelope the admin agent hands to the `librarian` specialist. Librarian runs the document-ingest skill against the temp file; memory-ingest MERGEs by attachmentId, replacing any prior :Section children, and stamps lastClassifiedAt + lastClassifiedBodyLength on the KD so the next gate call has a baseline. Bodyless attachment KDs (PDFs etc.) are not supported here — they have no growth signal.", {
1298
1402
  attachmentId: z.string().describe("attachmentId of the :KnowledgeDocument to classify. For email threads, this is sha256(`email:<accountId>:<rootMessageId>`)."),
1299
- }, async ({ attachmentId }) => {
1300
- if (!accountId)
1301
- return refuseNoAccount("kd-classify");
1403
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1404
+ }, async ({ attachmentId, targetAccountId }) => {
1405
+ const scoped = resolveToolAccount("kd-classify", targetAccountId);
1406
+ if ("reject" in scoped)
1407
+ return scoped.reject;
1408
+ const accountId = scoped.accountId;
1302
1409
  try {
1303
1410
  const text = await kdClassify({ accountId, attachmentId });
1304
1411
  return { content: [{ type: "text", text }] };
@@ -1336,9 +1443,12 @@ if (!readOnly) {
1336
1443
  .string()
1337
1444
  .optional()
1338
1445
  .describe("Skill-run UUID for provenance stamping. Falls back to SESSION_ID env var when absent."),
1339
- }, async ({ archiveType, ownerNodeId, rows, sessionId: sessionIdOverride }) => {
1340
- if (!accountId)
1341
- return refuseNoAccount("memory-archive-write");
1446
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1447
+ }, async ({ archiveType, ownerNodeId, rows, sessionId: sessionIdOverride, targetAccountId }) => {
1448
+ const scoped = resolveToolAccount("memory-archive-write", targetAccountId);
1449
+ if ("reject" in scoped)
1450
+ return scoped.reject;
1451
+ const accountId = scoped.accountId;
1342
1452
  try {
1343
1453
  const result = await memoryArchiveWrite({
1344
1454
  archiveType,
@@ -1418,9 +1528,12 @@ if (!readOnly) {
1418
1528
  .string()
1419
1529
  .optional()
1420
1530
  .describe("Skill-run UUID for provenance stamping. Falls back to SESSION_ID env var when absent."),
1421
- }, async ({ ownerNodeId, extractedDir, mode, importId, filters, resolutions, selectiveIngestThreshold, sessionId: sessionIdOverride, }) => {
1422
- if (!accountId)
1423
- return refuseNoAccount("obsidian-vault-import");
1531
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1532
+ }, async ({ ownerNodeId, extractedDir, mode, importId, filters, resolutions, selectiveIngestThreshold, sessionId: sessionIdOverride, targetAccountId, }) => {
1533
+ const scoped = resolveToolAccount("obsidian-vault-import", targetAccountId);
1534
+ if ("reject" in scoped)
1535
+ return scoped.reject;
1536
+ const accountId = scoped.accountId;
1424
1537
  try {
1425
1538
  const result = await obsidianVaultImport({
1426
1539
  accountId,
@@ -1469,9 +1582,12 @@ if (!readOnly) {
1469
1582
  "When present, used verbatim as `KnowledgeDocument.name` (capped at 200 chars). " +
1470
1583
  "When absent, the server falls back to the markdown H1, then a URL-derived slug, then the URL."),
1471
1584
  scope: z.string().optional().describe("Intended visibility scope (for logging). The actual scope is set when the agent subsequently calls memory-ingest."),
1472
- }, async ({ url, content, title, scope }) => {
1473
- if (!accountId)
1474
- return refuseNoAccount("memory-ingest-web");
1585
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1586
+ }, async ({ url, content, title, scope, targetAccountId }) => {
1587
+ const scoped = resolveToolAccount("memory-ingest-web", targetAccountId);
1588
+ if ("reject" in scoped)
1589
+ return scoped.reject;
1590
+ const accountId = scoped.accountId;
1475
1591
  try {
1476
1592
  const result = await memoryIngestWeb({
1477
1593
  accountId,
@@ -1518,9 +1634,12 @@ if (!readOnly) {
1518
1634
  agentType: z.enum(["admin", "public"]).optional().describe("Scope to conversations matching c.agentType. Omit to scope across both."),
1519
1635
  limit: z.number().int().positive().optional().describe("Max candidates to return (default 200, hard cap 1000)."),
1520
1636
  dryRun: z.boolean().optional().describe("Preview matches without minting an actionable filterToken. Candidates are listed in the response, but no delete can be performed against them — caller must re-run without dryRun to receive a token."),
1521
- }, async ({ filter, agentType, limit, dryRun }) => {
1522
- if (!accountId)
1523
- return refuseNoAccount("memory-find-candidates");
1637
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1638
+ }, async ({ filter, agentType, limit, dryRun, targetAccountId }) => {
1639
+ const scoped = resolveToolAccount("memory-find-candidates", targetAccountId);
1640
+ if ("reject" in scoped)
1641
+ return scoped.reject;
1642
+ const accountId = scoped.accountId;
1524
1643
  try {
1525
1644
  const result = await memoryFindCandidates({ accountId, filter, agentType, limit, dryRun });
1526
1645
  if (result.items.length === 0) {
@@ -1561,9 +1680,12 @@ if (!readOnly) {
1561
1680
  "ranked discovery on general labels. Use this before any branch that claims a " +
1562
1681
  "singleton root is missing.", {
1563
1682
  label: z.enum(MEMORY_NODE_EXISTS_LABELS).describe("Installer-seeded singleton label. One of: LocalBusiness, AdminUser, UserProfile, Person."),
1564
- }, async ({ label }) => {
1565
- if (!accountId)
1566
- return refuseNoAccount("memory-node-exists");
1683
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1684
+ }, async ({ label, targetAccountId }) => {
1685
+ const scoped = resolveToolAccount("memory-node-exists", targetAccountId);
1686
+ if ("reject" in scoped)
1687
+ return scoped.reject;
1688
+ const accountId = scoped.accountId;
1567
1689
  try {
1568
1690
  const result = await memoryNodeExists({ accountId, label });
1569
1691
  const payload = JSON.stringify(result);
@@ -1588,9 +1710,12 @@ if (!readOnly) {
1588
1710
  filterToken: z.string().optional().describe("Opaque token from memory-find-candidates. Required for bulk-selector flows; the server re-runs the " +
1589
1711
  "canonical predicate on this elementId before trashing. Omit for single-node deletes where the caller " +
1590
1712
  "chose the target directly (e.g. from a memory-search result)."),
1591
- }, async ({ elementId, reason, filterToken }) => {
1592
- if (!accountId)
1593
- return refuseNoAccount("memory-delete");
1713
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1714
+ }, async ({ elementId, reason, filterToken, targetAccountId }) => {
1715
+ const scoped = resolveToolAccount("memory-delete", targetAccountId);
1716
+ if ("reject" in scoped)
1717
+ return scoped.reject;
1718
+ const accountId = scoped.accountId;
1594
1719
  try {
1595
1720
  const result = await memoryDelete({ accountId, elementId, reason, filterToken });
1596
1721
  const head = result.alreadyTrashed
@@ -1617,9 +1742,12 @@ if (!readOnly) {
1617
1742
  "node needs back (e.g. a fresh LocalBusiness was created while the old one was in trash) — " +
1618
1743
  "the error names the conflicting elementId so the caller can decide how to resolve.", {
1619
1744
  elementId: z.string().describe("Neo4j elementId of the trashed node to restore."),
1620
- }, async ({ elementId }) => {
1621
- if (!accountId)
1622
- return refuseNoAccount("memory-restore");
1745
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1746
+ }, async ({ elementId, targetAccountId }) => {
1747
+ const scoped = resolveToolAccount("memory-restore", targetAccountId);
1748
+ if ("reject" in scoped)
1749
+ return scoped.reject;
1750
+ const accountId = scoped.accountId;
1623
1751
  try {
1624
1752
  const result = await memoryRestore({ accountId, elementId });
1625
1753
  const cascade = result.cascade.sections + result.cascade.chunks > 0
@@ -1647,9 +1775,12 @@ if (!readOnly) {
1647
1775
  graceDays: z.number().int().min(0).optional().describe("Minimum age in days a trashed node must reach before it's eligible. Default 30."),
1648
1776
  dryRun: z.boolean().optional().describe("When true, lists candidates without deleting. Default false."),
1649
1777
  labels: z.array(z.string()).optional().describe("Optional whitelist of node labels — only nodes carrying any of these labels are eligible."),
1650
- }, async ({ graceDays, dryRun, labels }) => {
1651
- if (!accountId)
1652
- return refuseNoAccount("memory-empty-trash");
1778
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1779
+ }, async ({ graceDays, dryRun, labels, targetAccountId }) => {
1780
+ const scoped = resolveToolAccount("memory-empty-trash", targetAccountId);
1781
+ if ("reject" in scoped)
1782
+ return scoped.reject;
1783
+ const accountId = scoped.accountId;
1653
1784
  try {
1654
1785
  const result = await memoryEmptyTrash({ accountId, graceDays, dryRun, labels });
1655
1786
  const lines = [];
@@ -1673,9 +1804,13 @@ if (!readOnly) {
1673
1804
  });
1674
1805
  lifelineTool(server, "memory-list-attachments", "List stored file attachments for this account with their ingestion status. " +
1675
1806
  "Returns original filenames, sizes, upload dates, and whether each file has been ingested " +
1676
- "into the knowledge graph. Does not include web-ingested documents (those have no file on disk).", {}, async () => {
1677
- if (!accountId)
1678
- return refuseNoAccount("memory-list-attachments");
1807
+ "into the knowledge graph. Does not include web-ingested documents (those have no file on disk).", {
1808
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1809
+ }, async ({ targetAccountId }) => {
1810
+ const scoped = resolveToolAccount("memory-list-attachments", targetAccountId);
1811
+ if ("reject" in scoped)
1812
+ return scoped.reject;
1813
+ const accountId = scoped.accountId;
1679
1814
  try {
1680
1815
  const result = await memoryListAttachments(accountId);
1681
1816
  if (result.entries.length === 0) {
@@ -1713,9 +1848,12 @@ if (!readOnly) {
1713
1848
  "extracted text for PDFs (via pdftotext), and metadata + HTTP URL for images. " +
1714
1849
  "Use this to show the user what's in a file or to prepare content for editing.", {
1715
1850
  attachmentId: z.string().describe("UUID of the attachment to read"),
1716
- }, async ({ attachmentId }) => {
1717
- if (!accountId)
1718
- return refuseNoAccount("memory-read-attachment");
1851
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1852
+ }, async ({ attachmentId, targetAccountId }) => {
1853
+ const scoped = resolveToolAccount("memory-read-attachment", targetAccountId);
1854
+ if ("reject" in scoped)
1855
+ return scoped.reject;
1856
+ const accountId = scoped.accountId;
1719
1857
  try {
1720
1858
  const result = await memoryReadAttachment(accountId, attachmentId);
1721
1859
  let text = `**${result.filename}** (${result.mimeType}, ${result.sizeBytes} bytes)\nStored: ${result.storedAt}\n`;
@@ -1740,9 +1878,12 @@ if (!readOnly) {
1740
1878
  "Only text/plain and text/markdown files are editable — PDFs and images are rejected.", {
1741
1879
  attachmentId: z.string().describe("UUID of the attachment to edit"),
1742
1880
  content: z.string().describe("The full replacement text content"),
1743
- }, async ({ attachmentId, content }) => {
1744
- if (!accountId)
1745
- return refuseNoAccount("memory-edit-attachment");
1881
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1882
+ }, async ({ attachmentId, content, targetAccountId }) => {
1883
+ const scoped = resolveToolAccount("memory-edit-attachment", targetAccountId);
1884
+ if ("reject" in scoped)
1885
+ return scoped.reject;
1886
+ const accountId = scoped.accountId;
1746
1887
  try {
1747
1888
  const result = await memoryEditAttachment({ accountId, attachmentId, content });
1748
1889
  let text = `Edited: ${result.filename} (${result.oldSizeBytes} → ${result.newSizeBytes} bytes)`;
@@ -1768,9 +1909,12 @@ if (!readOnly) {
1768
1909
  "is not renamed — it uses UUID-based naming. Only the human-readable metadata changes.", {
1769
1910
  attachmentId: z.string().describe("UUID of the attachment to rename"),
1770
1911
  newFilename: z.string().describe("The new filename (e.g. 'quarterly-report-q1.pdf')"),
1771
- }, async ({ attachmentId, newFilename }) => {
1772
- if (!accountId)
1773
- return refuseNoAccount("memory-rename-attachment");
1912
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1913
+ }, async ({ attachmentId, newFilename, targetAccountId }) => {
1914
+ const scoped = resolveToolAccount("memory-rename-attachment", targetAccountId);
1915
+ if ("reject" in scoped)
1916
+ return scoped.reject;
1917
+ const accountId = scoped.accountId;
1774
1918
  try {
1775
1919
  const result = await memoryRenameAttachment({ accountId, attachmentId, newFilename });
1776
1920
  if (result.oldFilename === result.newFilename) {
@@ -1874,9 +2018,12 @@ if (!readOnly) {
1874
2018
  .number()
1875
2019
  .optional()
1876
2020
  .describe("Maximum results (default 50 for conversations, 200 for messages)"),
1877
- }, async ({ sessionId, agentType, agentSlug, since, includeMessages, limit }) => {
1878
- if (!accountId)
1879
- return refuseNoAccount("conversation-list");
2021
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
2022
+ }, async ({ sessionId, agentType, agentSlug, since, includeMessages, limit, targetAccountId }) => {
2023
+ const scoped = resolveToolAccount("conversation-list", targetAccountId);
2024
+ if ("reject" in scoped)
2025
+ return scoped.reject;
2026
+ const accountId = scoped.accountId;
1880
2027
  const startTime = Date.now();
1881
2028
  const dbSession = getSession();
1882
2029
  try {
@@ -2055,16 +2202,20 @@ if (!readOnly) {
2055
2202
  .describe("Operator-identity fields written to the OWNS-bound Person. Use canonical `telephone` (NOT `phone` — that is the schema synonym, not the canonical name). Tool throws if the AdminUser-OWNS-Person edge is missing rather than silently no-op."),
2056
2203
  mergeSourceIds: z.array(z.string()).optional()
2057
2204
  .describe("For mode 'merge': preferenceIds of sources to combine into this preference"),
2058
- }, async ({ category, key, value, source, mode, sessionId, profileFields, personFields, mergeSourceIds }) => {
2059
- if (!accountId)
2060
- return refuseNoAccount("profile-update");
2205
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
2206
+ }, async ({ category, key, value, source, mode, sessionId, profileFields, personFields, mergeSourceIds, targetAccountId }) => {
2207
+ const scoped = resolveToolAccount("profile-update", targetAccountId);
2208
+ if ("reject" in scoped)
2209
+ return scoped.reject;
2210
+ const accountId = scoped.accountId;
2211
+ const crossAccount = accountId !== BOOT_ACCOUNT_ID;
2061
2212
  try {
2062
- if (!userId) {
2063
- return { content: [{ type: "text", text: "profile-update requires an authenticated admin session with userId" }], isError: true };
2064
- }
2213
+ const ident = await resolveProfileIdentity("profile-update", crossAccount, accountId, userId);
2214
+ if ("reject" in ident)
2215
+ return ident.reject;
2065
2216
  const result = await profileUpdate({
2066
2217
  accountId,
2067
- userId,
2218
+ userId: ident.userId,
2068
2219
  category,
2069
2220
  key,
2070
2221
  value,
@@ -2074,6 +2225,7 @@ if (!readOnly) {
2074
2225
  profileFields: profileFields,
2075
2226
  personFields,
2076
2227
  mergeSourceIds,
2228
+ bootstrapProfile: crossAccount,
2077
2229
  });
2078
2230
  return {
2079
2231
  content: [{
@@ -2096,14 +2248,18 @@ if (!readOnly) {
2096
2248
  category: z.enum(["communication", "scheduling", "decision", "workflow", "content", "interaction"])
2097
2249
  .describe("Preference category"),
2098
2250
  key: z.string().describe("Preference key to delete"),
2099
- }, async ({ category, key }) => {
2100
- if (!accountId)
2101
- return refuseNoAccount("profile-delete");
2251
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
2252
+ }, async ({ category, key, targetAccountId }) => {
2253
+ const scoped = resolveToolAccount("profile-delete", targetAccountId);
2254
+ if ("reject" in scoped)
2255
+ return scoped.reject;
2256
+ const accountId = scoped.accountId;
2257
+ const crossAccount = accountId !== BOOT_ACCOUNT_ID;
2102
2258
  try {
2103
- if (!userId) {
2104
- return { content: [{ type: "text", text: "profile-delete requires an authenticated admin session with userId" }], isError: true };
2105
- }
2106
- const result = await profileDelete({ accountId, userId, category, key });
2259
+ const ident = await resolveProfileIdentity("profile-delete", crossAccount, accountId, userId);
2260
+ if ("reject" in ident)
2261
+ return ident.reject;
2262
+ const result = await profileDelete({ accountId, userId: ident.userId, category, key });
2107
2263
  return {
2108
2264
  content: [{
2109
2265
  type: "text",
@@ -2130,14 +2286,18 @@ if (!readOnly) {
2130
2286
  "Use detail=true when the owner asks 'what do you know about me?' or when reviewing memory accuracy.", {
2131
2287
  detail: z.boolean().optional().describe("When true, returns per-preference confidence, source, evidence trail, and memory statistics. " +
2132
2288
  "Default false returns a compact summary for system prompt injection."),
2133
- }, async ({ detail }) => {
2134
- if (!accountId)
2135
- return refuseNoAccount("profile-read");
2289
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
2290
+ }, async ({ detail, targetAccountId }) => {
2291
+ const scoped = resolveToolAccount("profile-read", targetAccountId);
2292
+ if ("reject" in scoped)
2293
+ return scoped.reject;
2294
+ const accountId = scoped.accountId;
2295
+ const crossAccount = accountId !== BOOT_ACCOUNT_ID;
2136
2296
  try {
2137
- if (!userId) {
2138
- return { content: [{ type: "text", text: "profile-read requires an authenticated admin session with userId" }], isError: true };
2139
- }
2140
- const result = await profileRead({ accountId, userId, detail: detail ?? false });
2297
+ const ident = await resolveProfileIdentity("profile-read", crossAccount, accountId, userId);
2298
+ if ("reject" in ident)
2299
+ return ident.reject;
2300
+ const result = await profileRead({ accountId, userId: ident.userId, detail: detail ?? false });
2141
2301
  return {
2142
2302
  content: [{ type: "text", text: result.summary }],
2143
2303
  };
@@ -2166,10 +2326,13 @@ if (!readOnly) {
2166
2326
  "Use this when the operator asks 'what has the brain captured today?', 'show me recent mentions of X', or 'what ideas did I leave on the table this week?'.", {
2167
2327
  windowHours: z.number().int().optional().describe("Hours back from now. Default 24. Capped at 720 (30 days)."),
2168
2328
  limit: z.number().int().optional().describe("Max rows returned. Default 50, max 200."),
2169
- }, async ({ windowHours, limit }) => {
2329
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
2330
+ }, async ({ windowHours, limit, targetAccountId }) => {
2170
2331
  try {
2171
- if (!accountId)
2172
- return refuseNoAccount("memory-brain-capture-recent");
2332
+ const scoped = resolveToolAccount("memory-brain-capture-recent", targetAccountId);
2333
+ if ("reject" in scoped)
2334
+ return scoped.reject;
2335
+ const accountId = scoped.accountId;
2173
2336
  const result = await memoryBrainCaptureRecent({ accountId, windowHours, limit });
2174
2337
  return {
2175
2338
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
@@ -2203,8 +2366,10 @@ if (!readOnly) {
2203
2366
  "per-device and survives upgrades.", {
2204
2367
  values: z.array(z.string().min(1)).min(1).describe("Substring values to add. Match is exact substring (case-sensitive) against string-typed properties only."),
2205
2368
  }, async ({ values }) => {
2206
- if (!accountId)
2207
- return refuseNoAccount("graph-prune-denylist-add");
2369
+ const scoped = resolveToolAccount("graph-prune-denylist-add", undefined);
2370
+ if ("reject" in scoped)
2371
+ return scoped.reject;
2372
+ const accountId = scoped.accountId;
2208
2373
  try {
2209
2374
  const result = await graphPruneDenylistAdd(values);
2210
2375
  const lines = [
@@ -2229,8 +2394,10 @@ if (!readOnly) {
2229
2394
  "restored — audit entries on each affected node remain for forensic review.", {
2230
2395
  values: z.array(z.string().min(1)).min(1).describe("Substring values to remove (must match exactly as previously added)."),
2231
2396
  }, async ({ values }) => {
2232
- if (!accountId)
2233
- return refuseNoAccount("graph-prune-denylist-remove");
2397
+ const scoped = resolveToolAccount("graph-prune-denylist-remove", undefined);
2398
+ if ("reject" in scoped)
2399
+ return scoped.reject;
2400
+ const accountId = scoped.accountId;
2234
2401
  try {
2235
2402
  const result = await graphPruneDenylistRemove(values);
2236
2403
  const lines = [
@@ -2257,9 +2424,12 @@ if (!readOnly) {
2257
2424
  "that should not propagate (e.g. wrong-account hostnames, fabricated identifiers).", {
2258
2425
  sessionId: z.string().min(1).describe("The Conversation node's sessionId to scope the expunge to."),
2259
2426
  pattern: z.string().min(1).describe("The substring to scrub from string properties on linked :Memory nodes."),
2260
- }, async ({ sessionId, pattern }) => {
2261
- if (!accountId)
2262
- return refuseNoAccount("conversation-memory-expunge");
2427
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
2428
+ }, async ({ sessionId, pattern, targetAccountId }) => {
2429
+ const scoped = resolveToolAccount("conversation-memory-expunge", targetAccountId);
2430
+ if ("reject" in scoped)
2431
+ return scoped.reject;
2432
+ const accountId = scoped.accountId;
2263
2433
  try {
2264
2434
  const result = await conversationMemoryExpunge({ accountId, sessionId, pattern });
2265
2435
  const lines = [
@@ -2286,8 +2456,10 @@ if (!readOnly) {
2286
2456
  // graph-prune-denylist-list and conversation-search are read-only — available regardless of readOnly mode.
2287
2457
  lifelineTool(server, "graph-prune-denylist-list", "List the current operator-curated deny-list values used by the daily graph-prune cron. " +
2288
2458
  "Returns the substring values, the timestamp of the last edit, and the total count.", {}, () => {
2289
- if (!accountId)
2290
- return refuseNoAccount("graph-prune-denylist-list");
2459
+ const scoped = resolveToolAccount("graph-prune-denylist-list", undefined);
2460
+ if ("reject" in scoped)
2461
+ return scoped.reject;
2462
+ const accountId = scoped.accountId;
2291
2463
  try {
2292
2464
  const result = graphPruneDenylistList();
2293
2465
  if (result.count === 0) {
@@ -2311,9 +2483,12 @@ eagerTool(server, "conversation-search", "Search conversation history using sema
2311
2483
  .number()
2312
2484
  .optional()
2313
2485
  .describe("Maximum number of results (default 10)"),
2314
- }, async ({ query, limit: maxResults }) => {
2315
- if (!accountId)
2316
- return refuseNoAccount("conversation-search");
2486
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
2487
+ }, async ({ query, limit: maxResults, targetAccountId }) => {
2488
+ const scoped = resolveToolAccount("conversation-search", targetAccountId);
2489
+ if ("reject" in scoped)
2490
+ return scoped.reject;
2491
+ const accountId = scoped.accountId;
2317
2492
  try {
2318
2493
  const queryEmbedding = await embed(query);
2319
2494
  const resultLimit = maxResults ?? 10;
@@ -2438,9 +2613,12 @@ lifelineTool(server, "memory-dream-run", "Operator-initiated dream-cycle: graph-
2438
2613
  operatorId: z.string().optional().describe("Identifier of the operator who triggered the run; appears in the report and log line."),
2439
2614
  phases: z.array(z.number().int().min(1).max(7)).optional().describe("Subset of phases to run. Default [1,2,3,4,5,6,7]. Order is fixed."),
2440
2615
  maxEventsPerRun: z.number().int().positive().optional().describe("Phase 4 cap on :TimelineEvent rows scanned per run. Default 200."),
2441
- }, async ({ operatorId, phases, maxEventsPerRun }) => {
2442
- if (!accountId)
2443
- return refuseNoAccount("memory-dream-run");
2616
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
2617
+ }, async ({ operatorId, phases, maxEventsPerRun, targetAccountId }) => {
2618
+ const scoped = resolveToolAccount("memory-dream-run", targetAccountId);
2619
+ if ("reject" in scoped)
2620
+ return scoped.reject;
2621
+ const accountId = scoped.accountId;
2444
2622
  try {
2445
2623
  const result = await memoryDreamRun({ accountId, operatorId, phases, maxEventsPerRun });
2446
2624
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
@@ -2455,9 +2633,12 @@ lifelineTool(server, "memory-dream-run", "Operator-initiated dream-cycle: graph-
2455
2633
  lifelineTool(server, "memory-review-queue", "Read-only summary of the dream-cycle's review queues (:OrphanCandidate, :CitationProposal). " +
2456
2634
  "Returns per-queue counts and a small sample so the operator can see whether to triage.", {
2457
2635
  sampleSize: z.number().int().positive().optional().describe("Sample rows per queue. Default 5, max 25."),
2458
- }, async ({ sampleSize }) => {
2459
- if (!accountId)
2460
- return refuseNoAccount("memory-review-queue");
2636
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
2637
+ }, async ({ sampleSize, targetAccountId }) => {
2638
+ const scoped = resolveToolAccount("memory-review-queue", targetAccountId);
2639
+ if ("reject" in scoped)
2640
+ return scoped.reject;
2641
+ const accountId = scoped.accountId;
2461
2642
  try {
2462
2643
  const result = await memoryReviewQueue({ accountId, sampleSize });
2463
2644
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };