@rubytech/create-maxy-code 0.1.435 → 0.1.437
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.
- package/package.json +1 -1
- package/payload/platform/docs/superpowers/plans/2026-07-11-task-1557-operator-on-behalf-preference-attribution.md +542 -0
- package/payload/platform/docs/superpowers/specs/2026-07-11-task-1557-operator-on-behalf-preference-attribution-design.md +90 -0
- package/payload/platform/lib/active-rules/dist/index.d.ts +14 -0
- package/payload/platform/lib/active-rules/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/active-rules/dist/index.js +56 -9
- package/payload/platform/lib/active-rules/dist/index.js.map +1 -1
- package/payload/platform/lib/active-rules/src/index.test.ts +106 -0
- package/payload/platform/lib/active-rules/src/index.ts +98 -9
- package/payload/platform/lib/embed-client/dist/index.d.ts +1 -1
- package/payload/platform/lib/embed-client/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/embed-client/dist/index.js +42 -13
- package/payload/platform/lib/embed-client/dist/index.js.map +1 -1
- package/payload/platform/lib/embed-client/src/index.ts +44 -13
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +7 -7
- package/payload/platform/plugins/docs/references/admin-session.md +2 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +2 -2
- package/payload/platform/plugins/docs/references/internals.md +4 -4
- package/payload/platform/plugins/memory/PLUGIN.md +2 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +54 -25
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/embed-progress-reporter.d.ts +22 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/embed-progress-reporter.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/embed-progress-reporter.js +43 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/embed-progress-reporter.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/profile-identity.d.ts +34 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/profile-identity.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/profile-identity.js +22 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/profile-identity.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-admin-home-account.d.ts +13 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-admin-home-account.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-admin-home-account.js +26 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-admin-home-account.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-progress-reporter.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-progress-reporter.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-progress-reporter.test.js +52 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-progress-reporter.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-progress.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-progress.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-progress.test.js +56 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-progress.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-throughput.test.js +30 -5
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embed-throughput.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embeddings-cap.test.js +9 -7
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/embeddings-cap.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write-progress.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write-progress.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write-progress.test.js +79 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write-progress.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-write-phase-progress.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-write-phase-progress.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-write-phase-progress.test.js +50 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-write-phase-progress.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-reindex-progress.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-reindex-progress.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-reindex-progress.test.js +114 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-reindex-progress.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-identity.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-identity.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-identity.test.js +72 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-identity.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-read-active-rules.test.js +5 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-read-active-rules.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-bootstrap.test.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-bootstrap.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-admin-home-account.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-admin-home-account.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-admin-home-account.test.js +38 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/resolve-admin-home-account.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts +15 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +17 -17
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +9 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +22 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js +19 -4
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js +4 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +5 -3
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
- package/payload/platform/plugins/memory/skills/conversational-memory/SKILL.md +8 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +16 -6
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts +7 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js +9 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.d.ts +4 -1
- package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.js.map +1 -1
- package/payload/server/{chunk-DAYMXQG2.js → chunk-LMLVOZ6G.js} +7 -2
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/{AdminLoginScreens-CMh1pdOp.js → AdminLoginScreens-DeR71H29.js} +1 -1
- package/payload/server/public/assets/AdminShell-DlAVQUda.js +2 -0
- package/payload/server/public/assets/{Checkbox-OCfZCTCO.js → Checkbox-B629mOPp.js} +1 -1
- package/payload/server/public/assets/{admin-Bi2uYnAg.js → admin-C15h-NL0.js} +1 -1
- package/payload/server/public/assets/{arc-Bm9oSOth.js → arc-qKfXIt8v.js} +1 -1
- package/payload/server/public/assets/architecture-YZFGNWBL-FPHT7s-C.js +1 -0
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-C2dK88g3.js → architectureDiagram-Q4EWVU46-DkVrGn7G.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DfLSsjCJ.js → blockDiagram-DXYQGD6D-DqavswGH.js} +1 -1
- package/payload/server/public/assets/browser-B7xQLIRK.js +1 -0
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Bxkmp6OL.js → c4Diagram-AHTNJAMY-B7fyKiXe.js} +1 -1
- package/payload/server/public/assets/calendar-A-vGm6pL.js +1 -0
- package/payload/server/public/assets/channel-j70eMvcL.js +1 -0
- package/payload/server/public/assets/chat-DAae6Z51.js +1 -0
- package/payload/server/public/assets/chevron-left-u4tjq7fI.js +1 -0
- package/payload/server/public/assets/{chunk-2KRD3SAO-BVNDFwNH.js → chunk-2KRD3SAO-DsgMtGkv.js} +1 -1
- package/payload/server/public/assets/{chunk-336JU56O-Daz6nMta.js → chunk-336JU56O-CkdISfNt.js} +2 -2
- package/payload/server/public/assets/chunk-426QAEUC-D-NcAlTW.js +1 -0
- package/payload/server/public/assets/{chunk-4BX2VUAB-DTldfvwP.js → chunk-4BX2VUAB-CYpiF2bl.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-DhM8pwRG.js → chunk-4TB4RGXK-CWyc_F7v.js} +1 -1
- package/payload/server/public/assets/{chunk-55IACEB6-CzTsbIHo.js → chunk-55IACEB6-BbzKMw2Z.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-BrcObZhe.js → chunk-5FUZZQ4R-u19Nai9e.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-SFqTuS11.js → chunk-5PVQY5BW-Ba8by6Uy.js} +1 -1
- package/payload/server/public/assets/{chunk-67CJDMHE-EIbrSGOo.js → chunk-67CJDMHE-BwUuxtyb.js} +1 -1
- package/payload/server/public/assets/{chunk-7N4EOEYR-B1D4Jorr.js → chunk-7N4EOEYR-BRH9iM9L.js} +1 -1
- package/payload/server/public/assets/{chunk-AA7GKIK3-BKHiX4cC.js → chunk-AA7GKIK3-BGoztgLL.js} +1 -1
- package/payload/server/public/assets/{chunk-BSJP7CBP-xBzu29m7.js → chunk-BSJP7CBP-C5GDAZTl.js} +1 -1
- package/payload/server/public/assets/{chunk-CIAEETIT-x2x0gMpL.js → chunk-CIAEETIT-ChY32VPS.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-D5UkD28M.js → chunk-EDXVE4YY-CPSD3p6p.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-Dbz67a7E.js → chunk-ENJZ2VHE-D1oL-iUr.js} +1 -1
- package/payload/server/public/assets/{chunk-FMBD7UC4-XfWvuu2F.js → chunk-FMBD7UC4-CPvYEr54.js} +1 -1
- package/payload/server/public/assets/{chunk-FOC6F5B3-DfOEDS4q.js → chunk-FOC6F5B3-9Yyj7Vbz.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-D_WEZZvg.js → chunk-ICPOFSXX-BaYgh8FR.js} +2 -2
- package/payload/server/public/assets/{chunk-K5T4RW27-7O627Xi1.js → chunk-K5T4RW27-5ucpeRvm.js} +1 -1
- package/payload/server/public/assets/{chunk-KGLVRYIC-BXI3XDP9.js → chunk-KGLVRYIC-DW-FHk7T.js} +1 -1
- package/payload/server/public/assets/{chunk-LIHQZDEY-B9cFNN0l.js → chunk-LIHQZDEY-CGGVfAyG.js} +1 -1
- package/payload/server/public/assets/{chunk-ORNJ4GCN-BR3UNDTn.js → chunk-ORNJ4GCN-Bv8EUa5d.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-2wKIEtFZ.js → chunk-OYMX7WX6-C1mXJnyw.js} +1 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-CAwbQBtf.js +1 -0
- package/payload/server/public/assets/{chunk-U2HBQHQK-augM3AXB.js → chunk-U2HBQHQK-PwSQz6EP.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-BAM9q9ZQ.js → chunk-X2U36JSP--NPHTBlU.js} +1 -1
- package/payload/server/public/assets/{chunk-XPW4576I-DxPpji5P.js → chunk-XPW4576I-Bbc7Da32.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-BD3ClFXP.js → chunk-YZCP3GAM-D-9v8e--.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-CqxhLPep.js → chunk-ZZ45TVLE-CMPg670Q.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-Ct6Rn0vt.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-B_2XCIFv.js +1 -0
- package/payload/server/public/assets/clone-DjPWA_NH.js +1 -0
- package/payload/server/public/assets/{cose-bilkent-S5V4N54A-xywWX-9i.js → cose-bilkent-S5V4N54A-NVSb1Bhy.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-ChRpqqiH.js → dagre-KV5264BT-V2Ai1DFl.js} +1 -1
- package/payload/server/public/assets/{dagre-BaHjDQ-8.js → dagre-QfLcO-1j.js} +1 -1
- package/payload/server/public/assets/data-BNWpKx1d.js +1 -0
- package/payload/server/public/assets/{diagram-5BDNPKRD-KjwTpyBi.js → diagram-5BDNPKRD-Caw21693.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-DZO9njAF.js → diagram-G4DWMVQ6-CgwVkT2q.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-Bus-U-TI.js → diagram-MMDJMWI5-DFUrLZhs.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-CfJodFgq.js → diagram-TYMM5635-CEEiEuZJ.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-Bt5zqWiW.js → erDiagram-SMLLAGMA-CG10NrT3.js} +1 -1
- package/payload/server/public/assets/file-download-4mKJxUi-.js +1 -0
- package/payload/server/public/assets/{flatten-Bpu1L63d.js → flatten-DAO0sfCM.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-B64Q590S.js → flowDiagram-DWJPFMVM-BqkV9Okv.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-BWdgCEpH.js → ganttDiagram-T4ZO3ILL-DRiCDQF6.js} +1 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-B8Hx7f-D.js +1 -0
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-Djq2B0WT.js → gitGraphDiagram-UUTBAWPF-BZV2voyy.js} +1 -1
- package/payload/server/public/assets/{graph-CrJ-itbq.js → graph-D0orIRAh.js} +3 -3
- package/payload/server/public/assets/{graph-labels-CmCWaKJT.js → graph-labels-B8YAdo3Z.js} +1 -1
- package/payload/server/public/assets/{graphlib-CNJh9_lx.js → graphlib-DGPThX8M.js} +1 -1
- package/payload/server/public/assets/info-OMHHGYJF-BicCJTqU.js +1 -0
- package/payload/server/public/assets/infoDiagram-42DDH7IO-tf-8rOez.js +2 -0
- package/payload/server/public/assets/{isEmpty-DkDEVnMZ.js → isEmpty-Cb_U81C6.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-V3ACkWCW.js → ishikawaDiagram-UXIWVN3A-BKsAxSvp.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-9T0qZeH3.js → journeyDiagram-VCZTEJTY-CmliuOd-.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-CxZ0ukx3.js → kanban-definition-6JOO6SKY-DAlElMb-.js} +1 -1
- package/payload/server/public/assets/{line-DUHKCQZT.js → line-DKZqLKAC.js} +1 -1
- package/payload/server/public/assets/{linear-BgIBblun.js → linear-CbSGdh1x.js} +1 -1
- package/payload/server/public/assets/{maximize-2-_cJWIgx2.js → maximize-2-CxstTgoK.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-CyHygs0-.js → mermaid-parser.core-DFvbao9C.js} +2 -2
- package/payload/server/public/assets/{mermaid.core-L55fJBSx.js → mermaid.core-C0vEBxDm.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-DczaUX_E.js → mindmap-definition-QFDTVHPH-CShASUKd.js} +1 -1
- package/payload/server/public/assets/operator-DZzuotDB.js +1 -0
- package/payload/server/public/assets/{ordinal-DK3QISRR.js → ordinal-H0tVmBDP.js} +1 -1
- package/payload/server/public/assets/packet-4T2RLAQJ-Ckn_5hq0.js +1 -0
- package/payload/server/public/assets/page-3g0A19Rv.js +32 -0
- package/payload/server/public/assets/page-Ck_pLhAk.js +1 -0
- package/payload/server/public/assets/{pdf-render-COLA4aPx.js → pdf-render-jAQU0nXt.js} +1 -1
- package/payload/server/public/assets/pie-ZZUOXDRM-DzlGQW3J.js +1 -0
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-Da5TdnNt.js → pieDiagram-DEJITSTG-CBZHpDAJ.js} +1 -1
- package/payload/server/public/assets/{public-DmBKnBkr.js → public-WFUQw9AD.js} +1 -1
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-DX7J81uh.js → quadrantDiagram-34T5L4WZ-n3bB3K91.js} +1 -1
- package/payload/server/public/assets/radar-PYXPWWZC-BHo2YD8b.js +1 -0
- package/payload/server/public/assets/{reduce-uzWdoyL7.js → reduce-DAvwat-a.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-DJQGrZPG.js → requirementDiagram-MS252O5E-DiDvQK3c.js} +1 -1
- package/payload/server/public/assets/rotate-ccw-COGQTi7n.js +1 -0
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-Ba8fGz96.js → sankeyDiagram-XADWPNL6-DWDQQZmd.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BmDPC7qx.js → sequenceDiagram-FGHM5R23-DmAqRlBT.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-1VkptYFz.js → stateDiagram-FHFEXIEX-D4R-n6bZ.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BcFRbGRu.js +1 -0
- package/payload/server/public/assets/tasks-B97VpwEJ.js +1 -0
- package/payload/server/public/assets/{time-entry-format-D477WAes.js → time-entry-format-C09rDHo9.js} +1 -1
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-B92U3T_f.js → timeline-definition-GMOUNBTQ-CjiHGP4l.js} +1 -1
- package/payload/server/public/assets/treeView-SZITEDCU-BtRr5vXp.js +1 -0
- package/payload/server/public/assets/treemap-W4RFUUIX-D8p0sbyo.js +1 -0
- package/payload/server/public/assets/useCopyFeedback-CSk9KjhY.js +1 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DLJFISqf.js +9 -0
- package/payload/server/public/assets/{useSubAccountSwitcher-BKCWKo97.css → useSubAccountSwitcher-Dzmd7Aip.css} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-CZPtVpM-.js → useVoiceRecorder-DCyJQkFK.js} +2 -2
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-Y6Wkihrs.js → vennDiagram-DHZGUBPP-CBdDQ7c-.js} +1 -1
- package/payload/server/public/assets/wardley-RL74JXVD-DgMn-9ZJ.js +1 -0
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-iJpfal2z.js → wardleyDiagram-NUSXRM2D-BlIQPKLb.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-S0bv7oH0.js → xychartDiagram-5P7HB3ND-CslKgh9B.js} +1 -1
- package/payload/server/public/browser.html +5 -5
- package/payload/server/public/calendar.html +6 -6
- package/payload/server/public/chat.html +13 -12
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +11 -10
- package/payload/server/public/index.html +14 -14
- package/payload/server/public/operator.html +15 -14
- package/payload/server/public/public.html +13 -12
- package/payload/server/public/tasks.html +5 -4
- package/payload/server/server.js +691 -542
- package/payload/server/public/assets/AdminShell-X9Bg3AKc.js +0 -2
- package/payload/server/public/assets/architecture-YZFGNWBL-Bbnxm4F4.js +0 -1
- package/payload/server/public/assets/browser-B0ieZy-b.js +0 -1
- package/payload/server/public/assets/calendar-CRL2_9Fl.js +0 -1
- package/payload/server/public/assets/channel-CatPTq-5.js +0 -1
- package/payload/server/public/assets/chat-C56bmC6C.js +0 -1
- package/payload/server/public/assets/chevron-left-B2DKUxLk.js +0 -1
- package/payload/server/public/assets/chunk-426QAEUC-CqLMwOdA.js +0 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-CjWy8NJ7.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-DHhR5rdN.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DH4pAdfu.js +0 -1
- package/payload/server/public/assets/clone-DCKzwjWW.js +0 -1
- package/payload/server/public/assets/data-C1ftMulP.js +0 -1
- package/payload/server/public/assets/file-download-CHTyUQqS.js +0 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-Ps5HDzLh.js +0 -1
- package/payload/server/public/assets/info-OMHHGYJF-gtlpwIig.js +0 -1
- package/payload/server/public/assets/infoDiagram-42DDH7IO-CPrWlrsa.js +0 -2
- package/payload/server/public/assets/operator-B4wN2c53.js +0 -1
- package/payload/server/public/assets/packet-4T2RLAQJ-DrSn1e0n.js +0 -1
- package/payload/server/public/assets/page-BIfOKi9I.js +0 -32
- package/payload/server/public/assets/page-C1G57kPZ.js +0 -1
- package/payload/server/public/assets/pie-ZZUOXDRM-C09i6N-g.js +0 -1
- package/payload/server/public/assets/radar-PYXPWWZC-CMZN2KQH.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BdgkPHuh.js +0 -1
- package/payload/server/public/assets/tasks-0OcRiBdQ.js +0 -1
- package/payload/server/public/assets/treeView-SZITEDCU-CeuUM5oL.js +0 -1
- package/payload/server/public/assets/treemap-W4RFUUIX-C--fAaJv.js +0 -1
- package/payload/server/public/assets/useCopyFeedback-CpfVOO26.js +0 -1
- package/payload/server/public/assets/useSubAccountSwitcher-Bo7bnFtC.js +0 -9
- package/payload/server/public/assets/wardley-RL74JXVD-4WPJ2UA9.js +0 -1
- /package/payload/server/public/assets/{_baseFor-Q4DeIqQC.js → _baseFor-D3BqAOBr.js} +0 -0
- /package/payload/server/public/assets/{array-WaOTgZu7.js → array-iYP9fzJp.js} +0 -0
- /package/payload/server/public/assets/{cytoscape.esm-CMmjo4hO.js → cytoscape.esm-BQSBzPsB.js} +0 -0
- /package/payload/server/public/assets/{defaultLocale-BxqJVxq5.js → defaultLocale-BrdA1gDi.js} +0 -0
- /package/payload/server/public/assets/{dist-Dx-cpU53.js → dist-Cvgxm7qv.js} +0 -0
- /package/payload/server/public/assets/{init-DX7c5Edh.js → init-Bx0S6pkp.js} +0 -0
- /package/payload/server/public/assets/{katex-B2dtGfSp.js → katex-BsiH_a6d.js} +0 -0
- /package/payload/server/public/assets/{path-CnSyroar.js → path-BLSgTy3w.js} +0 -0
- /package/payload/server/public/assets/{preload-helper-DqboRNgS.js → preload-helper-ClBMtM1g.js} +0 -0
- /package/payload/server/public/assets/{rough.esm-eLccZ4OJ.js → rough.esm-BMOtDaaS.js} +0 -0
- /package/payload/server/public/assets/{src-Dqs1wlnb.js → src-DV4LKv2E.js} +0 -0
package/package.json
CHANGED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
# Task 1557 — Operator-on-behalf Preference Attribution Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** A preference an operator states while working inside a client's sub-account attaches to that client's account owner, not to the operator.
|
|
6
|
+
|
|
7
|
+
**Architecture:** The decision lives in one place — `resolveProfileIdentity` in the memory MCP's `index.ts`. It gains an operator-on-behalf case: when the caller's `AdminUser` home account differs from the account the session is scoped to (a house operator inside a client's sub-account), the profile op attributes to the account's seeded owner. The decision is extracted into a pure classifier (`classifyProfileIdentity`) and a small DB helper (`resolveAdminHomeAccount`), both unit-tested; `index.ts` orchestrates the two and maps the result to the existing MCP reject envelope and bootstrap flags.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Node ESM, Neo4j driver, Vitest. All work is inside `platform/plugins/memory/mcp/`.
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- **Working directory for all builds/tests:** `platform/plugins/memory/mcp/`. Build with `npm run build`, test with `npm test` (vitest). The sibling `lib/*` packages must already be built (`dist/` present) — they are in this worktree.
|
|
14
|
+
- **Precision, no speculation:** implement exactly the four attribution cases in the spec (caller-is-admin-here, foreign-operator-with-owner, foreign-operator-no-owner, no-caller). No extra fallback paths.
|
|
15
|
+
- **Shipped skill markdown carries zero em-dashes** (`conversational-memory/SKILL.md`). Use commas or full stops.
|
|
16
|
+
- **The cross-account `[xacct] op=identity` log line stays byte-identical** to its current form; the operator-on-behalf case emits a distinct new line. PLUGIN.md documents both.
|
|
17
|
+
- **Committed tests:** the memory MCP has a committed vitest suite (`src/tools/__tests__/`). The new behaviour belongs in it, so the test files in this plan are committed, not discarded.
|
|
18
|
+
- **Session trailer on every commit:** append `Session: e53535e2-8c90-4e75-9936-6132ef8a86a0` and the `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>` line.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
### Task 1: Pure identity classifier
|
|
23
|
+
|
|
24
|
+
**Files:**
|
|
25
|
+
- Create: `platform/plugins/memory/mcp/src/lib/profile-identity.ts`
|
|
26
|
+
- Test: `platform/plugins/memory/mcp/src/tools/__tests__/profile-identity.test.ts`
|
|
27
|
+
|
|
28
|
+
**Interfaces:**
|
|
29
|
+
- Produces: `classifyProfileIdentity(input: { crossAccount: boolean; resolvedAccountId: string; callerUserId: string | undefined; callerHomeAccountId: string | null; ownerUserId: string | null }): ProfileAttribution` where `type ProfileAttribution = { kind: "caller"; userId: string } | { kind: "owner"; userId: string } | { kind: "reject"; reason: "no-caller" | "no-owner" }`.
|
|
30
|
+
|
|
31
|
+
- [ ] **Step 1: Write the failing test**
|
|
32
|
+
|
|
33
|
+
Create `platform/plugins/memory/mcp/src/tools/__tests__/profile-identity.test.ts`:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { describe, it, expect } from "vitest";
|
|
37
|
+
import { classifyProfileIdentity } from "../../lib/profile-identity.js";
|
|
38
|
+
|
|
39
|
+
const SESSION_ACCT = "acct-session";
|
|
40
|
+
const HOME_ACCT = "acct-home";
|
|
41
|
+
const CALLER = "user-caller";
|
|
42
|
+
const OWNER = "user-owner";
|
|
43
|
+
|
|
44
|
+
describe("classifyProfileIdentity (Task 1557)", () => {
|
|
45
|
+
it("cross-account with an owner attributes to the owner", () => {
|
|
46
|
+
expect(
|
|
47
|
+
classifyProfileIdentity({
|
|
48
|
+
crossAccount: true,
|
|
49
|
+
resolvedAccountId: SESSION_ACCT,
|
|
50
|
+
callerUserId: CALLER,
|
|
51
|
+
callerHomeAccountId: null,
|
|
52
|
+
ownerUserId: OWNER,
|
|
53
|
+
}),
|
|
54
|
+
).toEqual({ kind: "owner", userId: OWNER });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("cross-account with no owner rejects no-owner", () => {
|
|
58
|
+
expect(
|
|
59
|
+
classifyProfileIdentity({
|
|
60
|
+
crossAccount: true,
|
|
61
|
+
resolvedAccountId: SESSION_ACCT,
|
|
62
|
+
callerUserId: CALLER,
|
|
63
|
+
callerHomeAccountId: null,
|
|
64
|
+
ownerUserId: null,
|
|
65
|
+
}),
|
|
66
|
+
).toEqual({ kind: "reject", reason: "no-owner" });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("own-account with no caller userId rejects no-caller", () => {
|
|
70
|
+
expect(
|
|
71
|
+
classifyProfileIdentity({
|
|
72
|
+
crossAccount: false,
|
|
73
|
+
resolvedAccountId: SESSION_ACCT,
|
|
74
|
+
callerUserId: undefined,
|
|
75
|
+
callerHomeAccountId: null,
|
|
76
|
+
ownerUserId: OWNER,
|
|
77
|
+
}),
|
|
78
|
+
).toEqual({ kind: "reject", reason: "no-caller" });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("own-account caller who is an admin of this account attributes to the caller", () => {
|
|
82
|
+
expect(
|
|
83
|
+
classifyProfileIdentity({
|
|
84
|
+
crossAccount: false,
|
|
85
|
+
resolvedAccountId: SESSION_ACCT,
|
|
86
|
+
callerUserId: CALLER,
|
|
87
|
+
callerHomeAccountId: SESSION_ACCT,
|
|
88
|
+
ownerUserId: OWNER,
|
|
89
|
+
}),
|
|
90
|
+
).toEqual({ kind: "caller", userId: CALLER });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("own-account foreign operator (home differs) with an owner attributes to the owner", () => {
|
|
94
|
+
expect(
|
|
95
|
+
classifyProfileIdentity({
|
|
96
|
+
crossAccount: false,
|
|
97
|
+
resolvedAccountId: SESSION_ACCT,
|
|
98
|
+
callerUserId: CALLER,
|
|
99
|
+
callerHomeAccountId: HOME_ACCT,
|
|
100
|
+
ownerUserId: OWNER,
|
|
101
|
+
}),
|
|
102
|
+
).toEqual({ kind: "owner", userId: OWNER });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("own-account foreign operator with no owner rejects no-owner", () => {
|
|
106
|
+
expect(
|
|
107
|
+
classifyProfileIdentity({
|
|
108
|
+
crossAccount: false,
|
|
109
|
+
resolvedAccountId: SESSION_ACCT,
|
|
110
|
+
callerUserId: CALLER,
|
|
111
|
+
callerHomeAccountId: HOME_ACCT,
|
|
112
|
+
ownerUserId: null,
|
|
113
|
+
}),
|
|
114
|
+
).toEqual({ kind: "reject", reason: "no-owner" });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("own-account caller with no AdminUser node (home null) is treated as foreign", () => {
|
|
118
|
+
expect(
|
|
119
|
+
classifyProfileIdentity({
|
|
120
|
+
crossAccount: false,
|
|
121
|
+
resolvedAccountId: SESSION_ACCT,
|
|
122
|
+
callerUserId: CALLER,
|
|
123
|
+
callerHomeAccountId: null,
|
|
124
|
+
ownerUserId: OWNER,
|
|
125
|
+
}),
|
|
126
|
+
).toEqual({ kind: "owner", userId: OWNER });
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
132
|
+
|
|
133
|
+
Run: `cd platform/plugins/memory/mcp && npx vitest run src/tools/__tests__/profile-identity.test.ts`
|
|
134
|
+
Expected: FAIL — cannot resolve module `../../lib/profile-identity.js`.
|
|
135
|
+
|
|
136
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
137
|
+
|
|
138
|
+
Create `platform/plugins/memory/mcp/src/lib/profile-identity.ts`:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
/**
|
|
142
|
+
* Task 1557 — decide which identity a profile op (profile-read/update/delete)
|
|
143
|
+
* attributes to, given the already-resolved facts. Pure: no DB, no env.
|
|
144
|
+
*
|
|
145
|
+
* - Cross-account (a house session that set targetAccountId): attribute to the
|
|
146
|
+
* target account's owner; reject if the target has no owner on record.
|
|
147
|
+
* - Own-account, caller is a registered admin of THIS account (their AdminUser
|
|
148
|
+
* home account equals the session account): attribute to the caller. This is
|
|
149
|
+
* the owner-is-speaker path and the genuine second-admin path.
|
|
150
|
+
* - Own-account, caller is a FOREIGN operator (home account differs, or the
|
|
151
|
+
* caller has no AdminUser node in this graph — a house operator working inside
|
|
152
|
+
* a client's sub-account): attribute to the session account's owner; reject
|
|
153
|
+
* loudly if that account has no owner on record.
|
|
154
|
+
*/
|
|
155
|
+
export type ProfileAttribution =
|
|
156
|
+
| { kind: "caller"; userId: string }
|
|
157
|
+
| { kind: "owner"; userId: string }
|
|
158
|
+
| { kind: "reject"; reason: "no-caller" | "no-owner" };
|
|
159
|
+
|
|
160
|
+
export function classifyProfileIdentity(input: {
|
|
161
|
+
crossAccount: boolean;
|
|
162
|
+
resolvedAccountId: string;
|
|
163
|
+
callerUserId: string | undefined;
|
|
164
|
+
callerHomeAccountId: string | null;
|
|
165
|
+
ownerUserId: string | null;
|
|
166
|
+
}): ProfileAttribution {
|
|
167
|
+
const { crossAccount, resolvedAccountId, callerUserId, callerHomeAccountId, ownerUserId } = input;
|
|
168
|
+
if (crossAccount) {
|
|
169
|
+
return ownerUserId
|
|
170
|
+
? { kind: "owner", userId: ownerUserId }
|
|
171
|
+
: { kind: "reject", reason: "no-owner" };
|
|
172
|
+
}
|
|
173
|
+
if (!callerUserId) return { kind: "reject", reason: "no-caller" };
|
|
174
|
+
if (callerHomeAccountId === resolvedAccountId) {
|
|
175
|
+
return { kind: "caller", userId: callerUserId };
|
|
176
|
+
}
|
|
177
|
+
return ownerUserId
|
|
178
|
+
? { kind: "owner", userId: ownerUserId }
|
|
179
|
+
: { kind: "reject", reason: "no-owner" };
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
184
|
+
|
|
185
|
+
Run: `cd platform/plugins/memory/mcp && npx vitest run src/tools/__tests__/profile-identity.test.ts`
|
|
186
|
+
Expected: PASS (7 tests).
|
|
187
|
+
|
|
188
|
+
- [ ] **Step 5: Commit**
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
git add platform/plugins/memory/mcp/src/lib/profile-identity.ts platform/plugins/memory/mcp/src/tools/__tests__/profile-identity.test.ts
|
|
192
|
+
git commit -m "feat(memory): pure profile-identity classifier for operator-on-behalf attribution (Task 1557)
|
|
193
|
+
|
|
194
|
+
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
195
|
+
Session: e53535e2-8c90-4e75-9936-6132ef8a86a0"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### Task 2: Caller home-account resolver
|
|
201
|
+
|
|
202
|
+
**Files:**
|
|
203
|
+
- Create: `platform/plugins/memory/mcp/src/lib/resolve-admin-home-account.ts`
|
|
204
|
+
- Test: `platform/plugins/memory/mcp/src/tools/__tests__/resolve-admin-home-account.test.ts`
|
|
205
|
+
|
|
206
|
+
**Interfaces:**
|
|
207
|
+
- Consumes: `getSession` from `../lib/neo4j.js` (existing).
|
|
208
|
+
- Produces: `resolveAdminHomeAccount(userId: string): Promise<string | null>` — the `accountId` stamped on the caller's single `:AdminUser {userId}` node, or null.
|
|
209
|
+
|
|
210
|
+
- [ ] **Step 1: Write the failing test**
|
|
211
|
+
|
|
212
|
+
Create `platform/plugins/memory/mcp/src/tools/__tests__/resolve-admin-home-account.test.ts`:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
/**
|
|
216
|
+
* Task 1557 — resolveAdminHomeAccount returns the accountId stamped on the
|
|
217
|
+
* caller's AdminUser node (their home account), used to tell an operator working
|
|
218
|
+
* inside a client's sub-account from an admin of the account being operated.
|
|
219
|
+
*/
|
|
220
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
221
|
+
import { makeMockSession } from "./_helpers/emit-capture.js";
|
|
222
|
+
|
|
223
|
+
const CALLER = "11111111-2222-3333-4444-555555555555";
|
|
224
|
+
const HOUSE = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
|
|
225
|
+
|
|
226
|
+
let mockSession: ReturnType<typeof makeMockSession>;
|
|
227
|
+
vi.mock("../../lib/neo4j.js", () => ({
|
|
228
|
+
getSession: () => mockSession,
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
import { resolveAdminHomeAccount } from "../../lib/resolve-admin-home-account.js";
|
|
232
|
+
|
|
233
|
+
describe("resolveAdminHomeAccount (Task 1557)", () => {
|
|
234
|
+
beforeEach(() => {
|
|
235
|
+
mockSession = makeMockSession();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("returns the accountId stamped on the caller's AdminUser node", async () => {
|
|
239
|
+
mockSession = makeMockSession([[{ accountId: HOUSE }]]);
|
|
240
|
+
const result = await resolveAdminHomeAccount(CALLER);
|
|
241
|
+
expect(result).toBe(HOUSE);
|
|
242
|
+
const cypher = String(mockSession.run.mock.calls[0][0]);
|
|
243
|
+
expect(cypher).toMatch(/MATCH \(au:AdminUser \{userId: \$userId\}\)/);
|
|
244
|
+
expect(mockSession.run.mock.calls[0][1]).toMatchObject({ userId: CALLER });
|
|
245
|
+
expect(mockSession.close).toHaveBeenCalled();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("returns null when the caller has no AdminUser node", async () => {
|
|
249
|
+
mockSession = makeMockSession([[]]);
|
|
250
|
+
expect(await resolveAdminHomeAccount(CALLER)).toBeNull();
|
|
251
|
+
expect(mockSession.close).toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("returns null when the AdminUser node carries no accountId", async () => {
|
|
255
|
+
mockSession = makeMockSession([[{ accountId: "" }]]);
|
|
256
|
+
expect(await resolveAdminHomeAccount(CALLER)).toBeNull();
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
262
|
+
|
|
263
|
+
Run: `cd platform/plugins/memory/mcp && npx vitest run src/tools/__tests__/resolve-admin-home-account.test.ts`
|
|
264
|
+
Expected: FAIL — cannot resolve module `../../lib/resolve-admin-home-account.js`.
|
|
265
|
+
|
|
266
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
267
|
+
|
|
268
|
+
Create `platform/plugins/memory/mcp/src/lib/resolve-admin-home-account.ts`:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { getSession } from "./neo4j.js";
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Task 1557 — resolve the "home" account of an admin: the `accountId` stamped
|
|
275
|
+
* on their `:AdminUser` node. AdminUser is MERGE-keyed on `userId` alone
|
|
276
|
+
* (neo4j-store.ts), with `accountId` set ON CREATE, so each admin has exactly
|
|
277
|
+
* one node whose `accountId` is the account they were first provisioned under.
|
|
278
|
+
*
|
|
279
|
+
* A profile op whose caller's home account differs from the account the session
|
|
280
|
+
* is scoped to is a house operator working inside a client's sub-account — the
|
|
281
|
+
* preference belongs to that client's owner, not the operator. Returns null when
|
|
282
|
+
* the caller has no `:AdminUser` node or it carries no `accountId`.
|
|
283
|
+
*/
|
|
284
|
+
export async function resolveAdminHomeAccount(userId: string): Promise<string | null> {
|
|
285
|
+
const session = getSession();
|
|
286
|
+
try {
|
|
287
|
+
const res = await session.run(
|
|
288
|
+
`MATCH (au:AdminUser {userId: $userId})
|
|
289
|
+
RETURN au.accountId AS accountId
|
|
290
|
+
LIMIT 1`,
|
|
291
|
+
{ userId },
|
|
292
|
+
);
|
|
293
|
+
const accountId = res.records[0]?.get("accountId");
|
|
294
|
+
return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
|
|
295
|
+
} finally {
|
|
296
|
+
await session.close();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
302
|
+
|
|
303
|
+
Run: `cd platform/plugins/memory/mcp && npx vitest run src/tools/__tests__/resolve-admin-home-account.test.ts`
|
|
304
|
+
Expected: PASS (3 tests).
|
|
305
|
+
|
|
306
|
+
- [ ] **Step 5: Commit**
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
git add platform/plugins/memory/mcp/src/lib/resolve-admin-home-account.ts platform/plugins/memory/mcp/src/tools/__tests__/resolve-admin-home-account.test.ts
|
|
310
|
+
git commit -m "feat(memory): resolveAdminHomeAccount helper (Task 1557)
|
|
311
|
+
|
|
312
|
+
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
313
|
+
Session: e53535e2-8c90-4e75-9936-6132ef8a86a0"
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
### Task 3: Wire the classifier into resolveProfileIdentity and the handler bootstrap flags
|
|
319
|
+
|
|
320
|
+
**Files:**
|
|
321
|
+
- Modify: `platform/plugins/memory/mcp/src/index.ts` (imports; `resolveProfileIdentity` at lines 150-186; `profile-update` handler bootstrap flags at lines 2584-2585)
|
|
322
|
+
- Modify: `platform/plugins/memory/mcp/src/tools/__tests__/profile-update-personfields-bootstrap.test.ts:112-116` (structural assertion tracks the new wiring)
|
|
323
|
+
|
|
324
|
+
**Interfaces:**
|
|
325
|
+
- Consumes: `classifyProfileIdentity` (Task 1), `resolveAdminHomeAccount` (Task 2), `resolveOwnerUserId` (existing, `./lib/resolve-owner-userid.js`).
|
|
326
|
+
- Produces: `resolveProfileIdentity(...)` now returns `{ userId: string; bootstrap: boolean } | { reject: … }`. The `bootstrap` field is true exactly when the write attributes to a resolved owner (cross-account or operator-on-behalf); handlers pass it as `bootstrapProfile`/`bootstrapPerson`.
|
|
327
|
+
|
|
328
|
+
- [ ] **Step 1: Add the two imports**
|
|
329
|
+
|
|
330
|
+
In `platform/plugins/memory/mcp/src/index.ts`, immediately after the existing line 53 `import { resolveOwnerUserId } from "./lib/resolve-owner-userid.js";`, add:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import { resolveAdminHomeAccount } from "./lib/resolve-admin-home-account.js";
|
|
334
|
+
import { classifyProfileIdentity } from "./lib/profile-identity.js";
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
- [ ] **Step 2: Replace the IdentityResult type and resolveProfileIdentity body**
|
|
338
|
+
|
|
339
|
+
Replace the block at `platform/plugins/memory/mcp/src/index.ts:150-186` (from `type IdentityResult =` through the end of the `resolveProfileIdentity` function) with:
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
type IdentityResult =
|
|
343
|
+
| { userId: string; bootstrap: boolean }
|
|
344
|
+
| { reject: { content: { type: "text"; text: string }[]; isError: true } };
|
|
345
|
+
async function resolveProfileIdentity(
|
|
346
|
+
toolName: string,
|
|
347
|
+
crossAccount: boolean,
|
|
348
|
+
resolvedAccountId: string,
|
|
349
|
+
callerUserId: string | undefined,
|
|
350
|
+
): Promise<IdentityResult> {
|
|
351
|
+
// Resolve the facts the pure classifier needs. On the own-account path the
|
|
352
|
+
// account's owner is resolved only when the caller is NOT operating their
|
|
353
|
+
// home account (a foreign house operator inside a client's sub-account), so
|
|
354
|
+
// the common owner-is-speaker path costs one AdminUser lookup, not two.
|
|
355
|
+
const callerHomeAccountId =
|
|
356
|
+
!crossAccount && callerUserId ? await resolveAdminHomeAccount(callerUserId) : null;
|
|
357
|
+
const needOwner =
|
|
358
|
+
crossAccount || (!!callerUserId && callerHomeAccountId !== resolvedAccountId);
|
|
359
|
+
const ownerUserId = needOwner ? await resolveOwnerUserId(resolvedAccountId) : null;
|
|
360
|
+
|
|
361
|
+
const decision = classifyProfileIdentity({
|
|
362
|
+
crossAccount,
|
|
363
|
+
resolvedAccountId,
|
|
364
|
+
callerUserId,
|
|
365
|
+
callerHomeAccountId,
|
|
366
|
+
ownerUserId,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
if (decision.kind === "reject") {
|
|
370
|
+
const text =
|
|
371
|
+
decision.reason === "no-owner"
|
|
372
|
+
? `${toolName} failed: target account ${resolvedAccountId.slice(0, 8)}… has no owner AdminUser to attribute the profile to — its graph root is unprovisioned (see Task 1359 seedAccountGraphRoot).`
|
|
373
|
+
: `${toolName} requires an authenticated admin session with userId`;
|
|
374
|
+
return {
|
|
375
|
+
reject: {
|
|
376
|
+
content: [{ type: "text" as const, text }],
|
|
377
|
+
isError: true as const,
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (decision.kind === "owner") {
|
|
383
|
+
if (crossAccount) {
|
|
384
|
+
process.stderr.write(
|
|
385
|
+
`[xacct] op=identity tool=${toolName} target=${resolvedAccountId.slice(0, 8)} resolved-userId=${decision.userId.slice(0, 8)}\n`,
|
|
386
|
+
);
|
|
387
|
+
} else {
|
|
388
|
+
process.stderr.write(
|
|
389
|
+
`[xacct] op=identity tool=${toolName} mode=operator-on-behalf target=${resolvedAccountId.slice(0, 8)} caller=${(callerUserId ?? "").slice(0, 8)} resolved-userId=${decision.userId.slice(0, 8)}\n`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
return { userId: decision.userId, bootstrap: true };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// decision.kind === "caller" — own-account owner-is-speaker / second admin.
|
|
396
|
+
return { userId: decision.userId, bootstrap: false };
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
- [ ] **Step 3: Point the profile-update bootstrap flags at the resolved identity**
|
|
401
|
+
|
|
402
|
+
In `platform/plugins/memory/mcp/src/index.ts`, replace lines 2584-2585:
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
bootstrapProfile: crossAccount,
|
|
406
|
+
bootstrapPerson: crossAccount,
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
with:
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
bootstrapProfile: ident.bootstrap,
|
|
413
|
+
bootstrapPerson: ident.bootstrap,
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
(No change is needed in the `profile-read` or `profile-delete` handlers: they read only `ident.userId`, which the success shape still carries. The `if ("reject" in ident) return ident.reject;` guard is unchanged in all three.)
|
|
417
|
+
|
|
418
|
+
- [ ] **Step 4: Update the structural test to track the new wiring**
|
|
419
|
+
|
|
420
|
+
In `platform/plugins/memory/mcp/src/tools/__tests__/profile-update-personfields-bootstrap.test.ts`, replace lines 112-116:
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
it("index.ts wires bootstrapPerson: crossAccount at the profile-update call site", () => {
|
|
424
|
+
const SRC = resolve(dirname(fileURLToPath(import.meta.url)), "../../index.ts");
|
|
425
|
+
const src = readFileSync(SRC, "utf8");
|
|
426
|
+
expect(src).toContain("bootstrapPerson: crossAccount");
|
|
427
|
+
});
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
with:
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
it("index.ts wires bootstrapPerson from the resolved identity at the profile-update call site", () => {
|
|
434
|
+
const SRC = resolve(dirname(fileURLToPath(import.meta.url)), "../../index.ts");
|
|
435
|
+
const src = readFileSync(SRC, "utf8");
|
|
436
|
+
expect(src).toContain("bootstrapPerson: ident.bootstrap");
|
|
437
|
+
});
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
- [ ] **Step 5: Build and run the full suite**
|
|
441
|
+
|
|
442
|
+
Run: `cd platform/plugins/memory/mcp && npm run build && npm test`
|
|
443
|
+
Expected: build clean (no TypeScript errors); all tests PASS, including the new `profile-identity` and `resolve-admin-home-account` files and the updated `profile-update-personfields-bootstrap` structural test. Baseline before this task was 67 files / 477 tests passing; after Tasks 1-3 expect 69 files and 487 tests (477 + 7 classifier + 3 resolver), all green.
|
|
444
|
+
|
|
445
|
+
- [ ] **Step 6: Commit**
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
git add platform/plugins/memory/mcp/src/index.ts platform/plugins/memory/mcp/src/tools/__tests__/profile-update-personfields-bootstrap.test.ts
|
|
449
|
+
git commit -m "fix(memory): attribute preferences to the account owner when an operator works inside a client's sub-account (Task 1557)
|
|
450
|
+
|
|
451
|
+
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
452
|
+
Session: e53535e2-8c90-4e75-9936-6132ef8a86a0"
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
### Task 4: Teach the skill and document the model
|
|
458
|
+
|
|
459
|
+
**Files:**
|
|
460
|
+
- Modify: `platform/plugins/memory/skills/conversational-memory/SKILL.md` (add an operator-on-behalf section)
|
|
461
|
+
- Modify: `platform/plugins/memory/PLUGIN.md` (Conversational Memory section, after line 223)
|
|
462
|
+
- Modify: `platform/plugins/memory/mcp/src/tools/profile-update.ts:312-314` (refresh the now-stale bootstrap comment)
|
|
463
|
+
|
|
464
|
+
**Interfaces:** none (docs and comment only).
|
|
465
|
+
|
|
466
|
+
- [ ] **Step 1: Add the operator-on-behalf section to the skill**
|
|
467
|
+
|
|
468
|
+
In `platform/plugins/memory/skills/conversational-memory/SKILL.md`, insert this section immediately before the `## How to Store` heading (currently line 75). Zero em-dashes:
|
|
469
|
+
|
|
470
|
+
```markdown
|
|
471
|
+
## Operating a client's account on the owner's behalf
|
|
472
|
+
|
|
473
|
+
When you are a house operator working inside a client's sub-account, the preferences you capture there belong to that client, the account's owner on record, not to you. Attribution is automatic. A preference stored while you operate a client's account is written against that owner, and "what does this owner prefer?" reads the owner's list. You pass no target and no flag; the tool decides from who is driving the session.
|
|
474
|
+
|
|
475
|
+
Your own working preferences belong to your own session, not a client's. Store them when you are operating your own account.
|
|
476
|
+
|
|
477
|
+
If a client's account has no owner on record, a preference write is refused out loud rather than saved to your profile. That means the account's graph root was never seeded, so it cannot hold owner preferences until it is.
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
- [ ] **Step 2: Document the model in PLUGIN.md**
|
|
481
|
+
|
|
482
|
+
In `platform/plugins/memory/PLUGIN.md`, insert this paragraph immediately after line 223 (the end of the `## Conversational Memory` intro paragraph, before the `## UserProfile + Person Field Management` heading):
|
|
483
|
+
|
|
484
|
+
```markdown
|
|
485
|
+
When a house operator operates a client's sub-account, the session is scoped into that account (`ACCOUNT_ID` is the sub-account, `USER_ID` is the operator, no `HOUSE_ADMIN_SCOPE`). In that session `profile-read`/`profile-update`/`profile-delete` resolve to the account's **owner** `AdminUser`, not the operator: the operator has no `AdminUser` node in the sub-account subgraph, so `resolveProfileIdentity` sees the caller is not an admin of the account it is scoped to and attributes to the seeded owner — the same owner identity the cross-account `targetAccountId` path resolves (above), reached without `targetAccountId` because the re-scoped session cannot set it. An account with no seeded owner rejects the write loudly (`… has no owner AdminUser …`) rather than attributing it to the operator, and emits `[xacct] op=identity tool=profile-* mode=operator-on-behalf target=<sub8> caller=<op8> resolved-userId=<owner8>` on the owner-attributed path. The operator's own preferences live on their own (house) session.
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
- [ ] **Step 3: Refresh the stale bootstrap comment**
|
|
489
|
+
|
|
490
|
+
In `platform/plugins/memory/mcp/src/tools/profile-update.ts`, replace lines 312-314:
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
// preference lands. Own-account writes never set bootstrapProfile (their
|
|
494
|
+
// UserProfile is provisioned at PIN setup), so this is byte-identical for
|
|
495
|
+
// them. Idempotent: a MERGE that matches an existing UserProfile is a no-op.
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
with:
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
// preference lands. Own-account owner-is-speaker writes never set
|
|
502
|
+
// bootstrapProfile (their UserProfile is provisioned at PIN setup); an
|
|
503
|
+
// operator-on-behalf write (Task 1557) does set it, and this MERGE
|
|
504
|
+
// bootstraps the client owner's UserProfile exactly as the cross-account
|
|
505
|
+
// path does. Idempotent: a MERGE that matches an existing UserProfile is a no-op.
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
- [ ] **Step 4: Verify the build still compiles (comment-only source change)**
|
|
509
|
+
|
|
510
|
+
Run: `cd platform/plugins/memory/mcp && npm run build`
|
|
511
|
+
Expected: clean, no TypeScript errors.
|
|
512
|
+
|
|
513
|
+
- [ ] **Step 5: Commit**
|
|
514
|
+
|
|
515
|
+
```bash
|
|
516
|
+
git add platform/plugins/memory/skills/conversational-memory/SKILL.md platform/plugins/memory/PLUGIN.md platform/plugins/memory/mcp/src/tools/profile-update.ts
|
|
517
|
+
git commit -m "docs(memory): teach conversational-memory the operator-on-behalf owner attribution (Task 1557)
|
|
518
|
+
|
|
519
|
+
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
520
|
+
Session: e53535e2-8c90-4e75-9936-6132ef8a86a0"
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
## Self-Review
|
|
526
|
+
|
|
527
|
+
**Spec coverage:**
|
|
528
|
+
- "Attribute a foreign operator's preference to the account owner" → Task 1 (classifier `owner` branch) + Task 3 (wiring). ✓
|
|
529
|
+
- "Discriminator = caller not an admin of the scoped account, robust to multi-admin" → Task 2 (`resolveAdminHomeAccount`) + Task 1 (`callerHomeAccountId === resolvedAccountId` → caller). ✓
|
|
530
|
+
- "No owner → loud reject, never silent operator attribution" → Task 1 (`no-owner` reject) + Task 3 (reject envelope). ✓
|
|
531
|
+
- "Bootstrap the owner's profile on the operator-on-behalf path" → Task 3 (`bootstrap` from resolved identity) + Task 4 Step 3 (comment). ✓
|
|
532
|
+
- "Read side returns owner's preferences" → covered for free: `profile-read` uses `ident.userId` which now resolves to the owner (Task 3 note in Step 3). ✓
|
|
533
|
+
- "Own-account owner-is-speaker and second-admin unchanged" → Task 1 (`caller` branch) + tests in Task 1. ✓
|
|
534
|
+
- "Cross-account path unchanged, log line byte-identical" → Task 3 Step 2 (crossAccount branch keeps the exact existing line). ✓
|
|
535
|
+
- "Skill teaching" → Task 4 Step 1. "PLUGIN.md doc" → Task 4 Step 2. ✓
|
|
536
|
+
- "Tests: operator-on-behalf, own-account, second-admin, no-owner, cross-account" → Task 1 test (all classifier branches) + Task 2 test (home resolution) + existing `resolve-owner-userid.test.ts` and `cross-account.test.ts` remain green (verified in Task 3 Step 5). ✓
|
|
537
|
+
|
|
538
|
+
**Placeholder scan:** No TBD/TODO/"handle edge cases"; every code step carries complete code. ✓
|
|
539
|
+
|
|
540
|
+
**Type consistency:** `ProfileAttribution` kinds (`caller`/`owner`/`reject`) and the `classifyProfileIdentity` input keys are identical between Task 1's definition, Task 1's tests, and Task 3's call site. `IdentityResult` success shape `{ userId, bootstrap }` matches the handler reads (`ident.userId`, `ident.bootstrap`). `resolveAdminHomeAccount(userId): Promise<string | null>` matches its Task 3 call. ✓
|
|
541
|
+
|
|
542
|
+
**Out-of-scope confirmed untouched:** no change to `targetAccountId`/`resolveEffectiveAccountCore`, the sub-account picker/spawn path, owner seeding, or arbitrary-person attribution. A no-owner backfill remains a separate concern; if the on-device reproduce surfaces a rootless client account, file a `.tasks/NNN-*.md` rather than relaxing the reject.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Task 1557 — preferences an operator states inside a client's sub-account attach to the owner, not the operator
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-11
|
|
4
|
+
**Status:** Approved design, pending plan
|
|
5
|
+
**Scope:** `platform/plugins/memory/mcp/src/` (identity resolution), `platform/plugins/memory/skills/conversational-memory/SKILL.md`, `platform/plugins/memory/PLUGIN.md`, tests.
|
|
6
|
+
|
|
7
|
+
## The problem, in one line
|
|
8
|
+
|
|
9
|
+
A house operator working inside a client's sub-account states a preference on the client's behalf. It attaches to the operator, not the client's owner. The operator wants a way to say "this belongs to them, not me."
|
|
10
|
+
|
|
11
|
+
## What the evidence established (measure-first, now measured)
|
|
12
|
+
|
|
13
|
+
The attribution is decided in `resolveProfileIdentity` (`platform/plugins/memory/mcp/src/index.ts:153`). Two facts, both confirmed at the code layer, fix the shape of the solution:
|
|
14
|
+
|
|
15
|
+
1. **Operating a sub-account re-scopes the whole session into it.** The sub-account picker POSTs `/api/admin/session/switch-account`, which calls `createAdminSession(target.accountId, …, operatorUserId)` (`platform/ui/server/routes/admin/session.ts:341-371`). The rc-spawn that follows boots the memory MCP with `ACCOUNT_ID` = the **sub-account**, `USER_ID` = the **house operator**, and **no** `HOUSE_ADMIN_SCOPE` (`buildRcChildEnv`, and `houseAdminScopeFor` returns null for a non-house account). So the preference write is an ordinary own-account write, keyed to the operator's `USER_ID`, landing on `:UserProfile{accountId:sub, userId:operator}` — never the owner.
|
|
16
|
+
|
|
17
|
+
2. **The tool-layer `targetAccountId` lever is inert in this session.** `resolveEffectiveAccountCore` rejects any `targetAccountId` with `cross-account-denied` when `houseScope` is null (`platform/lib/account-scope/src/index.ts:49`). A sub-account operator session has no house scope, so the agent cannot set `targetAccountId`. **Teaching the skill to pass `targetAccountId` would do nothing** — it would be rejected. This is the branch the task pre-authorised: MCP `src/` changes "only if the operator-surface `targetAccountId` wiring proves absent — measure first." It is absent.
|
|
18
|
+
|
|
19
|
+
3. **The owner is a distinct, single, resolvable identity.** Each sub-account is seeded with exactly one `:AdminUser{accountId, role:'owner'}` carrying a **fresh per-account `userId`** (`platform/plugins/admin/mcp/src/tools/account-lifecycle.ts:256,279`), deliberately never the operator's. `resolveOwnerUserId(accountId)` already resolves it (`platform/plugins/memory/mcp/src/lib/resolve-owner-userid.ts`).
|
|
20
|
+
|
|
21
|
+
4. **`AdminUser` nodes are keyed on `userId` alone; `accountId` is a home-account stamp.** `neo4j-store.ts:627-630` MERGEs `:AdminUser {userId}` and sets `accountId` only `ON CREATE`. Each admin therefore has one global `AdminUser` node whose `accountId` is their home account. This makes "is the caller an admin of the account they are operating?" answerable with one query, with the home account fixed at the node's first creation.
|
|
22
|
+
|
|
23
|
+
## The fix
|
|
24
|
+
|
|
25
|
+
A preference can only ever attach to an account's owner — that is the only "designated person" the memory model supports, confirmed with the operator. So "attribute this to the designated person" resolves to "attribute this to the owner of the account being operated." The switch that decides it lives in one place: `resolveProfileIdentity`.
|
|
26
|
+
|
|
27
|
+
### Behaviour
|
|
28
|
+
|
|
29
|
+
In `resolveProfileIdentity`, the own-account branch (`crossAccount === false`) gains an operator-on-behalf case:
|
|
30
|
+
|
|
31
|
+
- Resolve the caller's home account: the `accountId` on their `:AdminUser {userId: callerUserId}` node.
|
|
32
|
+
- **Caller is operating their home account** (home `accountId` == the session's resolved `accountId`): attribute to `callerUserId`. Unchanged — this is the owner-is-speaker path and the genuine second-admin path.
|
|
33
|
+
- **Caller is a foreign operator** (home `accountId` != the session's resolved `accountId`, i.e. a house operator working inside a client's sub-account): resolve the session account's owner via `resolveOwnerUserId`.
|
|
34
|
+
- Owner exists → attribute the preference to the **owner**, and bootstrap the owner's `UserProfile`/`Person` the same way the cross-account path already does. Emit `[xacct] op=identity tool=… mode=operator-on-behalf target=<acct8> caller=<caller8> resolved-userId=<owner8>`.
|
|
35
|
+
- No owner on record → **loud reject** (identical message class to the cross-account no-owner rejection). Never a silent fall-through to the operator's own profile.
|
|
36
|
+
- Caller `userId` absent → existing "requires an authenticated admin session with userId" reject, unchanged.
|
|
37
|
+
|
|
38
|
+
The cross-account branch (`crossAccount === true`, a house session that set `targetAccountId`) is unchanged.
|
|
39
|
+
|
|
40
|
+
### Discriminator — why it is correct and regression-safe
|
|
41
|
+
|
|
42
|
+
The condition "caller's `AdminUser.accountId` != the session's `accountId`" is equivalent to "no `:AdminUser {accountId: <session>, userId: <caller>}` exists," because there is exactly one `AdminUser` node per `userId`. Case-by-case:
|
|
43
|
+
|
|
44
|
+
| Caller | Session account | Home `accountId` | Result |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| Owner (home = own account) | own | own | attribute self — unchanged |
|
|
47
|
+
| Second admin of an account | that account | that account | attribute self — no regression |
|
|
48
|
+
| House operator | a client sub-account | house | attribute the sub-account **owner** — the fix |
|
|
49
|
+
| House operator | the house account | house | attribute self — unchanged |
|
|
50
|
+
|
|
51
|
+
A sub-account subgraph never contains an `AdminUser` node for the operator (their node carries `accountId=house`), so the foreign-operator branch fires for exactly the reported case and no other.
|
|
52
|
+
|
|
53
|
+
### Bootstrap generalisation
|
|
54
|
+
|
|
55
|
+
Today the handlers set `bootstrapProfile`/`bootstrapPerson` `= crossAccount`. After the fix, a foreign-operator own-account write also attributes to an owner whose `UserProfile` may not yet exist, so the bootstrap flags must be true whenever the resolved identity is a **foreign owner**, not only when `crossAccount`. `resolveProfileIdentity` will return whether the attribution is to a foreign owner, and the three handlers derive the bootstrap flags from that rather than from `crossAccount` directly. This keeps a single meaning: "we resolved to an owner who is not the caller → bootstrap their profile if absent."
|
|
56
|
+
|
|
57
|
+
### Skill
|
|
58
|
+
|
|
59
|
+
`conversational-memory/SKILL.md` currently says "the owner" throughout and assumes the person conversing **is** the owner. It gains the operator-on-behalf case: when the session is a house operator working inside a client's account, preferences captured there belong to that client (the account's owner), not to the operator; the operator's own preferences live on their own (house) session. "What does this owner prefer?" reads the owner's list. The skill also states the loud refusal when the client account has no owner on record, pointing at the rootless-account remedy. No new tool argument is introduced — attribution is decided by who is driving the session, deterministically, so the skill describes the model rather than instructing a per-call lever.
|
|
60
|
+
|
|
61
|
+
### Docs
|
|
62
|
+
|
|
63
|
+
`PLUGIN.md`'s preference section records the operator/owner attribution model: in a client sub-account session, preference reads and writes resolve to the account's owner, and a no-owner account rejects loudly. This sits alongside the existing cross-account `targetAccountId` note (lines 189-193), which documents the house-session path; the two paths resolve to the same owner identity.
|
|
64
|
+
|
|
65
|
+
## What success looks like
|
|
66
|
+
|
|
67
|
+
- A preference an operator states while operating a client's sub-account is stored as a `:Preference` attributed to that sub-account's **owner** `userId`, not the operator's.
|
|
68
|
+
- "What does the owner prefer?" on the sub-account returns the owner's preferences, not the operator's.
|
|
69
|
+
- A sub-account with no seeded owner rejects the write out loud, never silently attributing to the operator.
|
|
70
|
+
- The own-account owner-is-speaker path and the genuine second-admin path are byte-for-byte unchanged.
|
|
71
|
+
|
|
72
|
+
## Testing
|
|
73
|
+
|
|
74
|
+
Unit tests over `resolveProfileIdentity` / the profile handlers:
|
|
75
|
+
|
|
76
|
+
- **Operator-on-behalf write** — caller whose `AdminUser.accountId` != the session account resolves the session account's owner `userId`; the resolved identity is the owner and the bootstrap flag is set.
|
|
77
|
+
- **Own-account owner-is-speaker** — caller whose `AdminUser.accountId` == the session account keys to the caller `userId`; bootstrap flag is off. Unchanged.
|
|
78
|
+
- **Second admin of the account** — caller with a home `AdminUser.accountId` equal to the session account attributes to self, proving multi-admin accounts do not mis-redirect.
|
|
79
|
+
- **Foreign operator, no owner** — session account has no owner `AdminUser`; the write is rejected loudly, never caller-attributed.
|
|
80
|
+
- **Cross-account (`targetAccountId`) path** — unchanged: still resolves the target owner, still rejects a no-owner target.
|
|
81
|
+
|
|
82
|
+
The attribution is already logged (`[xacct] op=identity … resolved-userId=`, `[preference-write] op=… accountId=`), so on-device the before/after is visible from the journal without new instrumentation. The runtime reproduce on the SiteDesk device is a confirmation of the already-established code path, not a design input; this Mac cannot read that device's session log, so the design rests on the code evidence above.
|
|
83
|
+
|
|
84
|
+
## Out of scope
|
|
85
|
+
|
|
86
|
+
- The cross-account `targetAccountId` / `resolveOwnerUserId` mechanism (Task 1461) and owner seeding (Task 1359) — consumed, not re-implemented.
|
|
87
|
+
- How the sub-account picker switches/re-scopes sessions (Task 1311) — untouched; the fix needs no spawn-env change.
|
|
88
|
+
- Attribution to an arbitrary named individual who is not an account owner — a much larger storage/schema change, explicitly excluded.
|
|
89
|
+
- Backfill of pre-1359 sub-accounts created without an owner on record — a separate one-time seed, never a gate relaxation. If needed, filed as its own task.
|
|
90
|
+
- Non-preference profile fields and the `personFields` owner-`Person` bootstrap beyond their sharing the same resolved-owner identity path.
|
|
@@ -29,7 +29,21 @@ interface MinimalSession {
|
|
|
29
29
|
}>;
|
|
30
30
|
}>;
|
|
31
31
|
}
|
|
32
|
+
export type ActiveRuleSource = "owner" | "no-owner" | "owner-error";
|
|
33
|
+
export type InjectionSource = ActiveRuleSource | "fetch-error";
|
|
34
|
+
export interface ActiveRuleResolution {
|
|
35
|
+
rules: ActiveRule[];
|
|
36
|
+
ownerUserId: string | null;
|
|
37
|
+
source: ActiveRuleSource;
|
|
38
|
+
}
|
|
39
|
+
export interface StandingRulesInjection {
|
|
40
|
+
block: string;
|
|
41
|
+
ownerUserId: string | null;
|
|
42
|
+
source: InjectionSource;
|
|
43
|
+
}
|
|
44
|
+
export declare function resolveOwnerUserId(session: MinimalSession, accountId: string): Promise<string | null>;
|
|
32
45
|
export declare function queryActiveRuleCandidates(session: MinimalSession, accountId: string): Promise<RuleCandidate[]>;
|
|
46
|
+
export declare function resolveActiveRules(session: MinimalSession, accountId: string, cap?: number): Promise<ActiveRuleResolution>;
|
|
33
47
|
export declare function activeRuleSet(session: MinimalSession, accountId: string, cap?: number): Promise<ActiveRule[]>;
|
|
34
48
|
export {};
|
|
35
49
|
//# sourceMappingURL=index.d.ts.map
|