@rubytech/create-maxy-code 0.1.416 → 0.1.417

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 (204) hide show
  1. package/dist/__tests__/outlook-client-id-env.test.js +46 -0
  2. package/dist/index.js +31 -0
  3. package/dist/port-resolution.js +8 -1
  4. package/package.json +1 -1
  5. package/payload/platform/config/brand.json +1 -0
  6. package/payload/platform/lib/account-enumeration/dist/__tests__/is-provisioned.test.d.ts +2 -0
  7. package/payload/platform/lib/account-enumeration/dist/__tests__/is-provisioned.test.d.ts.map +1 -0
  8. package/payload/platform/lib/account-enumeration/dist/__tests__/is-provisioned.test.js +81 -0
  9. package/payload/platform/lib/account-enumeration/dist/__tests__/is-provisioned.test.js.map +1 -0
  10. package/payload/platform/lib/account-enumeration/dist/index.d.ts +17 -0
  11. package/payload/platform/lib/account-enumeration/dist/index.d.ts.map +1 -1
  12. package/payload/platform/lib/account-enumeration/dist/index.js +46 -9
  13. package/payload/platform/lib/account-enumeration/dist/index.js.map +1 -1
  14. package/payload/platform/lib/account-enumeration/src/__tests__/is-provisioned.test.ts +86 -0
  15. package/payload/platform/lib/account-enumeration/src/index.ts +45 -9
  16. package/payload/platform/lib/account-scope/dist/index.d.ts +14 -5
  17. package/payload/platform/lib/account-scope/dist/index.d.ts.map +1 -1
  18. package/payload/platform/lib/account-scope/dist/index.js +14 -5
  19. package/payload/platform/lib/account-scope/dist/index.js.map +1 -1
  20. package/payload/platform/lib/account-scope/src/index.ts +22 -5
  21. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +43 -10
  22. package/payload/platform/plugins/docs/references/outlook-guide.md +42 -9
  23. package/payload/platform/plugins/email/references/email-reference.md +1 -1
  24. package/payload/platform/plugins/filesystem/PLUGIN.md +1 -1
  25. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.js +16 -2
  26. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.js.map +1 -1
  27. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts +1 -1
  28. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts.map +1 -1
  29. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js +8 -3
  30. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js.map +1 -1
  31. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.js +2 -2
  32. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.js.map +1 -1
  33. package/payload/platform/plugins/memory/PLUGIN.md +1 -1
  34. package/payload/platform/plugins/memory/mcp/dist/index-testable.d.ts.map +1 -1
  35. package/payload/platform/plugins/memory/mcp/dist/index-testable.js +14 -5
  36. package/payload/platform/plugins/memory/mcp/dist/index-testable.js.map +1 -1
  37. package/payload/platform/plugins/memory/mcp/dist/index.js +5 -2
  38. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  39. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j-constraint.d.ts +29 -0
  40. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j-constraint.d.ts.map +1 -0
  41. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j-constraint.js +44 -0
  42. package/payload/platform/plugins/memory/mcp/dist/lib/neo4j-constraint.js.map +1 -0
  43. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/owner-person-embed-text.test.d.ts +2 -0
  44. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/owner-person-embed-text.test.d.ts.map +1 -0
  45. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/owner-person-embed-text.test.js +22 -0
  46. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/owner-person-embed-text.test.js.map +1 -0
  47. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-bootstrap.test.js +105 -0
  48. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-bootstrap.test.js.map +1 -1
  49. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-embed.test.d.ts +2 -0
  50. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-embed.test.d.ts.map +1 -0
  51. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-embed.test.js +78 -0
  52. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-embed.test.js.map +1 -0
  53. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.js +2 -2
  54. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.js.map +1 -1
  55. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts +0 -5
  56. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  57. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +2 -12
  58. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  59. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts +1 -0
  60. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts.map +1 -1
  61. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +106 -10
  62. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
  63. package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
  64. package/payload/platform/plugins/outlook/PLUGIN.md +40 -7
  65. package/payload/platform/plugins/outlook/mcp/dist/__tests__/account-register.test.d.ts +2 -0
  66. package/payload/platform/plugins/outlook/mcp/dist/__tests__/account-register.test.d.ts.map +1 -0
  67. package/payload/platform/plugins/outlook/mcp/dist/__tests__/account-register.test.js +137 -0
  68. package/payload/platform/plugins/outlook/mcp/dist/__tests__/account-register.test.js.map +1 -0
  69. package/payload/platform/plugins/outlook/mcp/dist/__tests__/calendar-write.test.d.ts +2 -0
  70. package/payload/platform/plugins/outlook/mcp/dist/__tests__/calendar-write.test.d.ts.map +1 -0
  71. package/payload/platform/plugins/outlook/mcp/dist/__tests__/calendar-write.test.js +186 -0
  72. package/payload/platform/plugins/outlook/mcp/dist/__tests__/calendar-write.test.js.map +1 -0
  73. package/payload/platform/plugins/outlook/mcp/dist/__tests__/device-flow.test.d.ts +2 -0
  74. package/payload/platform/plugins/outlook/mcp/dist/__tests__/device-flow.test.d.ts.map +1 -0
  75. package/payload/platform/plugins/outlook/mcp/dist/__tests__/device-flow.test.js +110 -0
  76. package/payload/platform/plugins/outlook/mcp/dist/__tests__/device-flow.test.js.map +1 -0
  77. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-retry.test.d.ts +2 -0
  78. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-retry.test.d.ts.map +1 -0
  79. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-retry.test.js +108 -0
  80. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-retry.test.js.map +1 -0
  81. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js +13 -0
  82. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js.map +1 -1
  83. package/payload/platform/plugins/outlook/mcp/dist/__tests__/message.test.d.ts +2 -0
  84. package/payload/platform/plugins/outlook/mcp/dist/__tests__/message.test.d.ts.map +1 -0
  85. package/payload/platform/plugins/outlook/mcp/dist/__tests__/message.test.js +32 -0
  86. package/payload/platform/plugins/outlook/mcp/dist/__tests__/message.test.js.map +1 -0
  87. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pending-store.test.d.ts +2 -0
  88. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pending-store.test.d.ts.map +1 -0
  89. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pending-store.test.js +67 -0
  90. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pending-store.test.js.map +1 -0
  91. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js +7 -5
  92. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js.map +1 -1
  93. package/payload/platform/plugins/outlook/mcp/dist/__tests__/write-tools.test.d.ts +2 -0
  94. package/payload/platform/plugins/outlook/mcp/dist/__tests__/write-tools.test.d.ts.map +1 -0
  95. package/payload/platform/plugins/outlook/mcp/dist/__tests__/write-tools.test.js +141 -0
  96. package/payload/platform/plugins/outlook/mcp/dist/__tests__/write-tools.test.js.map +1 -0
  97. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.d.ts +69 -0
  98. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.d.ts.map +1 -0
  99. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.js +129 -0
  100. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.js.map +1 -0
  101. package/payload/platform/plugins/outlook/mcp/dist/auth/pending-store.d.ts +39 -0
  102. package/payload/platform/plugins/outlook/mcp/dist/auth/pending-store.d.ts.map +1 -0
  103. package/payload/platform/plugins/outlook/mcp/dist/auth/pending-store.js +101 -0
  104. package/payload/platform/plugins/outlook/mcp/dist/auth/pending-store.js.map +1 -0
  105. package/payload/platform/plugins/outlook/mcp/dist/auth/pkce-flow.d.ts.map +1 -1
  106. package/payload/platform/plugins/outlook/mcp/dist/auth/pkce-flow.js +3 -2
  107. package/payload/platform/plugins/outlook/mcp/dist/auth/pkce-flow.js.map +1 -1
  108. package/payload/platform/plugins/outlook/mcp/dist/index.js +187 -5
  109. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  110. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts +34 -1
  111. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts.map +1 -1
  112. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js +96 -7
  113. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js.map +1 -1
  114. package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts +31 -0
  115. package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts.map +1 -0
  116. package/payload/platform/plugins/outlook/mcp/dist/lib/message.js +30 -0
  117. package/payload/platform/plugins/outlook/mcp/dist/lib/message.js.map +1 -0
  118. package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.d.ts +41 -17
  119. package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.d.ts.map +1 -1
  120. package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.js +87 -28
  121. package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.js.map +1 -1
  122. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-cancel.d.ts +17 -0
  123. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-cancel.d.ts.map +1 -0
  124. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-cancel.js +38 -0
  125. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-cancel.js.map +1 -0
  126. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-create.d.ts +29 -0
  127. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-create.d.ts.map +1 -0
  128. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-create.js +50 -0
  129. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-create.js.map +1 -0
  130. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-freebusy.d.ts +34 -0
  131. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-freebusy.d.ts.map +1 -0
  132. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-freebusy.js +46 -0
  133. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-freebusy.js.map +1 -0
  134. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-respond.d.ts +15 -0
  135. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-respond.d.ts.map +1 -0
  136. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-respond.js +27 -0
  137. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-respond.js.map +1 -0
  138. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-update.d.ts +11 -0
  139. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-update.d.ts.map +1 -0
  140. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-update.js +20 -0
  141. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-update.js.map +1 -0
  142. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-send.d.ts +17 -0
  143. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-send.d.ts.map +1 -0
  144. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-send.js +26 -0
  145. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-send.js.map +1 -0
  146. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts +11 -0
  147. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts.map +1 -0
  148. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js +26 -0
  149. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js.map +1 -0
  150. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts +14 -0
  151. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts.map +1 -0
  152. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js +26 -0
  153. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js.map +1 -0
  154. package/payload/platform/plugins/outlook/mcp/scripts/verify-doc-impl.sh +2 -2
  155. package/payload/platform/plugins/outlook/references/auth.md +51 -54
  156. package/payload/platform/plugins/outlook/references/graph-surfaces.md +97 -4
  157. package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +34 -9
  158. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  159. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +9 -0
  160. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  161. package/payload/platform/services/claude-session-manager/dist/config.d.ts +18 -0
  162. package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
  163. package/payload/platform/services/claude-session-manager/dist/config.js +16 -0
  164. package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
  165. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  166. package/payload/platform/services/claude-session-manager/dist/http-server.js +7 -2
  167. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  168. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  169. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +7 -2
  170. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  171. package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
  172. package/payload/server/public/assets/{AdminLoginScreens-CBlE6FB4.js → AdminLoginScreens-CBTx7ZPK.js} +1 -1
  173. package/payload/server/public/assets/{AdminShell-d1WQ3lLu.js → AdminShell-DPHUeXea.js} +1 -1
  174. package/payload/server/public/assets/{Checkbox-B0pbYJDQ.js → Checkbox-BHTxekRB.js} +1 -1
  175. package/payload/server/public/assets/{Transcript-CYaAPBFX.js → Transcript-5kLbBInN.js} +1 -1
  176. package/payload/server/public/assets/{admin-CVh9HO-f.js → admin-BWCLE32N.js} +1 -1
  177. package/payload/server/public/assets/{browser-C4f_D4Ih.js → browser-CRvCPsvV.js} +1 -1
  178. package/payload/server/public/assets/{calendar-WUxQ4fVo.js → calendar-BI-kd-QP.js} +1 -1
  179. package/payload/server/public/assets/chat-DFy8hb1y.js +1 -0
  180. package/payload/server/public/assets/chevron-left-Bggm5B5U.js +1 -0
  181. package/payload/server/public/assets/data-HvZ69Ya1.js +1 -0
  182. package/payload/server/public/assets/{graph-BNXE6mKP.js → graph-BFSyW_5i.js} +1 -1
  183. package/payload/server/public/assets/{graph-labels-Csnovm_c.js → graph-labels-DEjD_qJh.js} +1 -1
  184. package/payload/server/public/assets/{maximize-2-WRUJsg5n.js → maximize-2-BcMJNeCB.js} +1 -1
  185. package/payload/server/public/assets/{operator-CXtygMh6.js → operator-Dtvm9ero.js} +1 -1
  186. package/payload/server/public/assets/{page-DjpQp_ir.js → page-C_pm3hl3.js} +1 -1
  187. package/payload/server/public/assets/{page-IQDLdebG.js → page-TXi9dmaB.js} +1 -1
  188. package/payload/server/public/assets/{public-DSeYu61E.js → public-DBx3U0r8.js} +1 -1
  189. package/payload/server/public/assets/{rotate-ccw-BayMWwfc.js → rotate-ccw-8ti3QkeY.js} +1 -1
  190. package/payload/server/public/assets/useSubAccountSwitcher-BUvlETHN.css +1 -0
  191. package/payload/server/public/assets/{useSubAccountSwitcher-C9aLg8yI.js → useSubAccountSwitcher-Bc5dOBdI.js} +1 -1
  192. package/payload/server/public/browser.html +4 -4
  193. package/payload/server/public/calendar.html +5 -5
  194. package/payload/server/public/chat.html +9 -9
  195. package/payload/server/public/data.html +8 -8
  196. package/payload/server/public/graph.html +9 -9
  197. package/payload/server/public/index.html +11 -11
  198. package/payload/server/public/operator.html +11 -11
  199. package/payload/server/public/public.html +9 -9
  200. package/payload/server/server.js +8 -2
  201. package/payload/server/public/assets/chat-NJ2HXhOf.js +0 -1
  202. package/payload/server/public/assets/chevron-left-Da-iZGi_.js +0 -1
  203. package/payload/server/public/assets/data-ZUdT44Ba.js +0 -1
  204. package/payload/server/public/assets/useSubAccountSwitcher-CRZfyJXR.css +0 -1
@@ -0,0 +1,46 @@
1
+ // acceptance gate for OUTLOOK_CLIENT_ID stamping in the claude-session-manager
2
+ // systemd unit (Task 1473).
3
+ //
4
+ // The outlook MCP reads process.env.OUTLOOK_CLIENT_ID; the session manager is
5
+ // the process that spawns claude -> MCP, so the client ID must land on this
6
+ // unit's Environment= block. Two invariants:
7
+ // (a) a non-empty brand outlookClientId emits BOTH
8
+ // `Environment=OUTLOOK_CLIENT_ID=<id>` and
9
+ // `Environment=OUTLOOK_TENANT_ID=common` (multi-tenant /common app).
10
+ // (b) an empty / absent brand value emits NEITHER line — the plugin stays
11
+ // inert on that brand exactly as before (no regression), and the MCP's
12
+ // module-init no-throw path keeps the registry intact.
13
+ import test from "node:test";
14
+ import assert from "node:assert/strict";
15
+ import { buildClaudeSessionManagerUnitFile } from "../port-resolution.js";
16
+ const ACCOUNT_ID = "12345678-9abc-def0-1234-56789abcdef0";
17
+ const CLIENT_ID = "0fa68fe2-8bec-4007-a0c7-776636fbcf97";
18
+ function build(outlookClientId) {
19
+ return buildClaudeSessionManagerUnitFile({
20
+ productName: "SiteDesk",
21
+ brandHostname: "sitedesk-code",
22
+ installDir: "/home/me/sitedesk-code",
23
+ persistDir: "/home/me/.sitedesk-code",
24
+ claudeSessionManagerPort: 19402,
25
+ brandPort: 19300,
26
+ maxyUiInternalPort: 19301,
27
+ accountId: ACCOUNT_ID,
28
+ cdpPort: 9224,
29
+ outlookClientId,
30
+ });
31
+ }
32
+ test("stamps OUTLOOK_CLIENT_ID + OUTLOOK_TENANT_ID for a non-empty brand value", () => {
33
+ const unit = build(CLIENT_ID);
34
+ assert.match(unit, new RegExp(`^Environment=OUTLOOK_CLIENT_ID=${CLIENT_ID}$`, "m"), "OUTLOOK_CLIENT_ID line missing or malformed");
35
+ assert.match(unit, /^Environment=OUTLOOK_TENANT_ID=common$/m, "OUTLOOK_TENANT_ID=common line missing");
36
+ });
37
+ test("stamps NEITHER line when the brand value is empty", () => {
38
+ const unit = build("");
39
+ assert.doesNotMatch(unit, /OUTLOOK_CLIENT_ID/, "unexpected OUTLOOK_CLIENT_ID on empty brand");
40
+ assert.doesNotMatch(unit, /OUTLOOK_TENANT_ID/, "unexpected OUTLOOK_TENANT_ID on empty brand");
41
+ });
42
+ test("stamps NEITHER line when the brand value is absent", () => {
43
+ const unit = build(undefined);
44
+ assert.doesNotMatch(unit, /OUTLOOK_CLIENT_ID/, "unexpected OUTLOOK_CLIENT_ID on absent brand");
45
+ assert.doesNotMatch(unit, /OUTLOOK_TENANT_ID/, "unexpected OUTLOOK_TENANT_ID on absent brand");
46
+ });
package/dist/index.js CHANGED
@@ -3579,6 +3579,32 @@ function installServiceDarwin() {
3579
3579
  else
3580
3580
  envContent = envContent.trimEnd() + (envContent.length > 0 ? "\n" : "") + `NODE_BIN=${nodeBin}\n`;
3581
3581
  }
3582
+ // Task 1473 — darwin has no systemd EnvironmentFile primitive, so the vendor
3583
+ // Outlook app env is mirrored into .env (the wrapper sources it before exec).
3584
+ // Symmetric with the Linux unit, which is rebuilt wholesale each run: a
3585
+ // non-empty brand outlookClientId upserts BOTH vars together; empty / absent
3586
+ // STRIPS both lines. Stripping matters because .env persists across installs —
3587
+ // without it, a brand whose outlookClientId is later blanked (vendor-app
3588
+ // rotation/revocation) would keep sourcing a stale, now-invalid client ID and
3589
+ // keep reporting clientIdConfigured=true against its current empty intent.
3590
+ if (BRAND.outlookClientId) {
3591
+ for (const [key, value] of [
3592
+ ["OUTLOOK_CLIENT_ID", BRAND.outlookClientId],
3593
+ ["OUTLOOK_TENANT_ID", "common"],
3594
+ ]) {
3595
+ const re = new RegExp(`^${key}=.*$`, "m");
3596
+ if (re.test(envContent))
3597
+ envContent = envContent.replace(re, `${key}=${value}`);
3598
+ else
3599
+ envContent = envContent.trimEnd() + (envContent.length > 0 ? "\n" : "") + `${key}=${value}\n`;
3600
+ }
3601
+ }
3602
+ else {
3603
+ for (const key of ["OUTLOOK_CLIENT_ID", "OUTLOOK_TENANT_ID"]) {
3604
+ envContent = envContent.replace(new RegExp(`^${key}=.*\\n?`, "m"), "");
3605
+ }
3606
+ }
3607
+ logFile(` [installer] op=outlook-client-id brand=${BRAND.hostname} stamped=${BRAND.outlookClientId ? "true" : "false"}`);
3582
3608
  writeFileSync(envPath, envContent);
3583
3609
  logFile(` .env: DISPLAY_MODE=${DISPLAY_MODE}, EMBED_MODEL=${EMBED_MODEL}, EMBED_DIMENSIONS=${EMBED_DIMS}, NEO4J_URI=bolt://localhost:${NEO4J_PORT}, PORT=${PORT}, MAXY_UI_INTERNAL_PORT=${PORT} (darwin-collapsed), CLAUDE_SESSION_MANAGER_PORT=${BRAND.claudeSessionManagerPort}, HOSTNAME=0.0.0.0, ACCOUNT_ID=${installAccountId.slice(0, 8)}…, CLAUDE_CONFIG_DIR=${persistDir}/.claude, CLAUDE_BIN=${canonicalClaude ?? "unresolved(PATH)"}, NODE_BIN=${nodeBin}, PATH=${darwinServiceEnvPath}`);
3584
3610
  }
@@ -4161,9 +4187,14 @@ function installService() {
4161
4187
  maxyUiInternalPort: MAXY_UI_INTERNAL_PORT,
4162
4188
  accountId: installAccountId,
4163
4189
  cdpPort: CDP_PORT_BRAND,
4190
+ outlookClientId: BRAND.outlookClientId,
4164
4191
  });
4165
4192
  writeFileSync(join(serviceDir, claudeSessionManagerUnitName), claudeSessionManagerUnit);
4166
4193
  logFile(` ${claudeSessionManagerUnitName}: CLAUDE_SESSION_MANAGER_PORT=${CLAUDE_SESSION_MANAGER_PORT_BRAND} MAXY_UI_INTERNAL_PORT=${MAXY_UI_INTERNAL_PORT}`);
4194
+ // Task 1473 — standing install-time signal reconciling brand.json intent
4195
+ // (a non-empty outlookClientId) against what the unit shipped. `stamped=false`
4196
+ // while the brand has a value is the mis-stamp signature.
4197
+ logFile(` [installer] op=outlook-client-id brand=${BRAND.hostname} stamped=${BRAND.outlookClientId ? "true" : "false"}`);
4167
4198
  // Task 250 — write the brand-agnostic `claude-ptys.slice` unit. Every
4168
4199
  // brand's session manager drops its per-spawn scope units under this
4169
4200
  // slice. Writing it once per install is idempotent (the content is
@@ -128,6 +128,13 @@ export function buildClaudeSessionManagerUnitFile(o) {
128
128
  if (!o.accountId) {
129
129
  throw new Error("buildClaudeSessionManagerUnitFile: accountId is required — caller must resolve the on-disk account UUID before stamping the systemd unit.");
130
130
  }
131
+ // Task 1473 — a non-empty brand outlookClientId adds the vendor Outlook app
132
+ // env. Both lines land together so OUTLOOK_TENANT_ID (multi-tenant /common)
133
+ // never trails a bare client ID. Empty / absent brands stamp neither line —
134
+ // the plugin stays inert, no regression.
135
+ const outlookEnv = o.outlookClientId
136
+ ? `\nEnvironment=OUTLOOK_CLIENT_ID=${o.outlookClientId}\nEnvironment=OUTLOOK_TENANT_ID=common`
137
+ : "";
131
138
  return `[Unit]
132
139
  Description=${o.productName} Claude Session Manager
133
140
  After=network.target
@@ -149,7 +156,7 @@ Environment=PLATFORM_ROOT=${o.installDir}/platform
149
156
  Environment=MAXY_BRAND_PORT=${o.brandPort}
150
157
  Environment=MAXY_UI_INTERNAL_PORT=${o.maxyUiInternalPort}
151
158
  Environment=PLATFORM_PORT=${o.maxyUiInternalPort}
152
- Environment=CDP_PORT=${o.cdpPort}
159
+ Environment=CDP_PORT=${o.cdpPort}${outlookEnv}
153
160
  EnvironmentFile=-${o.persistDir}/.env
154
161
  StandardOutput=append:${o.persistDir}/logs/server.log
155
162
  StandardError=append:${o.persistDir}/logs/server.log
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.416",
3
+ "version": "0.1.417",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -14,6 +14,7 @@
14
14
  "websockifyPort": 6080,
15
15
  "cdpPort": 9222,
16
16
  "claudeSessionManagerPort": 19400,
17
+ "outlookClientId": "",
17
18
  "commercialMode": false,
18
19
  "vertical": "schema-knowledge-work",
19
20
  "shipsPremiumBundles": ["venture-studio", "writer-craft", "teaching", "management-consulting"],
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=is-provisioned.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"is-provisioned.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/is-provisioned.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const node_fs_1 = require("node:fs");
9
+ const node_os_1 = require("node:os");
10
+ const node_path_1 = require("node:path");
11
+ const index_js_1 = require("../index.js");
12
+ const BOOT_ID = "12345678-9abc-def0-1234-56789abcdef0";
13
+ const LATER_ID = "99999999-aaaa-bbbb-cccc-dddddddddddd";
14
+ /** One valid account dir (BOOT_ID) that existed at "boot". */
15
+ function makeFixture() {
16
+ const root = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "acct-prov-"));
17
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(root, BOOT_ID));
18
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, BOOT_ID, "account.json"), JSON.stringify({ id: BOOT_ID }));
19
+ return root;
20
+ }
21
+ /** Provision a second account dir on disk AFTER boot (simulates account_create). */
22
+ function provisionLater(root) {
23
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(root, LATER_ID));
24
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, LATER_ID, "account.json"), JSON.stringify({ id: LATER_ID }));
25
+ }
26
+ (0, node_test_1.default)("isProvisionedAccount: a created-after-boot id resolves via re-scan on miss", () => {
27
+ (0, index_js_1._resetEnumerationCache)();
28
+ const root = makeFixture();
29
+ try {
30
+ // Warm the cache at "boot" — only BOOT_ID is present.
31
+ strict_1.default.deepEqual((0, index_js_1.enumerateValidAccountIds)(root), [BOOT_ID]);
32
+ // A sub-account is provisioned after the snapshot.
33
+ provisionLater(root);
34
+ // Miss against the cached set must re-scan disk and find it — no restart.
35
+ strict_1.default.equal((0, index_js_1.isProvisionedAccount)(root, LATER_ID), true);
36
+ }
37
+ finally {
38
+ (0, node_fs_1.rmSync)(root, { recursive: true });
39
+ }
40
+ });
41
+ (0, node_test_1.default)("isProvisionedAccount: a truly-absent id still returns false after re-scan", () => {
42
+ (0, index_js_1._resetEnumerationCache)();
43
+ const root = makeFixture();
44
+ try {
45
+ (0, index_js_1.enumerateValidAccountIds)(root); // warm
46
+ const bogus = "00000000-0000-0000-0000-000000000000";
47
+ strict_1.default.equal((0, index_js_1.isProvisionedAccount)(root, bogus), false);
48
+ }
49
+ finally {
50
+ (0, node_fs_1.rmSync)(root, { recursive: true });
51
+ }
52
+ });
53
+ (0, node_test_1.default)("isProvisionedAccount: a warmed hit returns true without re-scanning", () => {
54
+ (0, index_js_1._resetEnumerationCache)();
55
+ const root = makeFixture();
56
+ try {
57
+ (0, index_js_1.enumerateValidAccountIds)(root); // warm = [BOOT_ID]
58
+ // Provision a second dir AFTER warming.
59
+ provisionLater(root);
60
+ // HIT: BOOT_ID is already in the cached set → true, and the cache must NOT
61
+ // be refreshed (no readdirSync). Proof: the cached set still hides LATER_ID.
62
+ strict_1.default.equal((0, index_js_1.isProvisionedAccount)(root, BOOT_ID), true);
63
+ strict_1.default.deepEqual((0, index_js_1.enumerateValidAccountIds)(root), [BOOT_ID], "a hit must not refresh the cache — LATER_ID must stay hidden");
64
+ }
65
+ finally {
66
+ (0, node_fs_1.rmSync)(root, { recursive: true });
67
+ }
68
+ });
69
+ (0, node_test_1.default)("isProvisionedAccount: cold cache scans disk and answers", () => {
70
+ (0, index_js_1._resetEnumerationCache)();
71
+ const root = makeFixture();
72
+ try {
73
+ // No warm call — cache is cold. A miss (cold) must scan and answer both ways.
74
+ strict_1.default.equal((0, index_js_1.isProvisionedAccount)(root, BOOT_ID), true);
75
+ strict_1.default.equal((0, index_js_1.isProvisionedAccount)(root, LATER_ID), false);
76
+ }
77
+ finally {
78
+ (0, node_fs_1.rmSync)(root, { recursive: true });
79
+ }
80
+ });
81
+ //# sourceMappingURL=is-provisioned.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"is-provisioned.test.js","sourceRoot":"","sources":["../../src/__tests__/is-provisioned.test.ts"],"names":[],"mappings":"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"}
@@ -6,6 +6,23 @@
6
6
  * be hiding).
7
7
  */
8
8
  export declare function enumerateValidAccountIds(accountsDir: string): string[];
9
+ /**
10
+ * Membership test with re-scan-on-miss: is `id` a currently-provisioned account
11
+ * under `accountsDir`?
12
+ *
13
+ * Hit path — `id` is in the cached set — returns true with no I/O (the common
14
+ * case; the cache still serves it). Miss path — `id` is absent from the cached
15
+ * set, or the cache is cold — drops the stale entry and re-scans `accountsDir`
16
+ * once (refreshing the cache) before answering.
17
+ *
18
+ * This closes the created-after-boot gap (Task 1471): a sub-account provisioned
19
+ * by the runtime `account_create` lifecycle tool after a consumer process booted
20
+ * is invisible to that process's boot-time cached set. Without the re-scan, the
21
+ * cross-account resolver rejects the id as not-provisioned even though its dir
22
+ * and `account.json` exist on disk. A single `readdirSync` on the miss finds it;
23
+ * a genuinely-absent id pays the same one scan and correctly returns false.
24
+ */
25
+ export declare function isProvisionedAccount(accountsDir: string, id: string): boolean;
9
26
  /**
10
27
  * Returns the single `role:"house"` account UUID under `accountsDir`.
11
28
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6BA;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAgCtE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAsBjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAqBvE;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAS9C;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,mCAAmC,GAC3C,SAAS,GACT,oBAAoB,GACpB,UAAU,CAAC;AAEf,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,GAC9C;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,mCAAmC,CAAC;IAC5C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEN,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,OAAO,EAAE,MAAM,EAAE,GAChB,sBAAsB,CAWxB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAwCA;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAgCtE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAO7E;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAsBjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAqBvE;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAS9C;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,mCAAmC,GAC3C,SAAS,GACT,oBAAoB,GACpB,UAAU,CAAC;AAEf,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,GAC9C;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,mCAAmC,CAAC;IAC5C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEN,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,OAAO,EAAE,MAAM,EAAE,GAChB,sBAAsB,CAWxB"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enumerateValidAccountIds = enumerateValidAccountIds;
4
+ exports.isProvisionedAccount = isProvisionedAccount;
4
5
  exports.resolveHouseAccountId = resolveHouseAccountId;
5
6
  exports.resolveHouseOrSoleAccountId = resolveHouseOrSoleAccountId;
6
7
  exports.getAccountsDirFromEnv = getAccountsDirFromEnv;
@@ -16,16 +17,27 @@ exports.validateAccountIdEnv = validateAccountIdEnv;
16
17
  * (`WHERE n.accountId = $accountId`) silently hides leaks from every UI;
17
18
  * an unenforced writer can leak indefinitely with zero downstream symptom.
18
19
  *
19
- * Two consumers:
20
- * 1. `writeNodeWithEdges` (platform/lib/graph-write/) central write-time
21
- * floor; rejects any write whose `props.accountId` is not in the set
22
- * returned here, unless `createdBy.agent === 'system'` (bootstrap).
23
- * 2. `resolvePlatformAccountId` (platform/ui/app/lib/whatsapp/platform-account-id.ts)
24
- * WhatsApp writer-side helper; loud-throws on zero or multi accounts.
20
+ * Consumers of the enumerated set:
21
+ * - The cross-account resolver's injected validity check (the `memory` and
22
+ * `filesystem` MCP plugins) the check that gates a `targetAccountId`.
23
+ * These call `isProvisionedAccount` (re-scan-on-miss), not the cached list
24
+ * directly, so a sub-account created at runtime is not masked by the boot
25
+ * snapshot (Task 1471).
26
+ * - `resolvePlatformAccountId` (platform/ui/app/lib/whatsapp/platform-account-id.ts)
27
+ * — WhatsApp writer-side helper; loud-throws on zero or multi accounts.
28
+ * - Boot-time env-vs-disk validation (`validateAccountIdEnv`).
25
29
  *
26
- * Per-process cache keyed on `accountsDir` Phase 0 invariant has accounts
27
- * created at install time, so adding/removing accounts requires a service
28
- * restart anyway.
30
+ * NOTE: `writeNodeWithEdges` (platform/lib/graph-write/) does NOT enumerate.
31
+ * Its account floor compares `props.accountId` against `process.env.ACCOUNT_ID`
32
+ * (the spawning process's boot-validated id), deliberately staying out of the
33
+ * `node:fs` import chain so the ESM payload bundle emits no `__require("fs")`
34
+ * shim. So the write gate does not share the cache's staleness.
35
+ *
36
+ * Per-process cache keyed on `accountsDir`. The install-time invariant (accounts
37
+ * created only at install) no longer holds: the runtime `account_create`
38
+ * lifecycle tool provisions sub-accounts during a live session. `enumerate…`
39
+ * keeps its cached-list semantics for the hit path; `isProvisionedAccount`
40
+ * re-scans on a miss so a created-after-boot account resolves without restart.
29
41
  */
30
42
  const node_fs_1 = require("node:fs");
31
43
  const node_path_1 = require("node:path");
@@ -73,6 +85,31 @@ function enumerateValidAccountIds(accountsDir) {
73
85
  cache.set(accountsDir, valid);
74
86
  return valid;
75
87
  }
88
+ /**
89
+ * Membership test with re-scan-on-miss: is `id` a currently-provisioned account
90
+ * under `accountsDir`?
91
+ *
92
+ * Hit path — `id` is in the cached set — returns true with no I/O (the common
93
+ * case; the cache still serves it). Miss path — `id` is absent from the cached
94
+ * set, or the cache is cold — drops the stale entry and re-scans `accountsDir`
95
+ * once (refreshing the cache) before answering.
96
+ *
97
+ * This closes the created-after-boot gap (Task 1471): a sub-account provisioned
98
+ * by the runtime `account_create` lifecycle tool after a consumer process booted
99
+ * is invisible to that process's boot-time cached set. Without the re-scan, the
100
+ * cross-account resolver rejects the id as not-provisioned even though its dir
101
+ * and `account.json` exist on disk. A single `readdirSync` on the miss finds it;
102
+ * a genuinely-absent id pays the same one scan and correctly returns false.
103
+ */
104
+ function isProvisionedAccount(accountsDir, id) {
105
+ const cached = cache.get(accountsDir);
106
+ if (cached?.includes(id))
107
+ return true;
108
+ // Miss: `id` is not in the cached set (or the cache is cold). Drop the stale
109
+ // snapshot and re-scan disk once so a real, on-disk account is not masked.
110
+ cache.delete(accountsDir);
111
+ return enumerateValidAccountIds(accountsDir).includes(id);
112
+ }
76
113
  /**
77
114
  * Returns the single `role:"house"` account UUID under `accountsDir`.
78
115
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import {
7
+ enumerateValidAccountIds,
8
+ isProvisionedAccount,
9
+ _resetEnumerationCache,
10
+ } from "../index.js";
11
+
12
+ const BOOT_ID = "12345678-9abc-def0-1234-56789abcdef0";
13
+ const LATER_ID = "99999999-aaaa-bbbb-cccc-dddddddddddd";
14
+
15
+ /** One valid account dir (BOOT_ID) that existed at "boot". */
16
+ function makeFixture(): string {
17
+ const root = mkdtempSync(join(tmpdir(), "acct-prov-"));
18
+ mkdirSync(join(root, BOOT_ID));
19
+ writeFileSync(join(root, BOOT_ID, "account.json"), JSON.stringify({ id: BOOT_ID }));
20
+ return root;
21
+ }
22
+
23
+ /** Provision a second account dir on disk AFTER boot (simulates account_create). */
24
+ function provisionLater(root: string): void {
25
+ mkdirSync(join(root, LATER_ID));
26
+ writeFileSync(join(root, LATER_ID, "account.json"), JSON.stringify({ id: LATER_ID }));
27
+ }
28
+
29
+ test("isProvisionedAccount: a created-after-boot id resolves via re-scan on miss", () => {
30
+ _resetEnumerationCache();
31
+ const root = makeFixture();
32
+ try {
33
+ // Warm the cache at "boot" — only BOOT_ID is present.
34
+ assert.deepEqual(enumerateValidAccountIds(root), [BOOT_ID]);
35
+ // A sub-account is provisioned after the snapshot.
36
+ provisionLater(root);
37
+ // Miss against the cached set must re-scan disk and find it — no restart.
38
+ assert.equal(isProvisionedAccount(root, LATER_ID), true);
39
+ } finally {
40
+ rmSync(root, { recursive: true });
41
+ }
42
+ });
43
+
44
+ test("isProvisionedAccount: a truly-absent id still returns false after re-scan", () => {
45
+ _resetEnumerationCache();
46
+ const root = makeFixture();
47
+ try {
48
+ enumerateValidAccountIds(root); // warm
49
+ const bogus = "00000000-0000-0000-0000-000000000000";
50
+ assert.equal(isProvisionedAccount(root, bogus), false);
51
+ } finally {
52
+ rmSync(root, { recursive: true });
53
+ }
54
+ });
55
+
56
+ test("isProvisionedAccount: a warmed hit returns true without re-scanning", () => {
57
+ _resetEnumerationCache();
58
+ const root = makeFixture();
59
+ try {
60
+ enumerateValidAccountIds(root); // warm = [BOOT_ID]
61
+ // Provision a second dir AFTER warming.
62
+ provisionLater(root);
63
+ // HIT: BOOT_ID is already in the cached set → true, and the cache must NOT
64
+ // be refreshed (no readdirSync). Proof: the cached set still hides LATER_ID.
65
+ assert.equal(isProvisionedAccount(root, BOOT_ID), true);
66
+ assert.deepEqual(
67
+ enumerateValidAccountIds(root),
68
+ [BOOT_ID],
69
+ "a hit must not refresh the cache — LATER_ID must stay hidden",
70
+ );
71
+ } finally {
72
+ rmSync(root, { recursive: true });
73
+ }
74
+ });
75
+
76
+ test("isProvisionedAccount: cold cache scans disk and answers", () => {
77
+ _resetEnumerationCache();
78
+ const root = makeFixture();
79
+ try {
80
+ // No warm call — cache is cold. A miss (cold) must scan and answer both ways.
81
+ assert.equal(isProvisionedAccount(root, BOOT_ID), true);
82
+ assert.equal(isProvisionedAccount(root, LATER_ID), false);
83
+ } finally {
84
+ rmSync(root, { recursive: true });
85
+ }
86
+ });
@@ -8,16 +8,27 @@
8
8
  * (`WHERE n.accountId = $accountId`) silently hides leaks from every UI;
9
9
  * an unenforced writer can leak indefinitely with zero downstream symptom.
10
10
  *
11
- * Two consumers:
12
- * 1. `writeNodeWithEdges` (platform/lib/graph-write/) central write-time
13
- * floor; rejects any write whose `props.accountId` is not in the set
14
- * returned here, unless `createdBy.agent === 'system'` (bootstrap).
15
- * 2. `resolvePlatformAccountId` (platform/ui/app/lib/whatsapp/platform-account-id.ts)
16
- * WhatsApp writer-side helper; loud-throws on zero or multi accounts.
11
+ * Consumers of the enumerated set:
12
+ * - The cross-account resolver's injected validity check (the `memory` and
13
+ * `filesystem` MCP plugins) the check that gates a `targetAccountId`.
14
+ * These call `isProvisionedAccount` (re-scan-on-miss), not the cached list
15
+ * directly, so a sub-account created at runtime is not masked by the boot
16
+ * snapshot (Task 1471).
17
+ * - `resolvePlatformAccountId` (platform/ui/app/lib/whatsapp/platform-account-id.ts)
18
+ * — WhatsApp writer-side helper; loud-throws on zero or multi accounts.
19
+ * - Boot-time env-vs-disk validation (`validateAccountIdEnv`).
17
20
  *
18
- * Per-process cache keyed on `accountsDir` Phase 0 invariant has accounts
19
- * created at install time, so adding/removing accounts requires a service
20
- * restart anyway.
21
+ * NOTE: `writeNodeWithEdges` (platform/lib/graph-write/) does NOT enumerate.
22
+ * Its account floor compares `props.accountId` against `process.env.ACCOUNT_ID`
23
+ * (the spawning process's boot-validated id), deliberately staying out of the
24
+ * `node:fs` import chain so the ESM payload bundle emits no `__require("fs")`
25
+ * shim. So the write gate does not share the cache's staleness.
26
+ *
27
+ * Per-process cache keyed on `accountsDir`. The install-time invariant (accounts
28
+ * created only at install) no longer holds: the runtime `account_create`
29
+ * lifecycle tool provisions sub-accounts during a live session. `enumerate…`
30
+ * keeps its cached-list semantics for the hit path; `isProvisionedAccount`
31
+ * re-scans on a miss so a created-after-boot account resolves without restart.
21
32
  */
22
33
  import { readdirSync, readFileSync } from "node:fs";
23
34
  import { resolve } from "node:path";
@@ -68,6 +79,31 @@ export function enumerateValidAccountIds(accountsDir: string): string[] {
68
79
  return valid;
69
80
  }
70
81
 
82
+ /**
83
+ * Membership test with re-scan-on-miss: is `id` a currently-provisioned account
84
+ * under `accountsDir`?
85
+ *
86
+ * Hit path — `id` is in the cached set — returns true with no I/O (the common
87
+ * case; the cache still serves it). Miss path — `id` is absent from the cached
88
+ * set, or the cache is cold — drops the stale entry and re-scans `accountsDir`
89
+ * once (refreshing the cache) before answering.
90
+ *
91
+ * This closes the created-after-boot gap (Task 1471): a sub-account provisioned
92
+ * by the runtime `account_create` lifecycle tool after a consumer process booted
93
+ * is invisible to that process's boot-time cached set. Without the re-scan, the
94
+ * cross-account resolver rejects the id as not-provisioned even though its dir
95
+ * and `account.json` exist on disk. A single `readdirSync` on the miss finds it;
96
+ * a genuinely-absent id pays the same one scan and correctly returns false.
97
+ */
98
+ export function isProvisionedAccount(accountsDir: string, id: string): boolean {
99
+ const cached = cache.get(accountsDir);
100
+ if (cached?.includes(id)) return true;
101
+ // Miss: `id` is not in the cached set (or the cache is cold). Drop the stale
102
+ // snapshot and re-scan disk once so a real, on-disk account is not masked.
103
+ cache.delete(accountsDir);
104
+ return enumerateValidAccountIds(accountsDir).includes(id);
105
+ }
106
+
71
107
  /**
72
108
  * Returns the single `role:"house"` account UUID under `accountsDir`.
73
109
  *
@@ -1,7 +1,7 @@
1
1
  export type EffectiveAccount = {
2
2
  accountId: string;
3
3
  } | {
4
- reject: "cross-account-denied" | "cross-account-invalid-target";
4
+ reject: "cross-account-denied" | "cross-account-invalid-target" | "cross-account-target-not-provisioned";
5
5
  };
6
6
  /**
7
7
  * Resolve the account a call acts on. Fail-closed: only a house-scoped session
@@ -10,15 +10,24 @@ export type EffectiveAccount = {
10
10
  * downgraded to its own account.
11
11
  *
12
12
  * `isValidAccountId` is injected (required, no default): the UI passes its
13
- * fs-backed `isValidAccountId`; the `filesystem` plugin passes a check over
14
- * `enumerateValidAccountIds`. Keeping validity injected is what lets this
15
- * module stay pure.
13
+ * fs-backed `isValidAccountId` (reads disk fresh every call); the `memory` and
14
+ * `filesystem` plugins pass `isProvisionedAccount` (cached-hit, re-scan-on-miss).
15
+ * Keeping validity injected is what lets this module stay pure.
16
+ *
17
+ * `notProvisionedReason` names the reject emitted when the injected check fails.
18
+ * It defaults to `cross-account-invalid-target` (the UI path, byte-identical to
19
+ * the 1440 landing). The MCP plugins pass `cross-account-target-not-provisioned`
20
+ * because their injected check re-scans disk on a miss (Task 1471) — so a false
21
+ * from it means the id is genuinely not on disk, distinct from a stale-cache
22
+ * miss (which no longer rejects). The distinct reason lets an operator tell a
23
+ * wrong/typo id from the (now-eliminated) created-after-boot case in the
24
+ * `[xacct] op=call … reason=` line.
16
25
  */
17
26
  export declare function resolveEffectiveAccountCore(input: {
18
27
  ownAccountId: string;
19
28
  houseScope: string | null;
20
29
  targetAccountId?: string | null;
21
- }, isValidAccountId: (id: string) => boolean): EffectiveAccount;
30
+ }, isValidAccountId: (id: string) => boolean, notProvisionedReason?: "cross-account-invalid-target" | "cross-account-target-not-provisioned"): EffectiveAccount;
22
31
  /**
23
32
  * Boot-reader that consumers import to learn their house scope from the spawn
24
33
  * env. An empty value reads as null — a non-house session carries no scope.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,gBAAgB,GACxB;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,sBAAsB,GAAG,8BAA8B,CAAA;CAAE,CAAC;AAExE;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAC3F,gBAAgB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GACxC,gBAAgB,CAMlB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,GAAG,IAAI,CAEvF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,gBAAgB,GACxB;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GACrB;IACE,MAAM,EACF,sBAAsB,GACtB,8BAA8B,GAC9B,sCAAsC,CAAC;CAC5C,CAAC;AAEN;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAC3F,gBAAgB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,EACzC,oBAAoB,GAChB,8BAA8B,GAC9B,sCAAuE,GAC1E,gBAAgB,CAMlB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,GAAG,IAAI,CAEvF"}
@@ -9,18 +9,27 @@ exports.readHouseAdminScope = readHouseAdminScope;
9
9
  * downgraded to its own account.
10
10
  *
11
11
  * `isValidAccountId` is injected (required, no default): the UI passes its
12
- * fs-backed `isValidAccountId`; the `filesystem` plugin passes a check over
13
- * `enumerateValidAccountIds`. Keeping validity injected is what lets this
14
- * module stay pure.
12
+ * fs-backed `isValidAccountId` (reads disk fresh every call); the `memory` and
13
+ * `filesystem` plugins pass `isProvisionedAccount` (cached-hit, re-scan-on-miss).
14
+ * Keeping validity injected is what lets this module stay pure.
15
+ *
16
+ * `notProvisionedReason` names the reject emitted when the injected check fails.
17
+ * It defaults to `cross-account-invalid-target` (the UI path, byte-identical to
18
+ * the 1440 landing). The MCP plugins pass `cross-account-target-not-provisioned`
19
+ * because their injected check re-scans disk on a miss (Task 1471) — so a false
20
+ * from it means the id is genuinely not on disk, distinct from a stale-cache
21
+ * miss (which no longer rejects). The distinct reason lets an operator tell a
22
+ * wrong/typo id from the (now-eliminated) created-after-boot case in the
23
+ * `[xacct] op=call … reason=` line.
15
24
  */
16
- function resolveEffectiveAccountCore(input, isValidAccountId) {
25
+ function resolveEffectiveAccountCore(input, isValidAccountId, notProvisionedReason = "cross-account-invalid-target") {
17
26
  const { ownAccountId, houseScope, targetAccountId } = input;
18
27
  if (!targetAccountId)
19
28
  return { accountId: ownAccountId };
20
29
  if (!houseScope)
21
30
  return { reject: "cross-account-denied" };
22
31
  if (!isValidAccountId(targetAccountId))
23
- return { reject: "cross-account-invalid-target" };
32
+ return { reject: notProvisionedReason };
24
33
  return { accountId: targetAccountId };
25
34
  }
26
35
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAyBA,kEASC;AAMD,kDAEC;AA5BD;;;;;;;;;;GAUG;AACH,SAAgB,2BAA2B,CACzC,KAA2F,EAC3F,gBAAyC;IAEzC,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,KAAK,CAAC;IAC5D,IAAI,CAAC,eAAe;QAAE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;IACzD,IAAI,CAAC,UAAU;QAAE,OAAO,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;IAC3D,IAAI,CAAC,gBAAgB,CAAC,eAAe,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,8BAA8B,EAAE,CAAC;IAC1F,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,CAAC;AACxC,CAAC;AAED;;;GAGG;AACH,SAAgB,mBAAmB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACtE,OAAO,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9D,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAuCA,kEAYC;AAMD,kDAEC;AAxCD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAgB,2BAA2B,CACzC,KAA2F,EAC3F,gBAAyC,EACzC,uBAE6C,8BAA8B;IAE3E,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,KAAK,CAAC;IAC5D,IAAI,CAAC,eAAe;QAAE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;IACzD,IAAI,CAAC,UAAU;QAAE,OAAO,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;IAC3D,IAAI,CAAC,gBAAgB,CAAC,eAAe,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;IAChF,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,CAAC;AACxC,CAAC;AAED;;;GAGG;AACH,SAAgB,mBAAmB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACtE,OAAO,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9D,CAAC"}