chainlesschain 0.163.8 → 0.165.1
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 +7 -4
- package/scripts/macos-mcp-launcher-build.mjs +317 -0
- package/scripts/macos-mcp-launcher-postinstall.sh +54 -0
- package/scripts/macos-mcp-launcher-preinstall.sh +61 -0
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-CavM7W3k.js → AIOps-CYxFDrcm.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CUvZGabQ.js → ActionButton-CPBtdiU2.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DeBnWnbe.js → Analytics-Dayq7Dp7.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DbI5kxvd.js → AppLayout-6U7bPb2s.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-D93bigKm.js → Artifacts-D2Iqg_3D.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CBNvfUZa.js → Audit-CdoOB7WV.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-CJLyh_YU.js → BackgroundAgents-DrnAVB8w.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-D_0yoehD.js → Backup-CesMi5cy.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-TJxmbObW.js → BaseInput-DaX9ptax.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CE0dto1q.js → Chat-OrwOPxDS.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CZ5-yYiD.js → ChatBubbleRenderer-CvPZFRxM.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-Ck1RVIVo.js → Checkbox-DiHbcysC.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-qpoyf6DK.js → Codegen-CbgCkaBJ.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DUyPWjIE.js → Col-Du5hgrh4.js} +1 -1
- package/src/assets/web-panel/assets/{Community-D_S1BemS.js → Community-DsPOBW-p.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BjxeK-Gc.js → Compact-BwHIFkNC.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-Dd3CEJsu.js → Compliance-BvrDQSgd.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-B4QG7-ad.js → Cowork-gwAE6arx.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-BtZnids5.js → Cron-DW-0l4uF.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CHPfCBFD.js → Crosschain-j-NsLesB.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DT8DCEOK.js → DID-CibVEBOI.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-COQab5_U.js → Dashboard-W4z9vSfe.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BY1PsQuU.js → Dropdown-DAcrkKxG.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CsuAuaq3.js → EmailListRenderer-6GXah-89.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BqpinD-j.js → FamilyGuardDashboard-TKDCq8Ku.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-D0gkfQFv.js → Federation-BXBLlcNm.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BqgE-axK.js → FormItemContext-BoJJBkG3.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-D0hJ2KvC.js → GenericCardRenderer-okjsl46v.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CmMB0uPY.js → Git-Rvs7i6nn.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BnVEI0Dk.js → Governance-CHSUxXXS.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-YTAMihRP.js → Inference-V3BlfxsB.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-0ax7VHop.js → KnowledgeGraph-C_NlD4aR.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-B9TRcokL.js → Logs-Nn4LNxhA.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-SVAgNnkt.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-B6CSuD_H.js → Marketplace-DEmjFcg-.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CjMXvOVU.js → McpTools-DlopXipI.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CXRF71U3.js → Memory-BG3ynX60.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DQgNp_oS.js → MobileBridge-DeRzWOfT.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-DlPwcCbq.js → MobileProjects-Tyd3o2A0.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-6hDTRPe1.js → Mtc-BoPbOv1C.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-BlVHU1DS.js → MtcAudit-un-rTHp7.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-DP9pzEbV.js → Multisig-Ds0oveEB.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-Djorh8EV.js → NLProgramming-D09JI-yY.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-oLZ-JkUg.js → Notes-DTRhiKJn.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-Cg288VH3.js → NotificationSettings-BAnnNCim.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-Yr-1fSYi.js +1 -0
- package/src/assets/web-panel/assets/{Organization-CBdo-bHs.js → Organization-S9EAolpj.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-qZdnVumx.js → Overflow-CqcLKqJ8.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D4kNfGNQ.js → P2P-Dw0F9EcU.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DnlMemM7.js → PdhVaultBrowser-BDRwUevN.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-DHucHFBJ.js → Permissions-D3_c3nKB.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BovlOIPR.js → PersonalDataHub-CxdMV98x.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-Deru3CQY.js → Pipeline-CgjNQFLI.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-Bq5oTTbU.js → Privacy-BHghIIht.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BfzxVWAz.js → ProjectInit-De6pqaKM.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CKC6XBtH.js → ProjectSettings-Bg4ZkNYQ.js} +2 -2
- package/src/assets/web-panel/assets/Projects-CP8vPvzs.js +1 -0
- package/src/assets/web-panel/assets/{Providers-C_jx4TWX.js → Providers-CUnHbNf-.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BdIHeVGC.js → QrScannerModal-DA2f3Wts.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BwZwAcPD.js → QuickAsk-C1FeHDqp.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BNTt0L-m.js → Recommend-B5guaUMN.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-PDADSds3.js +5 -0
- package/src/assets/web-panel/assets/{Reputation-D6VpG32B.js → Reputation-eTlRhgvK.js} +1 -1
- package/src/assets/web-panel/assets/{Row-D_74OvQQ.js → Row-D7AqSGY0.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BTr0a-BQ.js → RssFeed-JiuLm4Ja.js} +3 -3
- package/src/assets/web-panel/assets/{Search-C3MCFoNU.js → Search-CwruahW3.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CqW9aJ6s.js → Security-CdhqaR0b.js} +4 -4
- package/src/assets/web-panel/assets/{Services-Cw5GNaHF.js → Services-CU0ZIlcT.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DZps5qeY.js → Skeleton-CtduPfAb.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-C6NQ1znT.js → Skills-CROXm6AY.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DFP8G2Sh.js → Sla-DiZiABFD.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-ESt6MB6v.js → SpeechSettings-B5pLcXUw.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BHnN98aM.js → SyncSettings-DBX6J1Oe.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-NiSCaOf-.js → Tasks-BLyRJd3-.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DsKFhW2C.js → Templates-BKa5qzmk.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CQ0E0ze5.js → Tenant-DnD3a4M6.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-D_rBu_by.js → Terminal-DEitQeTb.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D3Vm4Po4.js → TimelineRenderer-CjHggTDq.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DNTxRH6q.js → Tokens-CsGT7sfl.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Dgprz1ms.js → Trigger-yr4rFDrW.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-B2rUnVBD.js → Trust-CLvu1Sjp.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DEGQNc6_.js → UkeySign-B0m7yUP8.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-jlsAx3Eh.js → VideoEditing-C0VvP4lc.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-8jQih1ij.js → Wallet-kllBAG1M.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-XLeO6xIB.js → WebAuthn-C54UQNLx.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-De_dgDAG.js → WorkflowEditor-B7y8qSvU.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CI19R_Sk.js → chat-DRzTh8CJ.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DsYTOYN3.js → colors-BSMwSnCx.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CM3-t_Qr.js → compact-item-BTg602H5.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BjxlBRZv.js → createContext-B6EGsdvp.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Dol8jLEP.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-RDaWX_4i.js → hasIn-DDKqJ8RD.js} +1 -1
- package/src/assets/web-panel/assets/{index-nAH87mQP.js → index-323Hhxxt.js} +1 -1
- package/src/assets/web-panel/assets/{index-DIUnNOjG.js → index-BICn3Qh1.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyIWssPC.js → index-BMuGd2--.js} +1 -1
- package/src/assets/web-panel/assets/{index-DX7tuWVJ.js → index-BRqt4fO-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd3euP_4.js → index-BVTAl5N2.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cpqm1qfU.js → index-BsR7Y2NG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcPFB8qJ.js → index-Bx044qDj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd9OJhHG.js → index-C1GFfuqN.js} +1 -1
- package/src/assets/web-panel/assets/index-C3QX5tOl.js +1 -0
- package/src/assets/web-panel/assets/{index-B-Z8eFUJ.js → index-C66NpWy5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlT2VAEK.js → index-CDTvsX8t.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dsij_KsN.js → index-CGNklbP6.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4gkzxkw.js → index-CHfLYwcW.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6sm5-Xq.js → index-CNxg61YP.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0yx-jtQ.js → index-CQlHFN_g.js} +1 -1
- package/src/assets/web-panel/assets/{index-CjTrRt13.js → index-CUEU48o1.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7WBt_4T.js → index-CWGax2QW.js} +1 -1
- package/src/assets/web-panel/assets/{index-IeiL9RKc.js → index-C_JzIXUq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnHH7bnD.js → index-Cf23wVMs.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKJr-TZp.js → index-CmAInUkX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiECJi-Q.js → index-CmAubjDi.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCnZyIMA.js → index-Co7oM1qr.js} +1 -1
- package/src/assets/web-panel/assets/{index-Drlnwr8F.js → index-CwPUZbMI.js} +1 -1
- package/src/assets/web-panel/assets/{index-9R6BxAr0.js → index-D1naNj4U.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGqqJfL0.js → index-D5CsBShd.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bi46Xu4u.js → index-DBT3YeQ9.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXXtGMUJ.js → index-DLdrHhML.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0PMlbMs.js → index-Dc77VR3H.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0zcy54x.js → index-Dfv2lKu3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BN6UPD2E.js → index-DiW0sDkz.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dxge-xWl.js → index-DpTHIRfZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CyOGN6a5.js → index-DqFXGzzC.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9O9YyWE.js → index-XFe-1twF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHNYPO0b.js → index-p2HYn89m.js} +3 -3
- package/src/assets/web-panel/assets/{index-CVQMeiwH.js → index-pqfux8vZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-pPL66J85.js → index-rjzk9z1Q.js} +1 -1
- package/src/assets/web-panel/assets/{index-DoQA0-52.js → index-th2ihDc0.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5REnKt8.js → index-u2YVld9W.js} +1 -1
- package/src/assets/web-panel/assets/index-xYybvEic.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-BTL88of4.js → initDefaultProps-C3Djh9fa.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BUB2pXwB.js → motion-BzqFmbSe.js} +1 -1
- package/src/assets/web-panel/assets/{move-egCkkOMm.js → move-DJo_FFaW.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DAWhZC8c.js → mtc-parser-_tlTiBvk.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CvOxe_6d.js → omit-CB2ZyxUg.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DZUSLZ5Z.js → pickAttrs-DZwIFD4u.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BERxGNMY.js → placementArrow-DxZY0dgk.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DZuDfZ9n.js → responsiveObserve-CzzGt7GL.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DO756sgI.js → slide-BaIK52yz.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BRQlIz28.js → statusUtils-3-FHu9_M.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DCSfdybl.js → styleChecker-BkRT68fK.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-LD8dWVve.js → useFlexGapSupport-DHYwKS-s.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BX9uW2LK.js → useFs-DVVUhV8p.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BPSlPuHU.js → usePersonalDataHub-CkrFKAzQ.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BmOILC9V.js → vnode-BuBLFz0c.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-1iKowKGg.js → zoom-kpm6LtXU.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +3 -3
- package/src/commands/cowork.js +418 -41
- package/src/commands/permissions.js +240 -16
- package/src/commands/plugin.js +1293 -56
- package/src/commands/session-location.js +276 -0
- package/src/commands/session.js +2 -0
- package/src/data/changelog.json +28 -2
- package/src/gateways/ws/action-protocol.js +316 -17
- package/src/gateways/ws/message-dispatcher.js +30 -0
- package/src/gateways/ws/remote-session-protocol.js +565 -35
- package/src/gateways/ws/ws-agent-handler.js +12 -0
- package/src/gateways/ws/ws-approval-gate.js +5 -0
- package/src/gateways/ws/ws-server.js +510 -50
- package/src/harness/jsonl-session-store.js +341 -24
- package/src/harness/mcp-client.js +8 -1
- package/src/harness/remote-session-audit.js +2 -0
- package/src/harness/remote-session-crypto.js +929 -21
- package/src/harness/remote-session-registry.js +945 -24
- package/src/harness/session-list-index.js +233 -14
- package/src/lib/agent-authority.js +6 -0
- package/src/lib/auto-mode-config.js +98 -3
- package/src/lib/background-agent-keeper-client.js +248 -0
- package/src/lib/background-agent-keeper-protocol.js +291 -0
- package/src/lib/background-agent-supervisor.js +1357 -85
- package/src/lib/background-interaction-resolver.js +12 -0
- package/src/lib/background-session-transport.js +23 -6
- package/src/lib/background-turn-bootstrap-protocol.js +190 -0
- package/src/lib/cowork-workflow.js +1188 -76
- package/src/lib/delivery-production-adapter.js +38 -2
- package/src/lib/dynamic-workflow-facade.js +1736 -0
- package/src/lib/execution-location-contract.js +551 -0
- package/src/lib/execution-location-runtime.js +130 -0
- package/src/lib/ide-context.js +94 -6
- package/src/lib/mcp-stdio-package-materialization.js +166 -14
- package/src/lib/packer/pkg-config-generator.js +2 -0
- package/src/lib/permission-authority.js +77 -0
- package/src/lib/permission-side-effect-center.js +495 -0
- package/src/lib/plugin-runtime/install.js +514 -13
- package/src/lib/plugin-runtime/marketplace-artifact-readback.js +900 -0
- package/src/lib/plugin-runtime/marketplace-catalog.js +1181 -0
- package/src/lib/plugin-runtime/marketplace-impact.js +408 -0
- package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1567 -0
- package/src/lib/plugin-security.js +60 -3
- package/src/lib/process-execution-broker/index.js +1095 -57
- package/src/lib/process-execution-broker/linux-bwrap-descriptor-launch.js +375 -0
- package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +90 -16
- package/src/lib/process-execution-broker/linux-generic-bwrap.js +159 -18
- package/src/lib/process-execution-broker/macos-mcp-launcher-contract.js +278 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher-protocol.json +23 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher.c +1497 -0
- package/src/lib/process-execution-broker/mcp-fd-entry-bootstrap.js +37 -0
- package/src/lib/process-execution-broker/platform-sandbox.js +2330 -279
- package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
- package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
- package/src/lib/process-execution-broker/windows-sandbox.cs +50 -4
- package/src/lib/public-ide-capability-manifest.js +3 -0
- package/src/lib/remote-approval-bridge.js +777 -34
- package/src/lib/remote-control.js +2 -0
- package/src/lib/remote-membership-authority.js +632 -0
- package/src/lib/remote-membership-coordinator.js +2973 -0
- package/src/lib/remote-membership-host-store.js +1799 -0
- package/src/lib/runtime-provenance-ledger.js +171 -46
- package/src/lib/scoped-permission-store.js +369 -0
- package/src/lib/session-anti-rollback-anchor.js +262 -5
- package/src/lib/shell-approval.js +169 -6
- package/src/lib/sub-agent-context.js +4 -0
- package/src/lib/with-file-lock.js +73 -38
- package/src/lib/workflow-definition-contract.js +149 -0
- package/src/repl/agent-repl.js +111 -36
- package/src/repl/chat-repl.js +2 -0
- package/src/repl/clipboard-image.js +735 -16
- package/src/repl/ide-status.js +1 -1
- package/src/repl/prompt-interactions.js +59 -6
- package/src/repl/remote-approval.js +29 -7
- package/src/runtime/agent-core.js +1182 -155
- package/src/runtime/headless-runner.js +52 -30
- package/src/runtime/headless-stream.js +51 -28
- package/src/workers/background-agent-keeper.js +689 -0
- package/src/workers/background-agent-worker.js +365 -13
- package/src/workers/background-turn-bootstrap.js +170 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-yySN5YNM.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-Bb1O3Ofr.js +0 -1
- package/src/assets/web-panel/assets/Projects-CMolGhae.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-Biyn2-Ro.js +0 -5
- package/src/assets/web-panel/assets/devWarning-S1ld2LwU.js +0 -1
- package/src/assets/web-panel/assets/index-Drz6yq0m.js +0 -1
- package/src/assets/web-panel/assets/index-uWiUs7S_.js +0 -1
|
@@ -1,16 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* RuntimeProvenanceLedger -
|
|
2
|
+
* RuntimeProvenanceLedger - bounded, hash-chained runtime provenance.
|
|
3
3
|
*
|
|
4
|
-
* Records
|
|
5
|
-
*
|
|
4
|
+
* Records context sources, tool calls and responses that flow through the CLI
|
|
5
|
+
* runtime. The in-memory view is deliberately a retained window: every entry
|
|
6
|
+
* keeps an absolute index, and the hash of the last evicted entry anchors the
|
|
7
|
+
* first retained entry. Exports state explicitly when older entries are no
|
|
8
|
+
* longer resident instead of claiming that the retained suffix is a complete
|
|
9
|
+
* history.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import { createHash } from "node:crypto";
|
|
9
13
|
|
|
14
|
+
export const DEFAULT_RUNTIME_PROVENANCE_MAX_ENTRIES = 4_096;
|
|
15
|
+
const DEFAULT_GENESIS_HASH = "0".repeat(64);
|
|
16
|
+
|
|
17
|
+
function positiveEntryLimit(value) {
|
|
18
|
+
const parsed = Number(value);
|
|
19
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
20
|
+
throw new TypeError(
|
|
21
|
+
"runtime provenance maxEntries must be a positive integer",
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return parsed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function freezeJsonSnapshot(value) {
|
|
28
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
29
|
+
for (const nested of Object.values(value)) freezeJsonSnapshot(nested);
|
|
30
|
+
Object.freeze(value);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
10
35
|
class RuntimeProvenanceLedger {
|
|
11
36
|
constructor(options = {}) {
|
|
12
|
-
this.
|
|
13
|
-
|
|
37
|
+
this._maxEntries = positiveEntryLimit(
|
|
38
|
+
options.maxEntries ?? DEFAULT_RUNTIME_PROVENANCE_MAX_ENTRIES,
|
|
39
|
+
);
|
|
40
|
+
this._genesisHash = options.genesisHash || DEFAULT_GENESIS_HASH;
|
|
41
|
+
this._lastHash = this._genesisHash;
|
|
42
|
+
this._entries = new Array(this._maxEntries);
|
|
43
|
+
this._retainedStart = 0;
|
|
44
|
+
this._retainedCount = 0;
|
|
45
|
+
this._nextIndex = 0;
|
|
46
|
+
this._evictedThroughIndex = null;
|
|
47
|
+
this._evictedThroughHash = null;
|
|
48
|
+
this._entriesByIndex = new Map();
|
|
14
49
|
this._spanIndex = new Map();
|
|
15
50
|
this._traceIndex = new Map();
|
|
16
51
|
}
|
|
@@ -94,97 +129,187 @@ class RuntimeProvenanceLedger {
|
|
|
94
129
|
});
|
|
95
130
|
}
|
|
96
131
|
|
|
132
|
+
_addIndex(index, key, absoluteIndex) {
|
|
133
|
+
if (!key) return;
|
|
134
|
+
let bucket = index.get(key);
|
|
135
|
+
if (!bucket) {
|
|
136
|
+
bucket = new Set();
|
|
137
|
+
index.set(key, bucket);
|
|
138
|
+
}
|
|
139
|
+
bucket.add(absoluteIndex);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_removeIndex(index, key, absoluteIndex) {
|
|
143
|
+
if (!key) return;
|
|
144
|
+
const bucket = index.get(key);
|
|
145
|
+
if (!bucket) return;
|
|
146
|
+
bucket.delete(absoluteIndex);
|
|
147
|
+
if (bucket.size === 0) index.delete(key);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_evictOldest() {
|
|
151
|
+
const evicted = this._entries[this._retainedStart];
|
|
152
|
+
if (!evicted) {
|
|
153
|
+
throw new Error("runtime provenance ring is internally inconsistent");
|
|
154
|
+
}
|
|
155
|
+
this._entriesByIndex.delete(evicted.index);
|
|
156
|
+
this._removeIndex(this._traceIndex, evicted.traceId, evicted.index);
|
|
157
|
+
this._removeIndex(this._spanIndex, evicted.span, evicted.index);
|
|
158
|
+
this._evictedThroughIndex = evicted.index;
|
|
159
|
+
this._evictedThroughHash = evicted.hash;
|
|
160
|
+
this._entries[this._retainedStart] = undefined;
|
|
161
|
+
this._retainedStart = (this._retainedStart + 1) % this._maxEntries;
|
|
162
|
+
this._retainedCount -= 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_retainedEntries() {
|
|
166
|
+
const retained = new Array(this._retainedCount);
|
|
167
|
+
for (let offset = 0; offset < this._retainedCount; offset += 1) {
|
|
168
|
+
retained[offset] =
|
|
169
|
+
this._entries[(this._retainedStart + offset) % this._maxEntries];
|
|
170
|
+
}
|
|
171
|
+
return retained;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_retainedAnchorHash() {
|
|
175
|
+
return this._evictedThroughHash || this._genesisHash;
|
|
176
|
+
}
|
|
177
|
+
|
|
97
178
|
/**
|
|
98
|
-
* Append entry with
|
|
179
|
+
* Append an entry with an absolute index and hash-chain binding.
|
|
99
180
|
*/
|
|
100
181
|
_append(entry) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
182
|
+
// The returned entries are also the ring's storage records. Snapshot the
|
|
183
|
+
// caller's JSON data before hashing so later mutations cannot invalidate
|
|
184
|
+
// the hash chain, then recursively freeze the snapshot so storage keys
|
|
185
|
+
// used during eviction cannot be rewritten through get/export results.
|
|
186
|
+
const payload = { ...entry };
|
|
187
|
+
delete payload.index;
|
|
188
|
+
delete payload.prevHash;
|
|
189
|
+
delete payload.hash;
|
|
190
|
+
const serialized = JSON.stringify({
|
|
191
|
+
...payload,
|
|
192
|
+
index: this._nextIndex,
|
|
104
193
|
prevHash: this._lastHash,
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const hash = createHash("sha256")
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
this.
|
|
194
|
+
});
|
|
195
|
+
const snapshot = JSON.parse(serialized);
|
|
196
|
+
const hash = createHash("sha256").update(serialized).digest("hex");
|
|
197
|
+
snapshot.hash = hash;
|
|
198
|
+
const entryWithHash = freezeJsonSnapshot(snapshot);
|
|
199
|
+
|
|
200
|
+
if (this._retainedCount === this._maxEntries) this._evictOldest();
|
|
201
|
+
const writeIndex =
|
|
202
|
+
(this._retainedStart + this._retainedCount) % this._maxEntries;
|
|
203
|
+
this._entries[writeIndex] = entryWithHash;
|
|
204
|
+
this._retainedCount += 1;
|
|
205
|
+
this._entriesByIndex.set(entryWithHash.index, entryWithHash);
|
|
206
|
+
this._addIndex(
|
|
207
|
+
this._traceIndex,
|
|
208
|
+
entryWithHash.traceId,
|
|
209
|
+
entryWithHash.index,
|
|
210
|
+
);
|
|
211
|
+
this._addIndex(this._spanIndex, entryWithHash.span, entryWithHash.index);
|
|
212
|
+
this._nextIndex += 1;
|
|
112
213
|
this._lastHash = hash;
|
|
113
|
-
// Index
|
|
114
|
-
if (entry.traceId) {
|
|
115
|
-
if (!this._traceIndex.has(entry.traceId)) this._traceIndex.set(entry.traceId, []);
|
|
116
|
-
this._traceIndex.get(entry.traceId).push(entryWithHash.index);
|
|
117
|
-
}
|
|
118
|
-
if (entry.span) {
|
|
119
|
-
if (!this._spanIndex.has(entry.span)) this._spanIndex.set(entry.span, []);
|
|
120
|
-
this._spanIndex.get(entry.span).push(entryWithHash.index);
|
|
121
|
-
}
|
|
122
214
|
return entryWithHash;
|
|
123
215
|
}
|
|
124
216
|
|
|
125
217
|
/**
|
|
126
|
-
* Verify
|
|
218
|
+
* Verify the retained suffix and its independently stored boundary anchor.
|
|
127
219
|
*/
|
|
128
220
|
verifyIntegrity() {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
221
|
+
const entries = this._retainedEntries();
|
|
222
|
+
let currentHash = this._retainedAnchorHash();
|
|
223
|
+
let expectedIndex =
|
|
224
|
+
this._evictedThroughIndex === null ? 0 : this._evictedThroughIndex + 1;
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
if (entry.index !== expectedIndex || entry.prevHash !== currentHash) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
const { hash } = entry;
|
|
230
|
+
const entryForHash = { ...entry };
|
|
231
|
+
delete entryForHash.hash;
|
|
232
|
+
const computed = createHash("sha256")
|
|
233
|
+
.update(JSON.stringify(entryForHash))
|
|
234
|
+
.digest("hex");
|
|
134
235
|
if (computed !== hash) return false;
|
|
135
236
|
currentHash = hash;
|
|
237
|
+
expectedIndex += 1;
|
|
136
238
|
}
|
|
137
|
-
return
|
|
239
|
+
return currentHash === this._lastHash && expectedIndex === this._nextIndex;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
_indexedEntries(index, key) {
|
|
243
|
+
const indices = index.get(key);
|
|
244
|
+
if (!indices) return [];
|
|
245
|
+
return [...indices]
|
|
246
|
+
.map((absoluteIndex) => this._entriesByIndex.get(absoluteIndex))
|
|
247
|
+
.filter(Boolean);
|
|
138
248
|
}
|
|
139
249
|
|
|
140
250
|
/**
|
|
141
|
-
* Get
|
|
251
|
+
* Get retained entries for a trace.
|
|
142
252
|
*/
|
|
143
253
|
getTraceEntries(traceId) {
|
|
144
|
-
|
|
145
|
-
return indices.map(i => this._entries[i]);
|
|
254
|
+
return this._indexedEntries(this._traceIndex, traceId);
|
|
146
255
|
}
|
|
147
256
|
|
|
148
257
|
/**
|
|
149
|
-
* Get
|
|
258
|
+
* Get retained entries for a span.
|
|
150
259
|
*/
|
|
151
260
|
getSpanEntries(spanId) {
|
|
152
|
-
|
|
153
|
-
return indices.map(i => this._entries[i]);
|
|
261
|
+
return this._indexedEntries(this._spanIndex, spanId);
|
|
154
262
|
}
|
|
155
263
|
|
|
156
264
|
/**
|
|
157
|
-
* Get provenance
|
|
265
|
+
* Get the retained provenance window for a response.
|
|
158
266
|
*/
|
|
159
267
|
getProvenance(options = {}) {
|
|
160
268
|
if (options.traceId) return this.getTraceEntries(options.traceId);
|
|
161
269
|
if (options.span) return this.getSpanEntries(options.span);
|
|
162
|
-
return
|
|
270
|
+
return this._retainedEntries();
|
|
163
271
|
}
|
|
164
272
|
|
|
165
273
|
/**
|
|
166
|
-
* Export
|
|
274
|
+
* Export the retained window without presenting it as complete history.
|
|
167
275
|
*/
|
|
168
276
|
export() {
|
|
277
|
+
const entries = this._retainedEntries();
|
|
278
|
+
const evictedEntries = this._nextIndex - entries.length;
|
|
279
|
+
const truncated = evictedEntries > 0;
|
|
169
280
|
return {
|
|
170
|
-
|
|
281
|
+
// Compatibility: genesisHash remains the hash immediately before the
|
|
282
|
+
// first exported entry, just as it was when every entry was retained.
|
|
283
|
+
genesisHash: entries[0]?.prevHash,
|
|
284
|
+
originalGenesisHash: this._genesisHash,
|
|
171
285
|
lastHash: this._lastHash,
|
|
172
|
-
entries
|
|
286
|
+
entries,
|
|
173
287
|
verified: this.verifyIntegrity(),
|
|
288
|
+
verificationScope: truncated ? "retained-window" : "complete",
|
|
289
|
+
totalEntries: this._nextIndex,
|
|
290
|
+
retainedEntries: entries.length,
|
|
291
|
+
evictedEntries,
|
|
292
|
+
truncated,
|
|
293
|
+
anchor: {
|
|
294
|
+
hash: this._retainedAnchorHash(),
|
|
295
|
+
evictedThroughIndex: this._evictedThroughIndex,
|
|
296
|
+
firstRetainedIndex: entries[0]?.index ?? null,
|
|
297
|
+
},
|
|
174
298
|
exportedAt: new Date().toISOString(),
|
|
175
299
|
};
|
|
176
300
|
}
|
|
177
301
|
|
|
178
302
|
/**
|
|
179
|
-
* Flush pending entries to disk (called on exit)
|
|
303
|
+
* Flush pending entries to disk (called on exit).
|
|
180
304
|
*/
|
|
181
305
|
flush() {
|
|
182
|
-
// In production this would persist to disk; for now
|
|
306
|
+
// In production this would persist to disk; for now the bounded retained
|
|
307
|
+
// window and its eviction anchor remain available in memory.
|
|
183
308
|
return Promise.resolve(true);
|
|
184
309
|
}
|
|
185
310
|
}
|
|
186
311
|
|
|
187
|
-
// Export global singleton instance
|
|
312
|
+
// Export global singleton instance.
|
|
188
313
|
const runtimeProvenanceLedger = new RuntimeProvenanceLedger();
|
|
189
314
|
export default runtimeProvenanceLedger;
|
|
190
315
|
export { RuntimeProvenanceLedger, runtimeProvenanceLedger };
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-bound, expiring permission rules owned by the CLI.
|
|
3
|
+
*
|
|
4
|
+
* The state lives outside the repository so an agent cannot authorize itself
|
|
5
|
+
* by editing a project file. Mutations use the shared durable security store:
|
|
6
|
+
* cross-process lock, atomic replace, fsync, and fail-closed corrupt reads.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
12
|
+
import projectRoot from "./project-root.cjs";
|
|
13
|
+
import permissionRules from "./permission-rules.cjs";
|
|
14
|
+
import { getMachineSecurityAnchorDir } from "./paths.js";
|
|
15
|
+
import {
|
|
16
|
+
mutateSecurityStore,
|
|
17
|
+
readSecurityStore,
|
|
18
|
+
} from "./durable-security-store.js";
|
|
19
|
+
|
|
20
|
+
const { findGitProjectRoot } = projectRoot;
|
|
21
|
+
const { parseRule } = permissionRules;
|
|
22
|
+
|
|
23
|
+
const STATE_SCHEMA = "cc.scoped-permission-rules";
|
|
24
|
+
const STATE_SCHEMA_VERSION = 1;
|
|
25
|
+
const STORE_LABEL = "scoped permission";
|
|
26
|
+
const RULE_ID_PATTERN = /^spr_[0-9a-f]{32}$/;
|
|
27
|
+
const DECISIONS = new Set(["allow", "ask", "deny"]);
|
|
28
|
+
const DEFAULT_MAX_RECORDS = 1000;
|
|
29
|
+
|
|
30
|
+
export const SCOPED_PERMISSION_ERROR_CODES = Object.freeze({
|
|
31
|
+
INVALID: "CC_SCOPED_PERMISSION_INVALID",
|
|
32
|
+
CONFLICT: "CC_SCOPED_PERMISSION_CONFLICT",
|
|
33
|
+
NOT_FOUND: "CC_SCOPED_PERMISSION_NOT_FOUND",
|
|
34
|
+
CORRUPT: "CC_SCOPED_PERMISSION_CORRUPT",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function scopedPermissionError(code, message, cause = null) {
|
|
38
|
+
const error = new Error(message);
|
|
39
|
+
error.code = code;
|
|
40
|
+
if (cause) error.cause = cause;
|
|
41
|
+
return error;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function canonicalWorkspaceRoot(cwd) {
|
|
45
|
+
const resolved = path.resolve(cwd || process.cwd());
|
|
46
|
+
const root = findGitProjectRoot(resolved) || resolved;
|
|
47
|
+
let canonical = root;
|
|
48
|
+
try {
|
|
49
|
+
canonical = fs.realpathSync.native(root);
|
|
50
|
+
} catch {
|
|
51
|
+
// A missing/unreadable cwd will be rejected by the caller that uses it.
|
|
52
|
+
}
|
|
53
|
+
const normalized = path.normalize(canonical);
|
|
54
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function workspaceBinding(cwd) {
|
|
58
|
+
const root = canonicalWorkspaceRoot(cwd);
|
|
59
|
+
const id = createHash("sha256")
|
|
60
|
+
.update(`cc-scoped-permission-workspace-v1\n${root}`, "utf8")
|
|
61
|
+
.digest("hex");
|
|
62
|
+
return Object.freeze({ id, root });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function defaultScopedPermissionStatePath(cwd = process.cwd()) {
|
|
66
|
+
const workspace = workspaceBinding(cwd);
|
|
67
|
+
return path.join(
|
|
68
|
+
getMachineSecurityAnchorDir(),
|
|
69
|
+
"permission-center",
|
|
70
|
+
"workspaces",
|
|
71
|
+
`${workspace.id}.json`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function emptyState(workspace) {
|
|
76
|
+
return {
|
|
77
|
+
schema: STATE_SCHEMA,
|
|
78
|
+
schemaVersion: STATE_SCHEMA_VERSION,
|
|
79
|
+
generation: 0,
|
|
80
|
+
updatedAt: null,
|
|
81
|
+
workspace,
|
|
82
|
+
rules: [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validateRecord(record, seenIds) {
|
|
87
|
+
if (
|
|
88
|
+
!record ||
|
|
89
|
+
!RULE_ID_PATTERN.test(record.id || "") ||
|
|
90
|
+
seenIds.has(record.id) ||
|
|
91
|
+
!DECISIONS.has(record.decision) ||
|
|
92
|
+
!parseRule(record.rule) ||
|
|
93
|
+
!Number.isInteger(record.revision) ||
|
|
94
|
+
record.revision < 1 ||
|
|
95
|
+
!Number.isFinite(record.createdAt) ||
|
|
96
|
+
!Number.isFinite(record.expiresAt) ||
|
|
97
|
+
record.expiresAt <= record.createdAt ||
|
|
98
|
+
(record.revokedAt !== null && !Number.isFinite(record.revokedAt)) ||
|
|
99
|
+
typeof record.reason !== "string" ||
|
|
100
|
+
record.reason.length > 500
|
|
101
|
+
) {
|
|
102
|
+
throw scopedPermissionError(
|
|
103
|
+
SCOPED_PERMISSION_ERROR_CODES.CORRUPT,
|
|
104
|
+
"Scoped permission state contains an invalid rule record",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
seenIds.add(record.id);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function validateState(value, workspace) {
|
|
111
|
+
if (
|
|
112
|
+
!value ||
|
|
113
|
+
value.schema !== STATE_SCHEMA ||
|
|
114
|
+
value.schemaVersion !== STATE_SCHEMA_VERSION ||
|
|
115
|
+
!Number.isInteger(value.generation) ||
|
|
116
|
+
value.generation < 0 ||
|
|
117
|
+
!value.workspace ||
|
|
118
|
+
value.workspace.id !== workspace.id ||
|
|
119
|
+
value.workspace.root !== workspace.root ||
|
|
120
|
+
!Array.isArray(value.rules)
|
|
121
|
+
) {
|
|
122
|
+
throw scopedPermissionError(
|
|
123
|
+
SCOPED_PERMISSION_ERROR_CODES.CORRUPT,
|
|
124
|
+
"Scoped permission state has an invalid schema or workspace binding",
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
const seenIds = new Set();
|
|
128
|
+
for (const record of value.rules) validateRecord(record, seenIds);
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function statusFor(record, now) {
|
|
133
|
+
if (record.revokedAt !== null) return "revoked";
|
|
134
|
+
if (record.expiresAt <= now) return "expired";
|
|
135
|
+
return "active";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function projectRecord(record, now, filePath) {
|
|
139
|
+
return Object.freeze({
|
|
140
|
+
...structuredClone(record),
|
|
141
|
+
status: statusFor(record, now),
|
|
142
|
+
source: "cli-security-store",
|
|
143
|
+
sourceFile: filePath,
|
|
144
|
+
scope: "workspace",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export class ScopedPermissionStore {
|
|
149
|
+
constructor({
|
|
150
|
+
cwd = process.cwd(),
|
|
151
|
+
filePath = null,
|
|
152
|
+
now = () => Date.now(),
|
|
153
|
+
maxRecords = DEFAULT_MAX_RECORDS,
|
|
154
|
+
lock = undefined,
|
|
155
|
+
randomId = () => `spr_${randomUUID().replaceAll("-", "")}`,
|
|
156
|
+
} = {}) {
|
|
157
|
+
this.workspace = workspaceBinding(cwd);
|
|
158
|
+
this.filePath = path.resolve(
|
|
159
|
+
filePath || defaultScopedPermissionStatePath(this.workspace.root),
|
|
160
|
+
);
|
|
161
|
+
const relativeToWorkspace = path.relative(
|
|
162
|
+
this.workspace.root,
|
|
163
|
+
this.filePath,
|
|
164
|
+
);
|
|
165
|
+
if (
|
|
166
|
+
relativeToWorkspace === "" ||
|
|
167
|
+
(!relativeToWorkspace.startsWith("..") &&
|
|
168
|
+
!path.isAbsolute(relativeToWorkspace))
|
|
169
|
+
) {
|
|
170
|
+
throw scopedPermissionError(
|
|
171
|
+
SCOPED_PERMISSION_ERROR_CODES.INVALID,
|
|
172
|
+
"Scoped permission authority must be stored outside the workspace",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
this._now = typeof now === "function" ? now : () => Number(now);
|
|
176
|
+
this.maxRecords = Math.max(
|
|
177
|
+
1,
|
|
178
|
+
Math.floor(Number(maxRecords) || DEFAULT_MAX_RECORDS),
|
|
179
|
+
);
|
|
180
|
+
this._lock = lock;
|
|
181
|
+
this._randomId = randomId;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
_read() {
|
|
185
|
+
const stored = readSecurityStore(this.filePath, STORE_LABEL);
|
|
186
|
+
if (Object.keys(stored).length === 0) return emptyState(this.workspace);
|
|
187
|
+
return validateState(stored, this.workspace);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
list() {
|
|
191
|
+
const state = this._read();
|
|
192
|
+
const now = this._now();
|
|
193
|
+
return Object.freeze({
|
|
194
|
+
schema: state.schema,
|
|
195
|
+
schemaVersion: state.schemaVersion,
|
|
196
|
+
generation: state.generation,
|
|
197
|
+
updatedAt: state.updatedAt,
|
|
198
|
+
workspace: structuredClone(state.workspace),
|
|
199
|
+
file: this.filePath,
|
|
200
|
+
rules: state.rules.map((record) =>
|
|
201
|
+
projectRecord(record, now, this.filePath),
|
|
202
|
+
),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
add({
|
|
207
|
+
decision,
|
|
208
|
+
rule,
|
|
209
|
+
expiresAt,
|
|
210
|
+
reason = "",
|
|
211
|
+
expectedGeneration = null,
|
|
212
|
+
} = {}) {
|
|
213
|
+
const normalizedDecision = String(decision || "")
|
|
214
|
+
.trim()
|
|
215
|
+
.toLowerCase();
|
|
216
|
+
const normalizedRule = String(rule || "").trim();
|
|
217
|
+
const normalizedReason = String(reason || "").trim();
|
|
218
|
+
const now = this._now();
|
|
219
|
+
const expiry = Number(expiresAt);
|
|
220
|
+
if (!DECISIONS.has(normalizedDecision)) {
|
|
221
|
+
throw scopedPermissionError(
|
|
222
|
+
SCOPED_PERMISSION_ERROR_CODES.INVALID,
|
|
223
|
+
"decision must be allow | ask | deny",
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (!parseRule(normalizedRule)) {
|
|
227
|
+
throw scopedPermissionError(
|
|
228
|
+
SCOPED_PERMISSION_ERROR_CODES.INVALID,
|
|
229
|
+
"rule must be a valid Tool or Tool(pattern) permission rule",
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (!Number.isFinite(expiry) || expiry <= now) {
|
|
233
|
+
throw scopedPermissionError(
|
|
234
|
+
SCOPED_PERMISSION_ERROR_CODES.INVALID,
|
|
235
|
+
"expiresAt must be a future timestamp",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (normalizedReason.length > 500) {
|
|
239
|
+
throw scopedPermissionError(
|
|
240
|
+
SCOPED_PERMISSION_ERROR_CODES.INVALID,
|
|
241
|
+
"reason must be at most 500 characters",
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let created = null;
|
|
246
|
+
mutateSecurityStore(
|
|
247
|
+
this.filePath,
|
|
248
|
+
STORE_LABEL,
|
|
249
|
+
(draft) => {
|
|
250
|
+
const current =
|
|
251
|
+
Object.keys(draft).length === 0
|
|
252
|
+
? emptyState(this.workspace)
|
|
253
|
+
: validateState(draft, this.workspace);
|
|
254
|
+
if (
|
|
255
|
+
expectedGeneration !== null &&
|
|
256
|
+
Number(expectedGeneration) !== current.generation
|
|
257
|
+
) {
|
|
258
|
+
throw scopedPermissionError(
|
|
259
|
+
SCOPED_PERMISSION_ERROR_CODES.CONFLICT,
|
|
260
|
+
`Scoped permission generation changed (expected ${expectedGeneration}, current ${current.generation})`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const retained = [...current.rules];
|
|
265
|
+
while (retained.length >= this.maxRecords) {
|
|
266
|
+
const inactiveIndex = retained.findIndex(
|
|
267
|
+
(record) => statusFor(record, now) !== "active",
|
|
268
|
+
);
|
|
269
|
+
if (inactiveIndex < 0) break;
|
|
270
|
+
retained.splice(inactiveIndex, 1);
|
|
271
|
+
}
|
|
272
|
+
if (retained.length >= this.maxRecords) {
|
|
273
|
+
throw scopedPermissionError(
|
|
274
|
+
SCOPED_PERMISSION_ERROR_CODES.INVALID,
|
|
275
|
+
`Scoped permission store is full (${this.maxRecords} retained rules)`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const id = this._randomId();
|
|
279
|
+
if (
|
|
280
|
+
!RULE_ID_PATTERN.test(id) ||
|
|
281
|
+
retained.some((item) => item.id === id)
|
|
282
|
+
) {
|
|
283
|
+
throw scopedPermissionError(
|
|
284
|
+
SCOPED_PERMISSION_ERROR_CODES.INVALID,
|
|
285
|
+
"Could not allocate a valid unique scoped permission id",
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
created = {
|
|
289
|
+
id,
|
|
290
|
+
decision: normalizedDecision,
|
|
291
|
+
rule: normalizedRule,
|
|
292
|
+
revision: 1,
|
|
293
|
+
createdAt: now,
|
|
294
|
+
expiresAt: expiry,
|
|
295
|
+
revokedAt: null,
|
|
296
|
+
reason: normalizedReason,
|
|
297
|
+
};
|
|
298
|
+
Object.assign(draft, current, {
|
|
299
|
+
generation: current.generation + 1,
|
|
300
|
+
updatedAt: now,
|
|
301
|
+
rules: [...retained, created],
|
|
302
|
+
});
|
|
303
|
+
validateState(draft, this.workspace);
|
|
304
|
+
},
|
|
305
|
+
this._lock ? { lock: this._lock } : undefined,
|
|
306
|
+
);
|
|
307
|
+
return projectRecord(created, now, this.filePath);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
revoke({ id, expectedRevision = null } = {}) {
|
|
311
|
+
const normalizedId = String(id || "").trim();
|
|
312
|
+
if (!RULE_ID_PATTERN.test(normalizedId)) {
|
|
313
|
+
throw scopedPermissionError(
|
|
314
|
+
SCOPED_PERMISSION_ERROR_CODES.INVALID,
|
|
315
|
+
"A valid scoped permission id is required",
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const now = this._now();
|
|
319
|
+
let revoked = null;
|
|
320
|
+
mutateSecurityStore(
|
|
321
|
+
this.filePath,
|
|
322
|
+
STORE_LABEL,
|
|
323
|
+
(draft) => {
|
|
324
|
+
const current =
|
|
325
|
+
Object.keys(draft).length === 0
|
|
326
|
+
? emptyState(this.workspace)
|
|
327
|
+
: validateState(draft, this.workspace);
|
|
328
|
+
const index = current.rules.findIndex(
|
|
329
|
+
(record) => record.id === normalizedId,
|
|
330
|
+
);
|
|
331
|
+
if (index < 0) {
|
|
332
|
+
throw scopedPermissionError(
|
|
333
|
+
SCOPED_PERMISSION_ERROR_CODES.NOT_FOUND,
|
|
334
|
+
`Scoped permission rule not found: ${normalizedId}`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const record = current.rules[index];
|
|
338
|
+
if (
|
|
339
|
+
expectedRevision !== null &&
|
|
340
|
+
Number(expectedRevision) !== record.revision
|
|
341
|
+
) {
|
|
342
|
+
throw scopedPermissionError(
|
|
343
|
+
SCOPED_PERMISSION_ERROR_CODES.CONFLICT,
|
|
344
|
+
`Scoped permission revision changed (expected ${expectedRevision}, current ${record.revision})`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
if (record.revokedAt !== null) {
|
|
348
|
+
revoked = record;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
revoked = {
|
|
352
|
+
...record,
|
|
353
|
+
revision: record.revision + 1,
|
|
354
|
+
revokedAt: now,
|
|
355
|
+
};
|
|
356
|
+
const rules = [...current.rules];
|
|
357
|
+
rules[index] = revoked;
|
|
358
|
+
Object.assign(draft, current, {
|
|
359
|
+
generation: current.generation + 1,
|
|
360
|
+
updatedAt: now,
|
|
361
|
+
rules,
|
|
362
|
+
});
|
|
363
|
+
validateState(draft, this.workspace);
|
|
364
|
+
},
|
|
365
|
+
this._lock ? { lock: this._lock } : undefined,
|
|
366
|
+
);
|
|
367
|
+
return projectRecord(revoked, now, this.filePath);
|
|
368
|
+
}
|
|
369
|
+
}
|