@rubytech/create-maxy-code 0.1.411 → 0.1.412

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 (147) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/account-scope/dist/index.d.ts +27 -0
  3. package/payload/platform/lib/account-scope/dist/index.d.ts.map +1 -0
  4. package/payload/platform/lib/account-scope/dist/index.js +33 -0
  5. package/payload/platform/lib/account-scope/dist/index.js.map +1 -0
  6. package/payload/platform/lib/account-scope/src/index.ts +43 -0
  7. package/payload/platform/lib/account-scope/tsconfig.json +5 -0
  8. package/payload/platform/package.json +2 -2
  9. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  10. package/payload/platform/plugins/admin/hooks/lib/maxy-mcp-plugins.txt +1 -0
  11. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +35 -1
  12. package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +2 -0
  13. package/payload/platform/plugins/docs/references/cross-account-authority.md +23 -0
  14. package/payload/platform/plugins/docs/references/memory-guide.md +6 -0
  15. package/payload/platform/plugins/filesystem/.claude-plugin/plugin.json +21 -0
  16. package/payload/platform/plugins/filesystem/PLUGIN.md +63 -0
  17. package/payload/platform/plugins/filesystem/lib/mcp-spawn-tee/index.js +193 -0
  18. package/payload/platform/plugins/filesystem/lib/mcp-spawn-tee/package.json +3 -0
  19. package/payload/platform/plugins/filesystem/mcp/dist/index.d.ts +2 -0
  20. package/payload/platform/plugins/filesystem/mcp/dist/index.d.ts.map +1 -0
  21. package/payload/platform/plugins/filesystem/mcp/dist/index.js +91 -0
  22. package/payload/platform/plugins/filesystem/mcp/dist/index.js.map +1 -0
  23. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.d.ts +2 -0
  24. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.d.ts.map +1 -0
  25. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.js +89 -0
  26. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.js.map +1 -0
  27. package/payload/platform/plugins/filesystem/mcp/dist/lib/result.d.ts +12 -0
  28. package/payload/platform/plugins/filesystem/mcp/dist/lib/result.d.ts.map +1 -0
  29. package/payload/platform/plugins/filesystem/mcp/dist/lib/result.js +6 -0
  30. package/payload/platform/plugins/filesystem/mcp/dist/lib/result.js.map +1 -0
  31. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts +45 -0
  32. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts.map +1 -0
  33. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js +86 -0
  34. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js.map +1 -0
  35. package/payload/platform/plugins/filesystem/mcp/dist/lib/walk.d.ts +11 -0
  36. package/payload/platform/plugins/filesystem/mcp/dist/lib/walk.d.ts.map +1 -0
  37. package/payload/platform/plugins/filesystem/mcp/dist/lib/walk.js +64 -0
  38. package/payload/platform/plugins/filesystem/mcp/dist/lib/walk.js.map +1 -0
  39. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.d.ts +2 -0
  40. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.d.ts.map +1 -0
  41. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.js +94 -0
  42. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.js.map +1 -0
  43. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-edit.d.ts +8 -0
  44. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-edit.d.ts.map +1 -0
  45. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-edit.js +40 -0
  46. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-edit.js.map +1 -0
  47. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-glob.d.ts +6 -0
  48. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-glob.d.ts.map +1 -0
  49. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-glob.js +13 -0
  50. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-glob.js.map +1 -0
  51. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-grep.d.ts +7 -0
  52. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-grep.d.ts.map +1 -0
  53. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-grep.js +71 -0
  54. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-grep.js.map +1 -0
  55. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-list.d.ts +6 -0
  56. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-list.d.ts.map +1 -0
  57. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-list.js +26 -0
  58. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-list.js.map +1 -0
  59. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-read.d.ts +6 -0
  60. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-read.d.ts.map +1 -0
  61. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-read.js +28 -0
  62. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-read.js.map +1 -0
  63. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-write.d.ts +7 -0
  64. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-write.d.ts.map +1 -0
  65. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-write.js +25 -0
  66. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-write.js.map +1 -0
  67. package/payload/platform/plugins/filesystem/mcp/package.json +21 -0
  68. package/payload/platform/plugins/filesystem/mcp/vitest.config.ts +8 -0
  69. package/payload/platform/plugins/memory/PLUGIN.md +6 -0
  70. package/payload/platform/plugins/memory/mcp/dist/index-testable.d.ts +34 -0
  71. package/payload/platform/plugins/memory/mcp/dist/index-testable.d.ts.map +1 -0
  72. package/payload/platform/plugins/memory/mcp/dist/index-testable.js +60 -0
  73. package/payload/platform/plugins/memory/mcp/dist/index-testable.js.map +1 -0
  74. package/payload/platform/plugins/memory/mcp/dist/index.js +275 -133
  75. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  76. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.d.ts +2 -0
  77. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.d.ts.map +1 -0
  78. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.js +65 -0
  79. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.js.map +1 -0
  80. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.d.ts +2 -0
  81. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.d.ts.map +1 -0
  82. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.js +54 -0
  83. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.js.map +1 -0
  84. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  85. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +7 -0
  86. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  87. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.d.ts.map +1 -1
  88. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.js +6 -1
  89. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.js.map +1 -1
  90. package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.d.ts +7 -1
  91. package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.d.ts.map +1 -1
  92. package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.js +7 -2
  93. package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.js.map +1 -1
  94. package/payload/platform/services/claude-session-manager/dist/config.d.ts +37 -0
  95. package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
  96. package/payload/platform/services/claude-session-manager/dist/config.js +34 -1
  97. package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
  98. package/payload/platform/services/claude-session-manager/dist/cross-account-census.d.ts +34 -0
  99. package/payload/platform/services/claude-session-manager/dist/cross-account-census.d.ts.map +1 -0
  100. package/payload/platform/services/claude-session-manager/dist/cross-account-census.js +98 -0
  101. package/payload/platform/services/claude-session-manager/dist/cross-account-census.js.map +1 -0
  102. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  103. package/payload/platform/services/claude-session-manager/dist/http-server.js +10 -0
  104. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  105. package/payload/platform/services/claude-session-manager/dist/index.js +17 -0
  106. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  107. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  108. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +25 -0
  109. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  110. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +7 -0
  111. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -1
  112. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +8 -0
  113. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -1
  114. package/payload/server/{chunk-ZYS5V5QG.js → chunk-3Z5WE53X.js} +4 -0
  115. package/payload/server/maxy-edge.js +1 -1
  116. package/payload/server/public/assets/{AdminLoginScreens-BRQgI2lW.js → AdminLoginScreens-CYPTMmTk.js} +1 -1
  117. package/payload/server/public/assets/{AdminShell-By9n5icG.js → AdminShell-CZH_EZIy.js} +1 -1
  118. package/payload/server/public/assets/{Checkbox-DjEWgYu1.js → Checkbox-B0pbYJDQ.js} +1 -1
  119. package/payload/server/public/assets/{Transcript-BejWvOaO.js → Transcript-CYaAPBFX.js} +1 -1
  120. package/payload/server/public/assets/{admin-BFEMryQm.js → admin-BKDMSRto.js} +1 -1
  121. package/payload/server/public/assets/{browser-CMJMEUz6.js → browser-CxeMDDZA.js} +1 -1
  122. package/payload/server/public/assets/{calendar-C6ZGlHEP.js → calendar-DiIiTY9-.js} +1 -1
  123. package/payload/server/public/assets/chat-C56Oqvt1.js +1 -0
  124. package/payload/server/public/assets/chevron-left-Da-iZGi_.js +1 -0
  125. package/payload/server/public/assets/data-ZUdT44Ba.js +1 -0
  126. package/payload/server/public/assets/{graph-DIrXtPLv.js → graph-DC3dwnMZ.js} +1 -1
  127. package/payload/server/public/assets/{graph-labels-BtPf1Jom.js → graph-labels-Csnovm_c.js} +1 -1
  128. package/payload/server/public/assets/{maximize-2-DeJ4K8kL.js → maximize-2-WRUJsg5n.js} +1 -1
  129. package/payload/server/public/assets/{operator-B8QhqzLI.js → operator-BpzeHTAw.js} +1 -1
  130. package/payload/server/public/assets/{page-iFOy0dFx.js → page-8jJOxx6y.js} +1 -1
  131. package/payload/server/public/assets/{page-Df6KYkf4.js → page-IQDLdebG.js} +1 -1
  132. package/payload/server/public/assets/{public-BrUIRQZd.js → public-Dqg7jwjq.js} +1 -1
  133. package/payload/server/public/assets/{rotate-ccw-CYfntpwu.js → rotate-ccw-BayMWwfc.js} +1 -1
  134. package/payload/server/public/assets/{useSubAccountSwitcher-Cvnc_fwQ.css → useSubAccountSwitcher-CRZfyJXR.css} +1 -1
  135. package/payload/server/public/browser.html +4 -4
  136. package/payload/server/public/calendar.html +5 -5
  137. package/payload/server/public/chat.html +9 -9
  138. package/payload/server/public/data.html +8 -8
  139. package/payload/server/public/graph.html +9 -9
  140. package/payload/server/public/index.html +11 -11
  141. package/payload/server/public/operator.html +11 -11
  142. package/payload/server/public/public.html +9 -9
  143. package/payload/server/server.js +36 -11
  144. package/payload/server/public/assets/chat-B7s62Nnp.js +0 -1
  145. package/payload/server/public/assets/chevron-left-Dfjcaiyr.js +0 -1
  146. package/payload/server/public/assets/data-741ZbBwR.js +0 -1
  147. /package/payload/server/public/assets/{useSubAccountSwitcher-BK_KfOLC.js → useSubAccountSwitcher-C9aLg8yI.js} +0 -0
@@ -59,6 +59,10 @@ import { getSession, closeDriver } from "./lib/neo4j.js";
59
59
  import { embed } from "./lib/embeddings.js";
60
60
  import { notTrashed } from "../../../../lib/graph-trash/dist/index.js";
61
61
  import { lifelineTool } from "../../../../lib/mcp-lifeline/dist/index.js";
62
+ import { resolve } from "node:path";
63
+ import { enumerateValidAccountIds } from "../../../../lib/account-enumeration/dist/index.js";
64
+ import { readHouseAdminScope } from "../../../../lib/account-scope/dist/index.js";
65
+ import { makeResolveToolAccount } from "./index-testable.js";
62
66
  const server = new McpServer({
63
67
  name: "maxy-memory",
64
68
  version: "0.1.0",
@@ -74,18 +78,32 @@ const accountId = process.env.ACCOUNT_ID ?? null;
74
78
  // accountId`; `agentSlug` empty on a specialist spawn (provenance lost);
75
79
  // `agentScope` non-empty on the admin operator spawn (search wrongly scoped).
76
80
  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.`,
84
- },
85
- ],
86
- isError: true,
87
- };
88
- }
81
+ // The no-account refusal (formerly `refuseNoAccount`) is now handled inside
82
+ // `resolveToolAccount` / `makeResolveToolAccount` (index-testable.ts), which
83
+ // preserves the same message when the boot ACCOUNT_ID is null. Task 1455.
84
+ // ---------------------------------------------------------------------------
85
+ // Cross-account authority (Task 1455). A house-scoped admin session may set an
86
+ // optional `targetAccountId` on any account-scoped tool to read/write a
87
+ // sub-account's subgraph; omitting it is byte-identical to own-account.
88
+ //
89
+ // `houseScope` rides the spawn env (Task 1440, `HOUSE_ADMIN_SCOPE`). `accountsDir`
90
+ // mirrors neo4j.ts's platform-root resolution (PLATFORM_ROOT env, else relative
91
+ // to the compiled file): from dist/index.js, `../../../..` is `platform/`, and
92
+ // accounts live at `<platform>/../data/accounts`. `enumerateValidAccountIds`
93
+ // returns [] (never throws) on a missing dir, so a misresolved path fails CLOSED
94
+ // — every target rejects invalid-target. The boot line makes that visible rather
95
+ // than silent: a zero `validAccounts` on a provisioned install is the alarm.
96
+ const houseScope = readHouseAdminScope();
97
+ const accountsDir = resolve(process.env.PLATFORM_ROOT ?? resolve(import.meta.dirname, "../../../.."), "..", "data/accounts");
98
+ process.stderr.write(`[xacct] boot houseScope=${houseScope ? "yes" : "no"} accountsDir=${accountsDir} validAccounts=${enumerateValidAccountIds(accountsDir).length}\n`);
99
+ const resolveToolAccount = makeResolveToolAccount({
100
+ accountId,
101
+ houseScope,
102
+ isValid: (id) => enumerateValidAccountIds(accountsDir).includes(id),
103
+ });
104
+ // Single source for the operator-facing targetAccountId description — the LLM
105
+ // reads this verbatim, so it must not drift across the 35 targetable tools.
106
+ 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.";
89
107
  // Load the markdown schema sidecar once at startup. Every memory-write call
90
108
  // reads its required-property and synonym maps. If loading fails we throw —
91
109
  // a memory server running with a broken schema is worse than one that fails
@@ -242,9 +260,12 @@ eagerTool(server, "memory-search", "Search the knowledge graph using semantic ve
242
260
  .enum(["entity", "temporal", "event", "general", "none"])
243
261
  .optional()
244
262
  .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");
263
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
264
+ }, async ({ query, labels, limit, expandHops, keywords, keywordMatch, agentSlug: toolAgentSlug, fields, retrievalClass, targetAccountId }) => {
265
+ const scoped = resolveToolAccount("memory-search", targetAccountId);
266
+ if ("reject" in scoped)
267
+ return scoped.reject;
268
+ const accountId = scoped.accountId;
248
269
  // AGENT_SLUG_SCOPE takes precedence over the tool param (security: public
249
270
  // agent server-level enforcement). Empty for the admin operator, so admin
250
271
  // falls through to the optional tool param (default: unscoped).
@@ -358,9 +379,12 @@ eagerTool(server, "memory-lookup-by-name", "Deterministic by-name entity lookup.
358
379
  .number()
359
380
  .optional()
360
381
  .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");
382
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
383
+ }, async ({ name, labels, limit, targetAccountId }) => {
384
+ const scoped = resolveToolAccount("memory-lookup-by-name", targetAccountId);
385
+ if ("reject" in scoped)
386
+ return scoped.reject;
387
+ const accountId = scoped.accountId;
364
388
  const effectiveAgentSlug = envAgentScopeSlug;
365
389
  try {
366
390
  const hits = await memoryLookupByName({
@@ -425,9 +449,12 @@ lifelineTool(server, "conversation-archive-list-chunks", "Page through the :Sect
425
449
  .max(20)
426
450
  .optional()
427
451
  .describe("Chunks per page (1..20). Default 5."),
428
- }, async ({ archiveElementId, chunkOffset, chunkLimit }) => {
429
- if (!accountId)
430
- return refuseNoAccount("conversation-archive-list-chunks");
452
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
453
+ }, async ({ archiveElementId, chunkOffset, chunkLimit, targetAccountId }) => {
454
+ const scoped = resolveToolAccount("conversation-archive-list-chunks", targetAccountId);
455
+ if ("reject" in scoped)
456
+ return scoped.reject;
457
+ const accountId = scoped.accountId;
431
458
  try {
432
459
  const result = await conversationArchiveListChunks({
433
460
  accountId,
@@ -504,9 +531,12 @@ lifelineTool(server, "conversation-archive-derive-insights", "Convert one chunk'
504
531
  .describe("Claims the dispatched specialist emitted in-turn from the chunk body. Zero is valid — pass [] to record an empty-chunk pass."),
505
532
  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
533
  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");
534
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
535
+ }, async ({ archiveElementId, chunkElementId, claims, inlineRewrite, inlineRewriteThreshold, targetAccountId }) => {
536
+ const scoped = resolveToolAccount("conversation-archive-derive-insights", targetAccountId);
537
+ if ("reject" in scoped)
538
+ return scoped.reject;
539
+ const accountId = scoped.accountId;
510
540
  try {
511
541
  const result = await conversationArchiveDeriveInsights({
512
542
  accountId,
@@ -583,9 +613,12 @@ lifelineTool(server, "conversation-archive-enrich-rejection", "Record or undo a
583
613
  mode: z
584
614
  .enum(["record", "undo"])
585
615
  .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");
616
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
617
+ }, async ({ archiveElementId, chunkElementId, kind, contentHash, mode, targetAccountId }) => {
618
+ const scoped = resolveToolAccount("conversation-archive-enrich-rejection", targetAccountId);
619
+ if ("reject" in scoped)
620
+ return scoped.reject;
621
+ const accountId = scoped.accountId;
589
622
  try {
590
623
  const result = await conversationArchiveEnrichRejection({
591
624
  accountId,
@@ -629,9 +662,12 @@ eagerTool(server, "memory-report-read-latest", "Return the most recent :Report n
629
662
  .max(20)
630
663
  .optional()
631
664
  .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");
665
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
666
+ }, async ({ keyword, limit, targetAccountId }) => {
667
+ const scoped = resolveToolAccount("memory-report-read-latest", targetAccountId);
668
+ if ("reject" in scoped)
669
+ return scoped.reject;
670
+ const accountId = scoped.accountId;
635
671
  try {
636
672
  const result = await memoryReportReadLatest({ accountId, keyword, limit });
637
673
  if (result.results.length === 0) {
@@ -686,9 +722,12 @@ eagerTool(server, "memory-report-list", "List :Report nodes, newest first, with
686
722
  .min(0)
687
723
  .optional()
688
724
  .describe("Zero-based offset for pagination. Default 0."),
689
- }, async ({ keyword, sourceWorkflow, limit, offset }) => {
690
- if (!accountId)
691
- return refuseNoAccount("memory-report-list");
725
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
726
+ }, async ({ keyword, sourceWorkflow, limit, offset, targetAccountId }) => {
727
+ const scoped = resolveToolAccount("memory-report-list", targetAccountId);
728
+ if ("reject" in scoped)
729
+ return scoped.reject;
730
+ const accountId = scoped.accountId;
692
731
  try {
693
732
  const result = await memoryReportList({
694
733
  accountId,
@@ -736,9 +775,12 @@ eagerTool(server, "memory-compiled-truth-history", "Return the audit history of
736
775
  nodeId: z
737
776
  .string()
738
777
  .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");
778
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
779
+ }, async ({ nodeId, targetAccountId }) => {
780
+ const scoped = resolveToolAccount("memory-compiled-truth-history", targetAccountId);
781
+ if ("reject" in scoped)
782
+ return scoped.reject;
783
+ const accountId = scoped.accountId;
742
784
  try {
743
785
  const result = await memoryCompiledTruthHistory({ accountId, nodeId });
744
786
  if (result.revisions.length === 0) {
@@ -793,9 +835,12 @@ if (!readOnly) {
793
835
  .string()
794
836
  .optional()
795
837
  .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");
838
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
839
+ }, async ({ labels, properties, scope, relationships, producedByTaskId, targetAccountId }) => {
840
+ const scoped = resolveToolAccount("memory-write", targetAccountId);
841
+ if ("reject" in scoped)
842
+ return scoped.reject;
843
+ const accountId = scoped.accountId;
799
844
  try {
800
845
  const result = await memoryWrite({
801
846
  labels,
@@ -853,9 +898,12 @@ if (!readOnly) {
853
898
  .string()
854
899
  .optional()
855
900
  .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");
901
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
902
+ }, async ({ title, body, keywords, sourceWorkflow, occurredAt, targetAccountId }) => {
903
+ const scoped = resolveToolAccount("memory-report-write", targetAccountId);
904
+ if ("reject" in scoped)
905
+ return scoped.reject;
906
+ const accountId = scoped.accountId;
859
907
  try {
860
908
  const result = await memoryReportWrite({
861
909
  accountId,
@@ -896,9 +944,12 @@ if (!readOnly) {
896
944
  properties: z
897
945
  .record(z.string(), z.unknown())
898
946
  .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");
947
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
948
+ }, async ({ label, name, properties, targetAccountId }) => {
949
+ const scoped = resolveToolAccount("memory-update-by-name", targetAccountId);
950
+ if ("reject" in scoped)
951
+ return scoped.reject;
952
+ const accountId = scoped.accountId;
902
953
  try {
903
954
  const result = await memoryUpdateByName({
904
955
  label,
@@ -937,9 +988,12 @@ if (!readOnly) {
937
988
  properties: z
938
989
  .record(z.string(), z.unknown())
939
990
  .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");
991
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
992
+ }, async ({ nodeId, properties, targetAccountId }) => {
993
+ const scoped = resolveToolAccount("memory-update", targetAccountId);
994
+ if ("reject" in scoped)
995
+ return scoped.reject;
996
+ const accountId = scoped.accountId;
943
997
  try {
944
998
  const result = await memoryUpdate({
945
999
  nodeId,
@@ -978,9 +1032,12 @@ if (!readOnly) {
978
1032
  toId: z.string().describe("elementId of the target node (from memory-search results)."),
979
1033
  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
1034
  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");
1035
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1036
+ }, async ({ action, fromId, toId, relationshipType, properties, targetAccountId }) => {
1037
+ const scoped = resolveToolAccount("memory-edge", targetAccountId);
1038
+ if ("reject" in scoped)
1039
+ return scoped.reject;
1040
+ const accountId = scoped.accountId;
984
1041
  try {
985
1042
  const result = await memoryEdge({
986
1043
  action,
@@ -1014,9 +1071,13 @@ if (!readOnly) {
1014
1071
  };
1015
1072
  }
1016
1073
  });
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");
1074
+ 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.", {
1075
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1076
+ }, async ({ targetAccountId }) => {
1077
+ const scoped = resolveToolAccount("session-compact-status", targetAccountId);
1078
+ if ("reject" in scoped)
1079
+ return scoped.reject;
1080
+ const accountId = scoped.accountId;
1020
1081
  const session = getSession();
1021
1082
  try {
1022
1083
  const result = await session.run(`MATCH (n:CreativeWork {accountId: $accountId})
@@ -1043,8 +1104,10 @@ if (!readOnly) {
1043
1104
  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
1105
  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
1106
  }, async ({ content }) => {
1046
- if (!accountId)
1047
- return refuseNoAccount("session-compact");
1107
+ const scoped = resolveToolAccount("session-compact", undefined);
1108
+ if ("reject" in scoped)
1109
+ return scoped.reject;
1110
+ const accountId = scoped.accountId;
1048
1111
  try {
1049
1112
  // Write doctrine: session summaries link back to the
1050
1113
  // Conversation they summarise, so an operator searching a session's
@@ -1103,9 +1166,13 @@ if (!readOnly) {
1103
1166
  };
1104
1167
  }
1105
1168
  });
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");
1169
+ eagerTool(server, "memory-reindex", "Rebuild vector embeddings for all nodes missing embeddings. Run after seeding or bulk updates.", {
1170
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1171
+ }, async ({ targetAccountId }) => {
1172
+ const scoped = resolveToolAccount("memory-reindex", targetAccountId);
1173
+ if ("reject" in scoped)
1174
+ return scoped.reject;
1175
+ const accountId = scoped.accountId;
1109
1176
  try {
1110
1177
  const result = await memoryReindex(accountId);
1111
1178
  return {
@@ -1140,8 +1207,10 @@ if (!readOnly) {
1140
1207
  mimeType: z.string().describe("MIME type of the file"),
1141
1208
  attachmentId: z.string().describe("UUID of the file attachment — used as cache key for memory-ingest"),
1142
1209
  }, async ({ storagePath, filename, mimeType, attachmentId }) => {
1143
- if (!accountId)
1144
- return refuseNoAccount("memory-ingest-extract");
1210
+ const scoped = resolveToolAccount("memory-ingest-extract", undefined);
1211
+ if ("reject" in scoped)
1212
+ return scoped.reject;
1213
+ const accountId = scoped.accountId;
1145
1214
  try {
1146
1215
  const result = await memoryIngestExtract({ storagePath, filename, mimeType, attachmentId });
1147
1216
  return {
@@ -1231,9 +1300,12 @@ if (!readOnly) {
1231
1300
  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
1301
  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
1302
  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");
1303
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1304
+ }, 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 }) => {
1305
+ const scoped = resolveToolAccount("memory-ingest", targetAccountId);
1306
+ if ("reject" in scoped)
1307
+ return scoped.reject;
1308
+ const accountId = scoped.accountId;
1237
1309
  try {
1238
1310
  const result = await memoryIngest({
1239
1311
  accountId,
@@ -1296,9 +1368,12 @@ if (!readOnly) {
1296
1368
  });
1297
1369
  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
1370
  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");
1371
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1372
+ }, async ({ attachmentId, targetAccountId }) => {
1373
+ const scoped = resolveToolAccount("kd-classify", targetAccountId);
1374
+ if ("reject" in scoped)
1375
+ return scoped.reject;
1376
+ const accountId = scoped.accountId;
1302
1377
  try {
1303
1378
  const text = await kdClassify({ accountId, attachmentId });
1304
1379
  return { content: [{ type: "text", text }] };
@@ -1336,9 +1411,12 @@ if (!readOnly) {
1336
1411
  .string()
1337
1412
  .optional()
1338
1413
  .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");
1414
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1415
+ }, async ({ archiveType, ownerNodeId, rows, sessionId: sessionIdOverride, targetAccountId }) => {
1416
+ const scoped = resolveToolAccount("memory-archive-write", targetAccountId);
1417
+ if ("reject" in scoped)
1418
+ return scoped.reject;
1419
+ const accountId = scoped.accountId;
1342
1420
  try {
1343
1421
  const result = await memoryArchiveWrite({
1344
1422
  archiveType,
@@ -1418,9 +1496,12 @@ if (!readOnly) {
1418
1496
  .string()
1419
1497
  .optional()
1420
1498
  .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");
1499
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1500
+ }, async ({ ownerNodeId, extractedDir, mode, importId, filters, resolutions, selectiveIngestThreshold, sessionId: sessionIdOverride, targetAccountId, }) => {
1501
+ const scoped = resolveToolAccount("obsidian-vault-import", targetAccountId);
1502
+ if ("reject" in scoped)
1503
+ return scoped.reject;
1504
+ const accountId = scoped.accountId;
1424
1505
  try {
1425
1506
  const result = await obsidianVaultImport({
1426
1507
  accountId,
@@ -1469,9 +1550,12 @@ if (!readOnly) {
1469
1550
  "When present, used verbatim as `KnowledgeDocument.name` (capped at 200 chars). " +
1470
1551
  "When absent, the server falls back to the markdown H1, then a URL-derived slug, then the URL."),
1471
1552
  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");
1553
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1554
+ }, async ({ url, content, title, scope, targetAccountId }) => {
1555
+ const scoped = resolveToolAccount("memory-ingest-web", targetAccountId);
1556
+ if ("reject" in scoped)
1557
+ return scoped.reject;
1558
+ const accountId = scoped.accountId;
1475
1559
  try {
1476
1560
  const result = await memoryIngestWeb({
1477
1561
  accountId,
@@ -1518,9 +1602,12 @@ if (!readOnly) {
1518
1602
  agentType: z.enum(["admin", "public"]).optional().describe("Scope to conversations matching c.agentType. Omit to scope across both."),
1519
1603
  limit: z.number().int().positive().optional().describe("Max candidates to return (default 200, hard cap 1000)."),
1520
1604
  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");
1605
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1606
+ }, async ({ filter, agentType, limit, dryRun, targetAccountId }) => {
1607
+ const scoped = resolveToolAccount("memory-find-candidates", targetAccountId);
1608
+ if ("reject" in scoped)
1609
+ return scoped.reject;
1610
+ const accountId = scoped.accountId;
1524
1611
  try {
1525
1612
  const result = await memoryFindCandidates({ accountId, filter, agentType, limit, dryRun });
1526
1613
  if (result.items.length === 0) {
@@ -1561,9 +1648,12 @@ if (!readOnly) {
1561
1648
  "ranked discovery on general labels. Use this before any branch that claims a " +
1562
1649
  "singleton root is missing.", {
1563
1650
  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");
1651
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1652
+ }, async ({ label, targetAccountId }) => {
1653
+ const scoped = resolveToolAccount("memory-node-exists", targetAccountId);
1654
+ if ("reject" in scoped)
1655
+ return scoped.reject;
1656
+ const accountId = scoped.accountId;
1567
1657
  try {
1568
1658
  const result = await memoryNodeExists({ accountId, label });
1569
1659
  const payload = JSON.stringify(result);
@@ -1588,9 +1678,12 @@ if (!readOnly) {
1588
1678
  filterToken: z.string().optional().describe("Opaque token from memory-find-candidates. Required for bulk-selector flows; the server re-runs the " +
1589
1679
  "canonical predicate on this elementId before trashing. Omit for single-node deletes where the caller " +
1590
1680
  "chose the target directly (e.g. from a memory-search result)."),
1591
- }, async ({ elementId, reason, filterToken }) => {
1592
- if (!accountId)
1593
- return refuseNoAccount("memory-delete");
1681
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1682
+ }, async ({ elementId, reason, filterToken, targetAccountId }) => {
1683
+ const scoped = resolveToolAccount("memory-delete", targetAccountId);
1684
+ if ("reject" in scoped)
1685
+ return scoped.reject;
1686
+ const accountId = scoped.accountId;
1594
1687
  try {
1595
1688
  const result = await memoryDelete({ accountId, elementId, reason, filterToken });
1596
1689
  const head = result.alreadyTrashed
@@ -1617,9 +1710,12 @@ if (!readOnly) {
1617
1710
  "node needs back (e.g. a fresh LocalBusiness was created while the old one was in trash) — " +
1618
1711
  "the error names the conflicting elementId so the caller can decide how to resolve.", {
1619
1712
  elementId: z.string().describe("Neo4j elementId of the trashed node to restore."),
1620
- }, async ({ elementId }) => {
1621
- if (!accountId)
1622
- return refuseNoAccount("memory-restore");
1713
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1714
+ }, async ({ elementId, targetAccountId }) => {
1715
+ const scoped = resolveToolAccount("memory-restore", targetAccountId);
1716
+ if ("reject" in scoped)
1717
+ return scoped.reject;
1718
+ const accountId = scoped.accountId;
1623
1719
  try {
1624
1720
  const result = await memoryRestore({ accountId, elementId });
1625
1721
  const cascade = result.cascade.sections + result.cascade.chunks > 0
@@ -1647,9 +1743,12 @@ if (!readOnly) {
1647
1743
  graceDays: z.number().int().min(0).optional().describe("Minimum age in days a trashed node must reach before it's eligible. Default 30."),
1648
1744
  dryRun: z.boolean().optional().describe("When true, lists candidates without deleting. Default false."),
1649
1745
  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");
1746
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1747
+ }, async ({ graceDays, dryRun, labels, targetAccountId }) => {
1748
+ const scoped = resolveToolAccount("memory-empty-trash", targetAccountId);
1749
+ if ("reject" in scoped)
1750
+ return scoped.reject;
1751
+ const accountId = scoped.accountId;
1653
1752
  try {
1654
1753
  const result = await memoryEmptyTrash({ accountId, graceDays, dryRun, labels });
1655
1754
  const lines = [];
@@ -1673,9 +1772,13 @@ if (!readOnly) {
1673
1772
  });
1674
1773
  lifelineTool(server, "memory-list-attachments", "List stored file attachments for this account with their ingestion status. " +
1675
1774
  "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");
1775
+ "into the knowledge graph. Does not include web-ingested documents (those have no file on disk).", {
1776
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1777
+ }, async ({ targetAccountId }) => {
1778
+ const scoped = resolveToolAccount("memory-list-attachments", targetAccountId);
1779
+ if ("reject" in scoped)
1780
+ return scoped.reject;
1781
+ const accountId = scoped.accountId;
1679
1782
  try {
1680
1783
  const result = await memoryListAttachments(accountId);
1681
1784
  if (result.entries.length === 0) {
@@ -1713,9 +1816,12 @@ if (!readOnly) {
1713
1816
  "extracted text for PDFs (via pdftotext), and metadata + HTTP URL for images. " +
1714
1817
  "Use this to show the user what's in a file or to prepare content for editing.", {
1715
1818
  attachmentId: z.string().describe("UUID of the attachment to read"),
1716
- }, async ({ attachmentId }) => {
1717
- if (!accountId)
1718
- return refuseNoAccount("memory-read-attachment");
1819
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1820
+ }, async ({ attachmentId, targetAccountId }) => {
1821
+ const scoped = resolveToolAccount("memory-read-attachment", targetAccountId);
1822
+ if ("reject" in scoped)
1823
+ return scoped.reject;
1824
+ const accountId = scoped.accountId;
1719
1825
  try {
1720
1826
  const result = await memoryReadAttachment(accountId, attachmentId);
1721
1827
  let text = `**${result.filename}** (${result.mimeType}, ${result.sizeBytes} bytes)\nStored: ${result.storedAt}\n`;
@@ -1740,9 +1846,12 @@ if (!readOnly) {
1740
1846
  "Only text/plain and text/markdown files are editable — PDFs and images are rejected.", {
1741
1847
  attachmentId: z.string().describe("UUID of the attachment to edit"),
1742
1848
  content: z.string().describe("The full replacement text content"),
1743
- }, async ({ attachmentId, content }) => {
1744
- if (!accountId)
1745
- return refuseNoAccount("memory-edit-attachment");
1849
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1850
+ }, async ({ attachmentId, content, targetAccountId }) => {
1851
+ const scoped = resolveToolAccount("memory-edit-attachment", targetAccountId);
1852
+ if ("reject" in scoped)
1853
+ return scoped.reject;
1854
+ const accountId = scoped.accountId;
1746
1855
  try {
1747
1856
  const result = await memoryEditAttachment({ accountId, attachmentId, content });
1748
1857
  let text = `Edited: ${result.filename} (${result.oldSizeBytes} → ${result.newSizeBytes} bytes)`;
@@ -1768,9 +1877,12 @@ if (!readOnly) {
1768
1877
  "is not renamed — it uses UUID-based naming. Only the human-readable metadata changes.", {
1769
1878
  attachmentId: z.string().describe("UUID of the attachment to rename"),
1770
1879
  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");
1880
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1881
+ }, async ({ attachmentId, newFilename, targetAccountId }) => {
1882
+ const scoped = resolveToolAccount("memory-rename-attachment", targetAccountId);
1883
+ if ("reject" in scoped)
1884
+ return scoped.reject;
1885
+ const accountId = scoped.accountId;
1774
1886
  try {
1775
1887
  const result = await memoryRenameAttachment({ accountId, attachmentId, newFilename });
1776
1888
  if (result.oldFilename === result.newFilename) {
@@ -1874,9 +1986,12 @@ if (!readOnly) {
1874
1986
  .number()
1875
1987
  .optional()
1876
1988
  .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");
1989
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
1990
+ }, async ({ sessionId, agentType, agentSlug, since, includeMessages, limit, targetAccountId }) => {
1991
+ const scoped = resolveToolAccount("conversation-list", targetAccountId);
1992
+ if ("reject" in scoped)
1993
+ return scoped.reject;
1994
+ const accountId = scoped.accountId;
1880
1995
  const startTime = Date.now();
1881
1996
  const dbSession = getSession();
1882
1997
  try {
@@ -2056,8 +2171,10 @@ if (!readOnly) {
2056
2171
  mergeSourceIds: z.array(z.string()).optional()
2057
2172
  .describe("For mode 'merge': preferenceIds of sources to combine into this preference"),
2058
2173
  }, async ({ category, key, value, source, mode, sessionId, profileFields, personFields, mergeSourceIds }) => {
2059
- if (!accountId)
2060
- return refuseNoAccount("profile-update");
2174
+ const scoped = resolveToolAccount("profile-update", undefined);
2175
+ if ("reject" in scoped)
2176
+ return scoped.reject;
2177
+ const accountId = scoped.accountId;
2061
2178
  try {
2062
2179
  if (!userId) {
2063
2180
  return { content: [{ type: "text", text: "profile-update requires an authenticated admin session with userId" }], isError: true };
@@ -2097,8 +2214,10 @@ if (!readOnly) {
2097
2214
  .describe("Preference category"),
2098
2215
  key: z.string().describe("Preference key to delete"),
2099
2216
  }, async ({ category, key }) => {
2100
- if (!accountId)
2101
- return refuseNoAccount("profile-delete");
2217
+ const scoped = resolveToolAccount("profile-delete", undefined);
2218
+ if ("reject" in scoped)
2219
+ return scoped.reject;
2220
+ const accountId = scoped.accountId;
2102
2221
  try {
2103
2222
  if (!userId) {
2104
2223
  return { content: [{ type: "text", text: "profile-delete requires an authenticated admin session with userId" }], isError: true };
@@ -2131,8 +2250,10 @@ if (!readOnly) {
2131
2250
  detail: z.boolean().optional().describe("When true, returns per-preference confidence, source, evidence trail, and memory statistics. " +
2132
2251
  "Default false returns a compact summary for system prompt injection."),
2133
2252
  }, async ({ detail }) => {
2134
- if (!accountId)
2135
- return refuseNoAccount("profile-read");
2253
+ const scoped = resolveToolAccount("profile-read", undefined);
2254
+ if ("reject" in scoped)
2255
+ return scoped.reject;
2256
+ const accountId = scoped.accountId;
2136
2257
  try {
2137
2258
  if (!userId) {
2138
2259
  return { content: [{ type: "text", text: "profile-read requires an authenticated admin session with userId" }], isError: true };
@@ -2166,10 +2287,13 @@ if (!readOnly) {
2166
2287
  "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
2288
  windowHours: z.number().int().optional().describe("Hours back from now. Default 24. Capped at 720 (30 days)."),
2168
2289
  limit: z.number().int().optional().describe("Max rows returned. Default 50, max 200."),
2169
- }, async ({ windowHours, limit }) => {
2290
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
2291
+ }, async ({ windowHours, limit, targetAccountId }) => {
2170
2292
  try {
2171
- if (!accountId)
2172
- return refuseNoAccount("memory-brain-capture-recent");
2293
+ const scoped = resolveToolAccount("memory-brain-capture-recent", targetAccountId);
2294
+ if ("reject" in scoped)
2295
+ return scoped.reject;
2296
+ const accountId = scoped.accountId;
2173
2297
  const result = await memoryBrainCaptureRecent({ accountId, windowHours, limit });
2174
2298
  return {
2175
2299
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
@@ -2203,8 +2327,10 @@ if (!readOnly) {
2203
2327
  "per-device and survives upgrades.", {
2204
2328
  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
2329
  }, async ({ values }) => {
2206
- if (!accountId)
2207
- return refuseNoAccount("graph-prune-denylist-add");
2330
+ const scoped = resolveToolAccount("graph-prune-denylist-add", undefined);
2331
+ if ("reject" in scoped)
2332
+ return scoped.reject;
2333
+ const accountId = scoped.accountId;
2208
2334
  try {
2209
2335
  const result = await graphPruneDenylistAdd(values);
2210
2336
  const lines = [
@@ -2229,8 +2355,10 @@ if (!readOnly) {
2229
2355
  "restored — audit entries on each affected node remain for forensic review.", {
2230
2356
  values: z.array(z.string().min(1)).min(1).describe("Substring values to remove (must match exactly as previously added)."),
2231
2357
  }, async ({ values }) => {
2232
- if (!accountId)
2233
- return refuseNoAccount("graph-prune-denylist-remove");
2358
+ const scoped = resolveToolAccount("graph-prune-denylist-remove", undefined);
2359
+ if ("reject" in scoped)
2360
+ return scoped.reject;
2361
+ const accountId = scoped.accountId;
2234
2362
  try {
2235
2363
  const result = await graphPruneDenylistRemove(values);
2236
2364
  const lines = [
@@ -2257,9 +2385,12 @@ if (!readOnly) {
2257
2385
  "that should not propagate (e.g. wrong-account hostnames, fabricated identifiers).", {
2258
2386
  sessionId: z.string().min(1).describe("The Conversation node's sessionId to scope the expunge to."),
2259
2387
  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");
2388
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
2389
+ }, async ({ sessionId, pattern, targetAccountId }) => {
2390
+ const scoped = resolveToolAccount("conversation-memory-expunge", targetAccountId);
2391
+ if ("reject" in scoped)
2392
+ return scoped.reject;
2393
+ const accountId = scoped.accountId;
2263
2394
  try {
2264
2395
  const result = await conversationMemoryExpunge({ accountId, sessionId, pattern });
2265
2396
  const lines = [
@@ -2286,8 +2417,10 @@ if (!readOnly) {
2286
2417
  // graph-prune-denylist-list and conversation-search are read-only — available regardless of readOnly mode.
2287
2418
  lifelineTool(server, "graph-prune-denylist-list", "List the current operator-curated deny-list values used by the daily graph-prune cron. " +
2288
2419
  "Returns the substring values, the timestamp of the last edit, and the total count.", {}, () => {
2289
- if (!accountId)
2290
- return refuseNoAccount("graph-prune-denylist-list");
2420
+ const scoped = resolveToolAccount("graph-prune-denylist-list", undefined);
2421
+ if ("reject" in scoped)
2422
+ return scoped.reject;
2423
+ const accountId = scoped.accountId;
2291
2424
  try {
2292
2425
  const result = graphPruneDenylistList();
2293
2426
  if (result.count === 0) {
@@ -2311,9 +2444,12 @@ eagerTool(server, "conversation-search", "Search conversation history using sema
2311
2444
  .number()
2312
2445
  .optional()
2313
2446
  .describe("Maximum number of results (default 10)"),
2314
- }, async ({ query, limit: maxResults }) => {
2315
- if (!accountId)
2316
- return refuseNoAccount("conversation-search");
2447
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
2448
+ }, async ({ query, limit: maxResults, targetAccountId }) => {
2449
+ const scoped = resolveToolAccount("conversation-search", targetAccountId);
2450
+ if ("reject" in scoped)
2451
+ return scoped.reject;
2452
+ const accountId = scoped.accountId;
2317
2453
  try {
2318
2454
  const queryEmbedding = await embed(query);
2319
2455
  const resultLimit = maxResults ?? 10;
@@ -2438,9 +2574,12 @@ lifelineTool(server, "memory-dream-run", "Operator-initiated dream-cycle: graph-
2438
2574
  operatorId: z.string().optional().describe("Identifier of the operator who triggered the run; appears in the report and log line."),
2439
2575
  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
2576
  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");
2577
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
2578
+ }, async ({ operatorId, phases, maxEventsPerRun, targetAccountId }) => {
2579
+ const scoped = resolveToolAccount("memory-dream-run", targetAccountId);
2580
+ if ("reject" in scoped)
2581
+ return scoped.reject;
2582
+ const accountId = scoped.accountId;
2444
2583
  try {
2445
2584
  const result = await memoryDreamRun({ accountId, operatorId, phases, maxEventsPerRun });
2446
2585
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
@@ -2455,9 +2594,12 @@ lifelineTool(server, "memory-dream-run", "Operator-initiated dream-cycle: graph-
2455
2594
  lifelineTool(server, "memory-review-queue", "Read-only summary of the dream-cycle's review queues (:OrphanCandidate, :CitationProposal). " +
2456
2595
  "Returns per-queue counts and a small sample so the operator can see whether to triage.", {
2457
2596
  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");
2597
+ targetAccountId: z.string().optional().describe(XACCT_DESC),
2598
+ }, async ({ sampleSize, targetAccountId }) => {
2599
+ const scoped = resolveToolAccount("memory-review-queue", targetAccountId);
2600
+ if ("reject" in scoped)
2601
+ return scoped.reject;
2602
+ const accountId = scoped.accountId;
2461
2603
  try {
2462
2604
  const result = await memoryReviewQueue({ accountId, sampleSize });
2463
2605
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };