@rubytech/create-maxy-code 0.1.411 → 0.1.414

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/dist/__tests__/installer-cwd-pin-and-auth-verify.test.js +82 -0
  2. package/dist/index.js +44 -5
  3. package/package.json +1 -1
  4. package/payload/platform/lib/account-scope/dist/index.d.ts +27 -0
  5. package/payload/platform/lib/account-scope/dist/index.d.ts.map +1 -0
  6. package/payload/platform/lib/account-scope/dist/index.js +33 -0
  7. package/payload/platform/lib/account-scope/dist/index.js.map +1 -0
  8. package/payload/platform/lib/account-scope/src/index.ts +43 -0
  9. package/payload/platform/lib/account-scope/tsconfig.json +5 -0
  10. package/payload/platform/package.json +2 -2
  11. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  12. package/payload/platform/plugins/admin/hooks/lib/maxy-mcp-plugins.txt +1 -0
  13. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +37 -1
  14. package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +2 -0
  15. package/payload/platform/plugins/docs/references/cross-account-authority.md +25 -0
  16. package/payload/platform/plugins/docs/references/memory-guide.md +6 -0
  17. package/payload/platform/plugins/filesystem/.claude-plugin/plugin.json +21 -0
  18. package/payload/platform/plugins/filesystem/PLUGIN.md +63 -0
  19. package/payload/platform/plugins/filesystem/lib/mcp-spawn-tee/index.js +193 -0
  20. package/payload/platform/plugins/filesystem/lib/mcp-spawn-tee/package.json +3 -0
  21. package/payload/platform/plugins/filesystem/mcp/dist/index.d.ts +2 -0
  22. package/payload/platform/plugins/filesystem/mcp/dist/index.d.ts.map +1 -0
  23. package/payload/platform/plugins/filesystem/mcp/dist/index.js +91 -0
  24. package/payload/platform/plugins/filesystem/mcp/dist/index.js.map +1 -0
  25. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.d.ts +2 -0
  26. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.d.ts.map +1 -0
  27. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.js +89 -0
  28. package/payload/platform/plugins/filesystem/mcp/dist/lib/__tests__/scope.test.js.map +1 -0
  29. package/payload/platform/plugins/filesystem/mcp/dist/lib/result.d.ts +12 -0
  30. package/payload/platform/plugins/filesystem/mcp/dist/lib/result.d.ts.map +1 -0
  31. package/payload/platform/plugins/filesystem/mcp/dist/lib/result.js +6 -0
  32. package/payload/platform/plugins/filesystem/mcp/dist/lib/result.js.map +1 -0
  33. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts +45 -0
  34. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts.map +1 -0
  35. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js +86 -0
  36. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js.map +1 -0
  37. package/payload/platform/plugins/filesystem/mcp/dist/lib/walk.d.ts +11 -0
  38. package/payload/platform/plugins/filesystem/mcp/dist/lib/walk.d.ts.map +1 -0
  39. package/payload/platform/plugins/filesystem/mcp/dist/lib/walk.js +64 -0
  40. package/payload/platform/plugins/filesystem/mcp/dist/lib/walk.js.map +1 -0
  41. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.d.ts +2 -0
  42. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.d.ts.map +1 -0
  43. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.js +94 -0
  44. package/payload/platform/plugins/filesystem/mcp/dist/tools/__tests__/handlers.test.js.map +1 -0
  45. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-edit.d.ts +8 -0
  46. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-edit.d.ts.map +1 -0
  47. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-edit.js +40 -0
  48. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-edit.js.map +1 -0
  49. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-glob.d.ts +6 -0
  50. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-glob.d.ts.map +1 -0
  51. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-glob.js +13 -0
  52. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-glob.js.map +1 -0
  53. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-grep.d.ts +7 -0
  54. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-grep.d.ts.map +1 -0
  55. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-grep.js +71 -0
  56. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-grep.js.map +1 -0
  57. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-list.d.ts +6 -0
  58. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-list.d.ts.map +1 -0
  59. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-list.js +26 -0
  60. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-list.js.map +1 -0
  61. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-read.d.ts +6 -0
  62. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-read.d.ts.map +1 -0
  63. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-read.js +28 -0
  64. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-read.js.map +1 -0
  65. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-write.d.ts +7 -0
  66. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-write.d.ts.map +1 -0
  67. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-write.js +25 -0
  68. package/payload/platform/plugins/filesystem/mcp/dist/tools/file-write.js.map +1 -0
  69. package/payload/platform/plugins/filesystem/mcp/package.json +21 -0
  70. package/payload/platform/plugins/filesystem/mcp/vitest.config.ts +8 -0
  71. package/payload/platform/plugins/memory/PLUGIN.md +8 -0
  72. package/payload/platform/plugins/memory/mcp/dist/index-testable.d.ts +34 -0
  73. package/payload/platform/plugins/memory/mcp/dist/index-testable.d.ts.map +1 -0
  74. package/payload/platform/plugins/memory/mcp/dist/index-testable.js +60 -0
  75. package/payload/platform/plugins/memory/mcp/dist/index-testable.js.map +1 -0
  76. package/payload/platform/plugins/memory/mcp/dist/index.js +327 -146
  77. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  78. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-owner-userid.d.ts +15 -0
  79. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-owner-userid.d.ts.map +1 -0
  80. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-owner-userid.js +29 -0
  81. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-owner-userid.js.map +1 -0
  82. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.d.ts +2 -0
  83. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.d.ts.map +1 -0
  84. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.js +74 -0
  85. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.js.map +1 -0
  86. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-owner-userid.test.d.ts +2 -0
  87. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-owner-userid.test.d.ts.map +1 -0
  88. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-owner-userid.test.js +40 -0
  89. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-owner-userid.test.js.map +1 -0
  90. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.d.ts +2 -0
  91. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.d.ts.map +1 -0
  92. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.js +54 -0
  93. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-tool-account.test.js.map +1 -0
  94. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts +11 -0
  95. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts.map +1 -1
  96. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +13 -1
  97. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
  98. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +2 -0
  99. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  100. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +7 -0
  101. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  102. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.d.ts.map +1 -1
  103. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.js +6 -1
  104. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.js.map +1 -1
  105. package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.d.ts +7 -1
  106. package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.d.ts.map +1 -1
  107. package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.js +7 -2
  108. package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.js.map +1 -1
  109. package/payload/platform/services/claude-session-manager/dist/config.d.ts +37 -0
  110. package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
  111. package/payload/platform/services/claude-session-manager/dist/config.js +34 -1
  112. package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
  113. package/payload/platform/services/claude-session-manager/dist/cross-account-census.d.ts +34 -0
  114. package/payload/platform/services/claude-session-manager/dist/cross-account-census.d.ts.map +1 -0
  115. package/payload/platform/services/claude-session-manager/dist/cross-account-census.js +98 -0
  116. package/payload/platform/services/claude-session-manager/dist/cross-account-census.js.map +1 -0
  117. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  118. package/payload/platform/services/claude-session-manager/dist/http-server.js +10 -0
  119. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  120. package/payload/platform/services/claude-session-manager/dist/index.js +17 -0
  121. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  122. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  123. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +25 -0
  124. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  125. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +7 -0
  126. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -1
  127. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +8 -0
  128. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -1
  129. package/payload/server/{chunk-ZYS5V5QG.js → chunk-3Z5WE53X.js} +4 -0
  130. package/payload/server/maxy-edge.js +1 -1
  131. package/payload/server/public/assets/{AdminLoginScreens-BRQgI2lW.js → AdminLoginScreens-CYPTMmTk.js} +1 -1
  132. package/payload/server/public/assets/{AdminShell-By9n5icG.js → AdminShell-CZH_EZIy.js} +1 -1
  133. package/payload/server/public/assets/{Checkbox-DjEWgYu1.js → Checkbox-B0pbYJDQ.js} +1 -1
  134. package/payload/server/public/assets/{Transcript-BejWvOaO.js → Transcript-CYaAPBFX.js} +1 -1
  135. package/payload/server/public/assets/{admin-BFEMryQm.js → admin-BKDMSRto.js} +1 -1
  136. package/payload/server/public/assets/{browser-CMJMEUz6.js → browser-CxeMDDZA.js} +1 -1
  137. package/payload/server/public/assets/{calendar-C6ZGlHEP.js → calendar-DiIiTY9-.js} +1 -1
  138. package/payload/server/public/assets/chat-C56Oqvt1.js +1 -0
  139. package/payload/server/public/assets/chevron-left-Da-iZGi_.js +1 -0
  140. package/payload/server/public/assets/data-ZUdT44Ba.js +1 -0
  141. package/payload/server/public/assets/{graph-DIrXtPLv.js → graph-DC3dwnMZ.js} +1 -1
  142. package/payload/server/public/assets/{graph-labels-BtPf1Jom.js → graph-labels-Csnovm_c.js} +1 -1
  143. package/payload/server/public/assets/{maximize-2-DeJ4K8kL.js → maximize-2-WRUJsg5n.js} +1 -1
  144. package/payload/server/public/assets/{operator-B8QhqzLI.js → operator-BpzeHTAw.js} +1 -1
  145. package/payload/server/public/assets/{page-iFOy0dFx.js → page-8jJOxx6y.js} +1 -1
  146. package/payload/server/public/assets/{page-Df6KYkf4.js → page-IQDLdebG.js} +1 -1
  147. package/payload/server/public/assets/{public-BrUIRQZd.js → public-Dqg7jwjq.js} +1 -1
  148. package/payload/server/public/assets/{rotate-ccw-CYfntpwu.js → rotate-ccw-BayMWwfc.js} +1 -1
  149. package/payload/server/public/assets/{useSubAccountSwitcher-Cvnc_fwQ.css → useSubAccountSwitcher-CRZfyJXR.css} +1 -1
  150. package/payload/server/public/browser.html +4 -4
  151. package/payload/server/public/calendar.html +5 -5
  152. package/payload/server/public/chat.html +9 -9
  153. package/payload/server/public/data.html +8 -8
  154. package/payload/server/public/graph.html +9 -9
  155. package/payload/server/public/index.html +11 -11
  156. package/payload/server/public/operator.html +11 -11
  157. package/payload/server/public/public.html +9 -9
  158. package/payload/server/server.js +72 -15
  159. package/payload/server/public/assets/chat-B7s62Nnp.js +0 -1
  160. package/payload/server/public/assets/chevron-left-Dfjcaiyr.js +0 -1
  161. package/payload/server/public/assets/data-741ZbBwR.js +0 -1
  162. /package/payload/server/public/assets/{useSubAccountSwitcher-BK_KfOLC.js → useSubAccountSwitcher-C9aLg8yI.js} +0 -0
@@ -0,0 +1,82 @@
1
+ // Static-grep contracts for Task 1462 — the installer must survive being
2
+ // invoked from a working directory that deployPayload() later wipes.
3
+ //
4
+ // Three defects were compounding on the customer Pi upgrade:
5
+ // A. main() never pins the process CWD, so once deployPayload() rmSync's the
6
+ // operator's CWD (they ran `npx` from inside ~/.<brand>/platform), every
7
+ // subprocess inherits a deleted inode → JVM `getcwd() failed` and Bun
8
+ // ENOENT.
9
+ // B. resetNeo4jAuth returned the freshly-generated password even when the
10
+ // 15x cypher-shell verify loop never passed, so the app persisted a
11
+ // password Neo4j never actually accepted.
12
+ // C. the version marker was stamped mid-deployPayload, before plugin
13
+ // registration could abort, so an aborted upgrade reported success.
14
+ //
15
+ // Why static-grep, not behaviour: the touched paths spawn (systemctl, brew,
16
+ // launchctl, cypher-shell) and run the whole install on import — the same
17
+ // reason macos-darwin-branch.test.ts contracts resetNeo4jAuth by shape. This
18
+ // suite pins the code shape so a refactor that drops a fix fails loudly here.
19
+ import test from "node:test";
20
+ import assert from "node:assert/strict";
21
+ import { readFileSync } from "node:fs";
22
+ import { fileURLToPath } from "node:url";
23
+ import { dirname, resolve } from "node:path";
24
+ // dist/__tests__/installer-cwd-pin-and-auth-verify.test.js → ../../src/index.ts
25
+ const here = dirname(fileURLToPath(import.meta.url));
26
+ const INDEX_TS = resolve(here, "../../src/index.ts");
27
+ const SRC = readFileSync(INDEX_TS, "utf-8");
28
+ // --- Defect A: CWD pin ---
29
+ test("installer pins the process CWD to a never-wiped directory at startup", () => {
30
+ assert.match(SRC, /process\.chdir\(/, "startup must call process.chdir to escape a wipeable inherited CWD");
31
+ // The pin target is the operator HOME (never wiped — deployPayload only
32
+ // rmSync's subdirs of INSTALL_DIR), with a "/" fallback if the chdir throws.
33
+ assert.match(SRC, /process\.chdir\(process\.env\.HOME\s*\?\?\s*["']\/["']\)/, "CWD pin target must be process.env.HOME with a '/' fallback");
34
+ assert.match(SRC, /process\.chdir\(["']\/["']\)/, "a '/' fallback chdir must exist for when the HOME chdir throws");
35
+ });
36
+ test("installer logs the resolved pinned CWD", () => {
37
+ assert.match(SRC, /cwd-pinned to=/, "startup must emit a grep-able resolved-CWD line");
38
+ });
39
+ test("the CWD pin runs before deployPayload wipes the tree", () => {
40
+ const chdirAt = SRC.indexOf("process.chdir(");
41
+ // `deployPayload();` (with semicolon) is the call site — the definition is
42
+ // `function deployPayload(): void`, so the semicolon disambiguates.
43
+ const deployCallAt = SRC.indexOf("deployPayload();");
44
+ assert.ok(chdirAt !== -1 && deployCallAt !== -1, "both anchors must exist");
45
+ assert.ok(chdirAt < deployCallAt, "process.chdir must appear before the deployPayload() call so no rmSync runs under a live inherited CWD");
46
+ });
47
+ // --- Defect B: resetNeo4jAuth verifies before returning ---
48
+ test("resetNeo4jAuth records whether the post-reset auth check ever passed", () => {
49
+ assert.match(SRC, /\[neo4j-auth-reset\] verified=/, "resetNeo4jAuth must emit a verified=<bool> port=<n> post-condition line");
50
+ });
51
+ test("resetNeo4jAuth throws instead of returning an unverified password", () => {
52
+ // The old shape returned `password` unconditionally after the wait loop.
53
+ // The fix guards the return on a `verified` flag and throws otherwise.
54
+ const fn = SRC.slice(SRC.indexOf("function resetNeo4jAuth"), SRC.indexOf("function redactInstallLogs"));
55
+ assert.ok(fn.length > 0, "resetNeo4jAuth body must be locatable");
56
+ const gateIdx = fn.search(/if\s*\(\s*!verified\s*\)/);
57
+ assert.ok(gateIdx !== -1, "resetNeo4jAuth must branch on !verified");
58
+ assert.match(fn, /throw new Error\(/, "resetNeo4jAuth must throw when the reset never verified");
59
+ // The credential can only be returned on the verified path: the !verified
60
+ // throw-gate must sit BEFORE `return password`, so an unverified reset can
61
+ // never fall through to a return. A positional check, not a
62
+ // `}...return password` regex — the fixed code legitimately has the
63
+ // throw-block's own `}` immediately before the return.
64
+ const returnIdx = fn.lastIndexOf("return password");
65
+ assert.ok(returnIdx !== -1, "resetNeo4jAuth must still return the password on the verified path");
66
+ assert.ok(gateIdx < returnIdx, "the !verified throw-gate must precede `return password` so an unverified reset can never return a credential");
67
+ });
68
+ // --- Defect C: version marker gated on full completion ---
69
+ test("deployPayload no longer stamps the version marker mid-deploy", () => {
70
+ const dp = SRC.slice(SRC.indexOf("function deployPayload"), SRC.indexOf("function registerLocalAndExternalPlugins"));
71
+ assert.ok(dp.length > 0, "deployPayload body must be locatable");
72
+ assert.ok(!/hostname\}-version`\)[\s\S]*?writeFileSync\([^\n]*PKG_VERSION/.test(dp), "deployPayload must not write the version marker — it moved to the end of a fully-successful install");
73
+ });
74
+ test("version marker is written only after plugin registration completes", () => {
75
+ // Anchor on the call site (`...Plugins();` with semicolon), not the
76
+ // `function ...Plugins(): void` definition — the definition sits far above
77
+ // main() and would make this pass trivially.
78
+ const registerCallAt = SRC.indexOf("registerLocalAndExternalPlugins();");
79
+ const markerAt = SRC.indexOf("version-marker written");
80
+ assert.ok(registerCallAt !== -1 && markerAt !== -1, "both anchors must exist");
81
+ assert.ok(markerAt > registerCallAt, "the version-marker write must appear after the registerLocalAndExternalPlugins() call so an aborted run does not report success");
82
+ });
package/dist/index.js CHANGED
@@ -1338,16 +1338,30 @@ function resetNeo4jAuth(port = DEFAULT_NEO4J_PORT, dataDir = "/var/lib/neo4j", p
1338
1338
  shell("systemctl", ["start", serviceName], { sudo: true });
1339
1339
  }
1340
1340
  console.log(" Waiting for Neo4j to start...");
1341
+ let verified = false;
1341
1342
  for (let i = 0; i < 15; i++) {
1342
1343
  const check = spawnSync("cypher-shell", [
1343
1344
  "-u", "neo4j", "-p", password,
1344
1345
  "-a", `bolt://localhost:${port}`,
1345
1346
  "RETURN 1",
1346
1347
  ], { stdio: "pipe", timeout: 5000 });
1347
- if (check.status === 0)
1348
+ if (check.status === 0) {
1349
+ verified = true;
1348
1350
  break;
1351
+ }
1349
1352
  spawnSync("sleep", ["2"]);
1350
1353
  }
1354
+ // Post-condition: the new password only becomes authoritative once a
1355
+ // cypher-shell login actually succeeds. Returning an unverified password let
1356
+ // ensureNeo4jPassword persist a credential Neo4j never accepted — a hard
1357
+ // mismatch no restart can heal (Task 1462). The commonest cause of a
1358
+ // never-verifying reset is a corrupted set-initial-password from a deleted
1359
+ // CWD (JVM `getcwd() failed`); the CWD pin removes that, this throw stops the
1360
+ // installer writing a bad password if it still fails.
1361
+ logFile(`[neo4j-auth-reset] verified=${verified} port=${port}`);
1362
+ if (!verified) {
1363
+ throw new Error(`Neo4j auth reset did not verify on port ${port}: cypher-shell RETURN 1 failed on all 15 attempts. The new password was NOT confirmed and will not be persisted. Check the install log for a JVM \`getcwd() failed\` (deleted install CWD) or a Neo4j that never accepted auth.`);
1364
+ }
1351
1365
  return password;
1352
1366
  }
1353
1367
  /**
@@ -2478,10 +2492,10 @@ function deployPayload() {
2478
2492
  // Mirrors checkAdminAuthInvariant() in platform/lib/admins-write/src/index.ts;
2479
2493
  // future divergence between the two should be caught by the test suite.
2480
2494
  runInstallInvariantCheck(persistentUsersFile, join(INSTALL_DIR, "data", "accounts"));
2481
- // Write version marker so the running platform knows which create-maxy produced this deployment
2482
- const versionMarkerPath = join(configDir, `.${BRAND.hostname}-version`);
2483
- writeFileSync(versionMarkerPath, PKG_VERSION, "utf-8");
2484
- console.log(` [install] version-marker written path=${versionMarkerPath} version=${PKG_VERSION}`);
2495
+ // The version marker is NOT written here. It is stamped at the very end of a
2496
+ // fully-successful install (after plugin registration and every throwing
2497
+ // step), so an aborted upgrade cannot leave a marker that reports success it
2498
+ // did not complete. Task 1462.
2485
2499
  console.log(` Deployed to ${INSTALL_DIR}`);
2486
2500
  }
2487
2501
  // register the local + external Claude Code plugins after the
@@ -4983,6 +4997,22 @@ const PLATFORM_HEADER = `[create-maxy] platform=${PLATFORM} arch=${process.arch}
4983
4997
  console.log(PLATFORM_HEADER);
4984
4998
  console.log(`[create-maxy] log=${LOG_FILE} persist=${PERSIST_DIR}`);
4985
4999
  initLogging();
5000
+ // Pin the process CWD before any step runs. npx inherits the operator's shell
5001
+ // CWD; if they invoked the upgrade from inside the install tree (e.g. `cd
5002
+ // ~/.<brand>/platform && npx ...`), deployPayload()'s rmSync deletes that live
5003
+ // CWD mid-run and every subprocess spawned afterward inherits a deleted inode
5004
+ // (JVM `getcwd() failed` during the Neo4j auth reset; Bun ENOENT during plugin
5005
+ // registration). HOME is never wiped — deployPayload only rmSync's subdirs of
5006
+ // INSTALL_DIR — so it is a safe anchor; fall back to "/" if the HOME chdir
5007
+ // throws. Task 1462.
5008
+ try {
5009
+ process.chdir(process.env.HOME ?? "/");
5010
+ }
5011
+ catch {
5012
+ process.chdir("/");
5013
+ }
5014
+ console.log(` [create-maxy] cwd-pinned to=${process.cwd()}`);
5015
+ logFile(`[create-maxy] cwd-pinned to=${process.cwd()}`);
4986
5016
  console.log("================================================================");
4987
5017
  console.log(` ${BRAND.productName} — ${BRAND.tagline}. (${BRAND.hostname} v${PKG_VERSION})`);
4988
5018
  console.log("================================================================");
@@ -5066,6 +5096,15 @@ try {
5066
5096
  // (the reboot-loop this defends against). A partial/failed install exits
5067
5097
  // before this point and never writes the drop-in.
5068
5098
  configureHardwareWatchdog();
5099
+ // Stamp the version marker as the last thing a fully-successful install does.
5100
+ // Every throwing step (plugin registration, service install, account setup)
5101
+ // runs before this, so an aborted upgrade exits via the catch below and never
5102
+ // writes it — the running platform never reports a version it did not finish
5103
+ // installing. platform/config is excluded from the Task 848 freeze above, so
5104
+ // it is still writable here. Task 1462.
5105
+ const versionMarkerPath = join(INSTALL_DIR, "platform/config", `.${BRAND.hostname}-version`);
5106
+ writeFileSync(versionMarkerPath, PKG_VERSION, "utf-8");
5107
+ console.log(` [install] version-marker written path=${versionMarkerPath} version=${PKG_VERSION}`);
5069
5108
  console.log("");
5070
5109
  console.log("================================================================");
5071
5110
  console.log("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.411",
3
+ "version": "0.1.414",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -0,0 +1,27 @@
1
+ export type EffectiveAccount = {
2
+ accountId: string;
3
+ } | {
4
+ reject: "cross-account-denied" | "cross-account-invalid-target";
5
+ };
6
+ /**
7
+ * Resolve the account a call acts on. Fail-closed: only a house-scoped session
8
+ * may set `targetAccountId`, and only to a valid account. There is no house
9
+ * fallback — an unscoped session that names a target is denied, never silently
10
+ * downgraded to its own account.
11
+ *
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.
16
+ */
17
+ export declare function resolveEffectiveAccountCore(input: {
18
+ ownAccountId: string;
19
+ houseScope: string | null;
20
+ targetAccountId?: string | null;
21
+ }, isValidAccountId: (id: string) => boolean): EffectiveAccount;
22
+ /**
23
+ * Boot-reader that consumers import to learn their house scope from the spawn
24
+ * env. An empty value reads as null — a non-house session carries no scope.
25
+ */
26
+ export declare function readHouseAdminScope(env?: NodeJS.ProcessEnv): string | null;
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveEffectiveAccountCore = resolveEffectiveAccountCore;
4
+ exports.readHouseAdminScope = readHouseAdminScope;
5
+ /**
6
+ * Resolve the account a call acts on. Fail-closed: only a house-scoped session
7
+ * may set `targetAccountId`, and only to a valid account. There is no house
8
+ * fallback — an unscoped session that names a target is denied, never silently
9
+ * downgraded to its own account.
10
+ *
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.
15
+ */
16
+ function resolveEffectiveAccountCore(input, isValidAccountId) {
17
+ const { ownAccountId, houseScope, targetAccountId } = input;
18
+ if (!targetAccountId)
19
+ return { accountId: ownAccountId };
20
+ if (!houseScope)
21
+ return { reject: "cross-account-denied" };
22
+ if (!isValidAccountId(targetAccountId))
23
+ return { reject: "cross-account-invalid-target" };
24
+ return { accountId: targetAccountId };
25
+ }
26
+ /**
27
+ * Boot-reader that consumers import to learn their house scope from the spawn
28
+ * env. An empty value reads as null — a non-house session carries no scope.
29
+ */
30
+ function readHouseAdminScope(env = process.env) {
31
+ return env.HOUSE_ADMIN_SCOPE ? env.HOUSE_ADMIN_SCOPE : null;
32
+ }
33
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"}
@@ -0,0 +1,43 @@
1
+ // account-scope — the single fail-closed cross-account resolver.
2
+ //
3
+ // Task 1440 landed this logic inline in
4
+ // platform/ui/app/lib/claude-agent/account.ts. Task 1458 relocates the two
5
+ // pure primitives here so BOTH the UI lib (account.ts, which re-exports them)
6
+ // and MCP plugins (the `filesystem` plugin) import ONE definition — "one
7
+ // resolver, no drift", the same privilege-boundary discipline as the 1383/1385
8
+ // account-manager escalation. This module is pure: no filesystem access, no
9
+ // throw at module load, so it is safe to import into an MCP server that boots
10
+ // with empty env during plugin-system `tools/list` enumeration.
11
+ export type EffectiveAccount =
12
+ | { accountId: string }
13
+ | { reject: "cross-account-denied" | "cross-account-invalid-target" };
14
+
15
+ /**
16
+ * Resolve the account a call acts on. Fail-closed: only a house-scoped session
17
+ * may set `targetAccountId`, and only to a valid account. There is no house
18
+ * fallback — an unscoped session that names a target is denied, never silently
19
+ * downgraded to its own account.
20
+ *
21
+ * `isValidAccountId` is injected (required, no default): the UI passes its
22
+ * fs-backed `isValidAccountId`; the `filesystem` plugin passes a check over
23
+ * `enumerateValidAccountIds`. Keeping validity injected is what lets this
24
+ * module stay pure.
25
+ */
26
+ export function resolveEffectiveAccountCore(
27
+ input: { ownAccountId: string; houseScope: string | null; targetAccountId?: string | null },
28
+ isValidAccountId: (id: string) => boolean,
29
+ ): EffectiveAccount {
30
+ const { ownAccountId, houseScope, targetAccountId } = input;
31
+ if (!targetAccountId) return { accountId: ownAccountId };
32
+ if (!houseScope) return { reject: "cross-account-denied" };
33
+ if (!isValidAccountId(targetAccountId)) return { reject: "cross-account-invalid-target" };
34
+ return { accountId: targetAccountId };
35
+ }
36
+
37
+ /**
38
+ * Boot-reader that consumers import to learn their house scope from the spawn
39
+ * env. An empty value reads as null — a non-house session carries no scope.
40
+ */
41
+ export function readHouseAdminScope(env: NodeJS.ProcessEnv = process.env): string | null {
42
+ return env.HOUSE_ADMIN_SCOPE ? env.HOUSE_ADMIN_SCOPE : null;
43
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": { "outDir": "dist", "rootDir": "src" },
4
+ "include": ["src"]
5
+ }
@@ -6,8 +6,8 @@
6
6
  "services/*"
7
7
  ],
8
8
  "scripts": {
9
- "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-lifeline/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/embed-client/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/admin-access-password/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p lib/aeo-llms-txt-writer/tsconfig.json && tsc -p lib/obsidian-parser/tsconfig.json && tsc -p services/claude-session-manager/tsconfig.json && tsc -p services/whatsapp-channel/tsconfig.json && tsc -p services/webchat-channel/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
- "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-lifeline/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/embed-client/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/admin-access-password/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p lib/aeo-llms-txt-writer/tsconfig.json && tsc -p lib/obsidian-parser/tsconfig.json",
9
+ "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-lifeline/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/account-scope/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/embed-client/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/admin-access-password/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p lib/aeo-llms-txt-writer/tsconfig.json && tsc -p lib/obsidian-parser/tsconfig.json && tsc -p services/claude-session-manager/tsconfig.json && tsc -p services/whatsapp-channel/tsconfig.json && tsc -p services/webchat-channel/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
+ "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-lifeline/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/account-scope/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/embed-client/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/admin-access-password/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p lib/aeo-llms-txt-writer/tsconfig.json && tsc -p lib/obsidian-parser/tsconfig.json",
11
11
  "gen:canonical-tools": "node scripts/generate-canonical-tool-names.mjs",
12
12
  "build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
13
13
  "build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
@@ -54,6 +54,11 @@
54
54
  "source": "./email",
55
55
  "version": "0.1.0"
56
56
  },
57
+ {
58
+ "name": "filesystem",
59
+ "source": "./filesystem",
60
+ "version": "0.1.0"
61
+ },
57
62
  {
58
63
  "name": "graph",
59
64
  "source": "./graph",
@@ -4,6 +4,7 @@ browser
4
4
  connector
5
5
  contacts
6
6
  email
7
+ filesystem
7
8
  graph
8
9
  graph-viewer
9
10
  joblogic
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:69d2ec47598e75fac6f763994a077fd39ccda77767ddb90404dac2d556f5791c
4
+ content-hash: sha256:9b227cf8ab845f23cb1899d5b2ac2581cfac3aae772715bf02d288ee93d8fbc4
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -516,6 +516,36 @@ A third layer closes the same gap from the platform side: when `claude-agent.ts`
516
516
 
517
517
  **Boot-smoke as publish-time gate.** The memory MCP carries `scripts/boot-smoke.sh` that spawns `dist/index.js` with stub env, sleeps 2s, asserts `kill -0 <pid>`, and reports `[boot-smoke] memory ok|FAILED tail=<n-lines>`. Wired to `prepublish` in `plugins/memory/mcp/package.json`. The pattern is propagatable to other plugin MCPs — it's deliberately not generalised yet because each plugin's stub-env requirements differ (memory needs ACCOUNT_ID + PLATFORM_ROOT + NEO4J_URI + SESSION_ID; others differ).
518
518
 
519
+ ---
520
+ # House-Admin Cross-Account Authority
521
+ Source: https://docs.getmaxy.com/cross-account-authority.md
522
+
523
+ # House-admin cross-account authority
524
+
525
+ The house account is the operator-owned account that runs the admin assistant. Sub-accounts are isolated: each one's tools read and write only that account's data, which is what keeps tenants separate.
526
+
527
+ By default the admin assistant is scoped the same way — it acts on the house account. Cross-account authority lets a house-scoped admin session act on a named sub-account by passing that account as a target, while every other session (public visitors, a session already scoped to a sub-account, background specialists) is refused. The refusal is fail-closed: a session that is not house-scoped and names a target is denied, never quietly redirected to its own account.
528
+
529
+ The operator wants this for two surfaces: the account graph and the account filesystem. Each is a separate adoption; a coverage ledger tracks them and a background check confirms neither silently regresses.
530
+
531
+ ## Memory (graph)
532
+
533
+ The `memory` plugin gives the admin session access to a sub-account's knowledge graph. Its tools whose read or write is scoped solely by the account — search, lookup, write, update, edit, delete, edges, attachments, reports, conversation history, ingest, and graph maintenance — take an optional target. Omit it and the tool acts on the session's own account, exactly as before; pass a sub-account and a house-scoped admin session reads or writes that account's subgraph instead; any other session that names a target is refused.
534
+
535
+ `profile-read`/`update`/`delete` also take a target. They are keyed to a user as well as the account, so cross-account they cannot use the caller's own user (a house operator has no record inside a sub-account); instead they resolve the sub-account's owner — the single admin identity seeded with every account — and act on that owner's profile. A house-scoped session can therefore read or set a sub-account's profile (its owner's name, role, timezone, locale, and preferences); a target with no owner is refused rather than writing a stray record. Setting a sub-account owner's email or telephone this way is not available yet.
536
+
537
+ A few memory tools do **not** take a target. `session-compact` acts on the caller's own live session, which belongs to the account the operator is in, so pointing it at a sub-account has no meaning. `graph-prune-denylist-*` and `memory-ingest-extract` are not account-scoped at all (a shared deny-list and a temporary staging step), so a target would mean nothing. These stay on the session's own account.
538
+
539
+ ## Filesystem
540
+
541
+ The `filesystem` plugin gives the admin session direct access to the account working directory (`data/accounts/<id>/`) — `SOUL.md`, generated documents, screenshots, arbitrary files. It exists because the native file tools (Read, Write, Edit, and the shell) are locked to the session's own account and cannot take a target. Its six tools are `file-read`, `file-write`, `file-edit`, `file-list`, `file-glob`, and `file-grep`.
542
+
543
+ Omit the target and each tool acts on the session's own account, exactly like the native tools. Pass a sub-account as the target and a house-scoped admin session reads or writes that account's files instead; any other session that names a target is refused. Every path is confined to the resolved account directory — a path that climbs out with `../`, an absolute path outside the account root, or a symlink pointing away is rejected before anything is read or written, so the tools can never reach another account or the platform's own files.
544
+
545
+ ## Signals
546
+
547
+ A standing background check reconciles live sessions against the account roles and raises an alarm if any session still holds house authority for an account that is no longer the house account, so a stale grant cannot sit unnoticed. Each cross-account tool call also records its decision — who called, which account it targeted, whether it was allowed, and why — before it touches the graph or disk, and a rejected filesystem path escape is recorded on its own line. In normal operation neither an allowed cross-account call from a non-house session nor a path escape ever appears.
548
+
519
549
  ---
520
550
  # Install Overview
521
551
  Source: https://docs.getmaxy.com/install.md
@@ -1582,6 +1612,12 @@ If you want to write the public summary yourself — for instance, because the a
1582
1612
 
1583
1613
  People entries (`:Person`) are deliberately excluded from this dual-summary system. Notes about contacts are private by definition and never get a public-facing form.
1584
1614
 
1615
+ ## Working across accounts
1616
+
1617
+ If you run Maxy as the house account — the operator account that manages sub-accounts — you can point a memory action at one of your sub-accounts instead of your own. Name the account when you ask, for example "search Anneke's account for the roofing quote" or "add this note to the Jones account." Most memory actions honour this: search, read, write, edit, delete, reports, and conversation history. Profile actions honour it too — you can read and set a sub-account's profile (its owner's name, role, timezone, locale, and preferences) from the house account, for example "set the Jones account's name to Jane"; the profile is attributed to that sub-account's owner, so there is no orphan record and no PIN is needed. Setting a sub-account owner's email or telephone from the house account is not available yet. Compacting a conversation always acts on the session you are in, so it cannot be pointed at a sub-account.
1618
+
1619
+ Leave the account out and nothing changes — the action runs on your own account exactly as before. Only the house account can do this. A sub-account session that tries to reach another account is refused, and naming an account that does not exist is refused too. Sub-accounts stay fully isolated from each other.
1620
+
1585
1621
  ## Privacy
1586
1622
 
1587
1623
  All memory is stored on your local Raspberry Pi. The Neo4j database never leaves your network. Maxy does not sync memory to any cloud service or third party.
@@ -439,6 +439,8 @@ shasum -a 256 "${ACCOUNT_DIR}/e-sign/<DOC_REF>/base.pdf" | awk '{print $1}' \
439
439
 
440
440
  Before persisting, **read every rendered page** of `base.pdf` — full-bleed pages bleed correctly, no footer split across pages, no near-blank trailing page (§ 2 pagination; A4 dimensions are not proof of fit). If the deployed page is behind Cloudflare Access, do this base render in a session that can authenticate to Access — it is a one-time, operator-driven step, unlike dispatch (§ 6), which must never fetch the page at all. A new `DOC_REF` triggers a fresh render; an existing one keeps its persisted base.
441
441
 
442
+ **The deployed signing-page URL is an explicit hand-back.** This deploy produces one value every downstream caller needs: the signing page's absolute `https://` URL — the canonical host (§ 7: `https://<project>.pages.dev`, or a custom domain mapped to it) joined to the document's slug (§ 1, e.g. `https://<project>.pages.dev/quote-lakeside-7f3a9c.html`). Return it to whatever flow invoked e-sign. **Invariant:** any external document that embeds a "Sign Online" / acceptance link — a quote, a proposal, a cover letter — must carry this exact value in that link, and **must not be finalised, exported, or sent before this deploy has produced it**. A Sign-Online link cannot point at a page that does not yet exist: deploy first, hand back the URL, then the caller injects it and renders. The quotation client-quote path is one such caller and gates on this — `sitedesk-job/skills/quotation/references/quote-generation.md` refuses to export a client quote whose Sign-Online anchor is not the live `https://` URL handed back here.
443
+
442
444
  ---
443
445
 
444
446
  ## 6. Sweep + dispatch
@@ -0,0 +1,25 @@
1
+ # House-admin cross-account authority
2
+
3
+ The house account is the operator-owned account that runs the admin assistant. Sub-accounts are isolated: each one's tools read and write only that account's data, which is what keeps tenants separate.
4
+
5
+ By default the admin assistant is scoped the same way — it acts on the house account. Cross-account authority lets a house-scoped admin session act on a named sub-account by passing that account as a target, while every other session (public visitors, a session already scoped to a sub-account, background specialists) is refused. The refusal is fail-closed: a session that is not house-scoped and names a target is denied, never quietly redirected to its own account.
6
+
7
+ The operator wants this for two surfaces: the account graph and the account filesystem. Each is a separate adoption; a coverage ledger tracks them and a background check confirms neither silently regresses.
8
+
9
+ ## Memory (graph)
10
+
11
+ The `memory` plugin gives the admin session access to a sub-account's knowledge graph. Its tools whose read or write is scoped solely by the account — search, lookup, write, update, edit, delete, edges, attachments, reports, conversation history, ingest, and graph maintenance — take an optional target. Omit it and the tool acts on the session's own account, exactly as before; pass a sub-account and a house-scoped admin session reads or writes that account's subgraph instead; any other session that names a target is refused.
12
+
13
+ `profile-read`/`update`/`delete` also take a target. They are keyed to a user as well as the account, so cross-account they cannot use the caller's own user (a house operator has no record inside a sub-account); instead they resolve the sub-account's owner — the single admin identity seeded with every account — and act on that owner's profile. A house-scoped session can therefore read or set a sub-account's profile (its owner's name, role, timezone, locale, and preferences); a target with no owner is refused rather than writing a stray record. Setting a sub-account owner's email or telephone this way is not available yet.
14
+
15
+ A few memory tools do **not** take a target. `session-compact` acts on the caller's own live session, which belongs to the account the operator is in, so pointing it at a sub-account has no meaning. `graph-prune-denylist-*` and `memory-ingest-extract` are not account-scoped at all (a shared deny-list and a temporary staging step), so a target would mean nothing. These stay on the session's own account.
16
+
17
+ ## Filesystem
18
+
19
+ The `filesystem` plugin gives the admin session direct access to the account working directory (`data/accounts/<id>/`) — `SOUL.md`, generated documents, screenshots, arbitrary files. It exists because the native file tools (Read, Write, Edit, and the shell) are locked to the session's own account and cannot take a target. Its six tools are `file-read`, `file-write`, `file-edit`, `file-list`, `file-glob`, and `file-grep`.
20
+
21
+ Omit the target and each tool acts on the session's own account, exactly like the native tools. Pass a sub-account as the target and a house-scoped admin session reads or writes that account's files instead; any other session that names a target is refused. Every path is confined to the resolved account directory — a path that climbs out with `../`, an absolute path outside the account root, or a symlink pointing away is rejected before anything is read or written, so the tools can never reach another account or the platform's own files.
22
+
23
+ ## Signals
24
+
25
+ A standing background check reconciles live sessions against the account roles and raises an alarm if any session still holds house authority for an account that is no longer the house account, so a stale grant cannot sit unnoticed. Each cross-account tool call also records its decision — who called, which account it targeted, whether it was allowed, and why — before it touches the graph or disk, and a rejected filesystem path escape is recorded on its own line. In normal operation neither an allowed cross-account call from a non-house session nor a path escape ever appears.
@@ -156,6 +156,12 @@ If you want to write the public summary yourself — for instance, because the a
156
156
 
157
157
  People entries (`:Person`) are deliberately excluded from this dual-summary system. Notes about contacts are private by definition and never get a public-facing form.
158
158
 
159
+ ## Working across accounts
160
+
161
+ If you run {{productName}} as the house account — the operator account that manages sub-accounts — you can point a memory action at one of your sub-accounts instead of your own. Name the account when you ask, for example "search Anneke's account for the roofing quote" or "add this note to the Jones account." Most memory actions honour this: search, read, write, edit, delete, reports, and conversation history. Profile actions honour it too — you can read and set a sub-account's profile (its owner's name, role, timezone, locale, and preferences) from the house account, for example "set the Jones account's name to Jane"; the profile is attributed to that sub-account's owner, so there is no orphan record and no PIN is needed. Setting a sub-account owner's email or telephone from the house account is not available yet. Compacting a conversation always acts on the session you are in, so it cannot be pointed at a sub-account.
162
+
163
+ Leave the account out and nothing changes — the action runs on your own account exactly as before. Only the house account can do this. A sub-account session that tries to reach another account is refused, and naming an account that does not exist is refused too. Sub-accounts stay fully isolated from each other.
164
+
159
165
  ## Privacy
160
166
 
161
167
  All memory is stored on your local Raspberry Pi. The Neo4j database never leaves your network. {{productName}} does not sync memory to any cloud service or third party.
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "filesystem",
3
+ "description": "Account-scoped file access. Read, write, edit, list, glob, and grep files under the session's own account directory; the house admin may reach any sub-account's files by naming targetAccountId, gated fail-closed and confined to that account's directory.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Rubytech LLC"
7
+ },
8
+ "mcpServers": {
9
+ "filesystem": {
10
+ "type": "stdio",
11
+ "command": "node",
12
+ "args": [
13
+ "${CLAUDE_PLUGIN_ROOT}/lib/mcp-spawn-tee/index.js",
14
+ "${CLAUDE_PLUGIN_ROOT}/mcp/dist/index.js"
15
+ ],
16
+ "env": {
17
+ "MCP_SPAWN_TEE_NAME": "filesystem"
18
+ }
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: filesystem
3
+ description: "Account-scoped file access. Read, write, edit, list, glob, and grep files under the session's own account directory; the house admin may reach any sub-account's files by naming targetAccountId, gated fail-closed and confined to that account's directory."
4
+ tools:
5
+ - name: file-read
6
+ publicAllowlist: false
7
+ adminAllowlist: true
8
+ - name: file-write
9
+ publicAllowlist: false
10
+ adminAllowlist: true
11
+ - name: file-edit
12
+ publicAllowlist: false
13
+ adminAllowlist: true
14
+ - name: file-list
15
+ publicAllowlist: false
16
+ adminAllowlist: true
17
+ - name: file-glob
18
+ publicAllowlist: false
19
+ adminAllowlist: true
20
+ - name: file-grep
21
+ publicAllowlist: false
22
+ adminAllowlist: true
23
+ metadata: {"platform":{"always":false,"embed":[],"pluginKey":"filesystem"}}
24
+ mcp:
25
+ command: node
26
+ args:
27
+ - ${PLATFORM_ROOT}/lib/mcp-spawn-tee/dist/index.js
28
+ - ${PLATFORM_ROOT}/plugins/filesystem/mcp/dist/index.js
29
+ env:
30
+ MCP_SPAWN_TEE_NAME: filesystem
31
+ LOG_DIR: ${LOG_DIR}
32
+ PLATFORM_ROOT: ${PLATFORM_ROOT}
33
+ ACCOUNT_ID: ${ACCOUNT_ID}
34
+ HOUSE_ADMIN_SCOPE: ${HOUSE_ADMIN_SCOPE}
35
+ SESSION_ID: ${SESSION_ID}
36
+ AGENT_SLUG: ${AGENT_SLUG}
37
+ mcp-manifest: auto
38
+ ---
39
+
40
+ # Filesystem
41
+
42
+ Direct access to the account working directory (`data/accounts/<id>/`) — `SOUL.md`, generated documents, screenshots, and arbitrary files. These tools complement the native Read/Write/Edit tools, which are locked to your own account; only these tools can reach another account's files, and only for the house admin.
43
+
44
+ ## Own account
45
+
46
+ Omit `targetAccountId`. Every tool takes a `path` relative to your account directory. `file-read`, `file-write`, `file-edit`, `file-list`, `file-glob`, and `file-grep` behave like their native counterparts but always resolve inside your account root.
47
+
48
+ ## Cross-account (house admin only)
49
+
50
+ Pass `targetAccountId` to operate on a sub-account's directory. This is honoured only for a house-scoped admin session. A sub-account, specialist, or public session that passes `targetAccountId` is refused with `cross-account-denied`; an unknown account id is refused with `cross-account-invalid-target`. Use this to inspect or repair a client account's files from the house session without switching accounts.
51
+
52
+ ## Boundaries
53
+
54
+ Every path is confined to the resolved account directory. A path that escapes it — `../`, an absolute path outside the account root, or a symlink pointing out — is rejected before any filesystem operation. Reads and greps skip files over 5 MB; grep skips binary files.
55
+
56
+ ## Tools
57
+
58
+ - `file-read` — Read a UTF-8 file. Args: `path`, optional `targetAccountId`.
59
+ - `file-write` — Create or overwrite a UTF-8 file, making parent directories. Args: `path`, `content`, optional `targetAccountId`.
60
+ - `file-edit` — Replace one unique occurrence of `oldString` with `newString`. Fails if `oldString` is missing or not unique. Args: `path`, `oldString`, `newString`, optional `targetAccountId`.
61
+ - `file-list` — List a directory's entries with their type. Args: optional `path` (defaults to the account root), optional `targetAccountId`.
62
+ - `file-glob` — List files matching a glob (`**`, `*`, `?`). Args: `pattern`, optional `targetAccountId`.
63
+ - `file-grep` — Search file contents for a JavaScript regular expression, returning `file:line:preview`. Args: `pattern`, optional `path`, optional `targetAccountId`.