chainlesschain 0.165.5 → 0.165.8
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 +2 -1
- package/src/assets/web-panel/assets/{AIOps-CEeYTeDB.js → AIOps-B3gWmXjo.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BeRUQ3V2.js → ActionButton-G9pdT-0e.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DrV718XF.js → Analytics-Ca30J7m2.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-Cd2Frt7Q.js → AppLayout-BtVTm8PU.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CUJMlORU.js → Artifacts-BhViNa6u.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CXJcfT6p.js → Audit-DhKq-FnY.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DOv6Z7H9.js → BackgroundAgents-BL0SVbnE.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BwdDyBI0.js → Backup-BWkblnFi.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BmLn4kYH.js → BaseInput-ClaRNrF8.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-COg8pAW2.js → Chat-sZE2Z77o.js} +4 -4
- package/src/assets/web-panel/assets/ChatBubbleRenderer-BAfKOnRe.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-BCP4uoU8.js → Checkbox-Cvhay5WM.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-pRt6Y9JI.js → Codegen-BpejL-4N.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DLaprKhS.js → Col-g2Ki6g_1.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DyATsz5f.js → Community-D2xKbNo9.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BXAaupZK.js → Compact-DbL1Pds3.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DpcJBELr.js → Compliance-DGtZCV4c.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DG4EFWyd.js → Cowork-4kQpoNsI.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-F6Oy5n2P.js → Cron-BxTYXXAN.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BOdgtalX.js → Crosschain-k0J0LT2N.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DT1dSkjH.js → DID-BaqhoPgN.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-NBUIqWzC.js → Dashboard-T4RVPIM1.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CSpHeMYB.js → Dropdown-DszOToWq.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CEG0xISr.js → EmailListRenderer-C9KJU7HF.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BTZ0BZna.js → FamilyGuardDashboard-Cmr0JnsA.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BuuCTn2V.js → Federation-BI-_2xZX.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-KAVceevk.js → FormItemContext-DsnDEPsl.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DnHpcQvW.js → GenericCardRenderer-BLncAKKK.js} +1 -1
- package/src/assets/web-panel/assets/{Git-C_B_0HwB.js → Git-BPQhGXDn.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DPSZuzuu.js → Governance-CyElbUQW.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BV-06V7q.js → Inference-CcSntBQD.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-wpDuupn6.js → KnowledgeGraph-p6e7HZ2r.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-Bseca1_I.js → Logs-CNEtr-wz.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-Bj9xI0YH.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-d3kM4DDL.js → Marketplace-Dd2notAJ.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DQ_xZ23i.js → McpTools-BBsN2CSW.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-CRIeYO4L.js → Memory-Dg2uTvwx.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BsllIa9l.js → MobileBridge-r1YMRiJV.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BUuQy30X.js → MobileProjects-C-89kSm4.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BOFDIzrC.js → Mtc-CrPZcVgf.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-BQg-vyvG.js → MtcAudit-sxi7hqJb.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-Bo1jrPdN.js → Multisig-CAgOMfF1.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-Bmod8alw.js → NLProgramming-DafaPts3.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CGGFL8w-.js → Notes-CJM-xUW_.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BsZvCJxC.js → NotificationSettings-L0ZjfAh_.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-C1-Quo_h.js → OrderTableRenderer-BB1CvCK0.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BjOeDYOi.js → Organization-DeLtgAD8.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-DwVV3l5A.js → Overflow-CKC2b10z.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Cu3DQT9W.js → P2P-eyf2rzY1.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Cg0gR7dH.js → PdhVaultBrowser-5upNIOW8.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-CsAXxRN5.js → Permissions-B8p3W6JK.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-C-nNmnYp.js → PersonalDataHub-BnRMxTq3.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-BOnW7nNc.js → Pipeline-DjCNI1li.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-ClLSyGKt.js → Privacy-u6gXLbWT.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-DuLvjjq7.js → ProjectInit-BZAw4tkM.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DQYYbDD9.js → ProjectSettings-B1Tr72q7.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-Drw8MBGK.js → Projects-DO7HFuBQ.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-ckqHQe6K.js → Providers-BjCztyhj.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-DW5BXk-G.js → QrScannerModal-ZWyCUbR1.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DDLnhNTp.js → QuickAsk-DhDjeW4G.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-PJQ2C91U.js → Recommend-R_CN2MuU.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-s6wmda34.js → RemoteSession-uxFcujeu.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-DC2sb-I3.js → Reputation-DX8D7rks.js} +1 -1
- package/src/assets/web-panel/assets/{Row-hPGk1eGj.js → Row-BYahQILD.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-RIvfZql0.js → RssFeed-CUSQ2tm-.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DFg1G1eR.js → Search-CnN_S8Cs.js} +1 -1
- package/src/assets/web-panel/assets/{Security-D6H5p8Dp.js → Security-DcZlh-O4.js} +4 -4
- package/src/assets/web-panel/assets/{Services-nxYiFV_w.js → Services-ZhBOjzIX.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BmgFNAdB.js → Skeleton-D5Qnq_lI.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CsaaD2av.js → Skills-BC9cZ3Be.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-yM5y9yvK.js → Sla-DauJ3E8_.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-DbNeZ_fh.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-CFVhEyyr.js → SyncSettings-DUvJbSct.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-DdO02Inn.js → Tasks-BXa-5noQ.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Eo2d8Llu.js → Templates-JxaiUt_3.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-ChESOXgy.js → Tenant-BW4ERQN8.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BJ7oDxTI.js → Terminal-DltUbdtv.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-hWX0fUvz.js → TimelineRenderer-CMoZz0Tc.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DVBZQnd-.js → Tokens-D_VEV60K.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-D89cQfVq.js → Trigger-CCXTCs0R.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BIVSb4-V.js → Trust-lkGRIPcG.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-B58gWopl.js → UkeySign-BfGeQ9HX.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DhHI7bmG.js → VideoEditing-BEC7pYvc.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-C4FheWS_.js → Wallet-BZdG24o0.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DnPRq-Bx.js → WebAuthn-yoEddedp.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-BerRPjpJ.js → WorkflowEditor-BXZEprOW.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BzZ52iKr.js → chat-gg0bzZal.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BI_E4BAn.js → colors-DjRVRuin.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-d50mr3yk.js → compact-item-su-dieAc.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-C6FsQ1IH.js → createContext-Ci3S8oKF.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CxbFX9uH.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DLZEMgLP.js → hasIn-D8F10heH.js} +1 -1
- package/src/assets/web-panel/assets/{index-D-aBumtY.js → index-11wtYQiF.js} +1 -1
- package/src/assets/web-panel/assets/{index-CjuKZBqJ.js → index-1IgxgALJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-9H_M2jju.js → index-1agckWSq.js} +1 -1
- package/src/assets/web-panel/assets/{index-DE0V6cp-.js → index-5JqhujnR.js} +1 -1
- package/src/assets/web-panel/assets/{index-Y2-q7QLm.js → index-6Tniqk1s.js} +1 -1
- package/src/assets/web-panel/assets/{index-DoFtCVZ7.js → index-8Z-8BIlN.js} +1 -1
- package/src/assets/web-panel/assets/{index-KNGgO4Wm.js → index-BCI5v0yB.js} +1 -1
- package/src/assets/web-panel/assets/{index-DY4j5zkF.js → index-BDJntYmJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZjjdQ62.js → index-BGzixOGZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BTpIOTfR.js → index-BMMOVoLD.js} +1 -1
- package/src/assets/web-panel/assets/{index-BB9SqVSt.js → index-BTFpdBtt.js} +1 -1
- package/src/assets/web-panel/assets/{index-AHr7ryWf.js → index-Ba_HyPnE.js} +1 -1
- package/src/assets/web-panel/assets/{index-Pvf30m8Q.js → index-BbF1IHof.js} +1 -1
- package/src/assets/web-panel/assets/{index-CRN5TRTQ.js → index-BgT9su_e.js} +1 -1
- package/src/assets/web-panel/assets/{index-CUaEHfQ0.js → index-BzPB6kGZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdKt9fOd.js → index-C6ygzplr.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dec0Kf_Z.js → index-CC3ZDjzY.js} +1 -1
- package/src/assets/web-panel/assets/{index-IPkwSAW_.js → index-CEmXRhAE.js} +1 -1
- package/src/assets/web-panel/assets/index-CJGOlduY.js +1 -0
- package/src/assets/web-panel/assets/{index-C3k8kevG.js → index-CMPqd5Cv.js} +1 -1
- package/src/assets/web-panel/assets/{index-A5CDWP7y.js → index-COWd8uGx.js} +1 -1
- package/src/assets/web-panel/assets/{index-CO8DAF7U.js → index-CPIYSp4H.js} +1 -1
- package/src/assets/web-panel/assets/{index-CVzuawCX.js → index-CR0h6seT.js} +1 -1
- package/src/assets/web-panel/assets/{index-Co7s9LsB.js → index-CUmDhC4j.js} +1 -1
- package/src/assets/web-panel/assets/index-CkBCN5yH.js +1 -0
- package/src/assets/web-panel/assets/{index-CLoTQr0W.js → index-CusgZPtc.js} +1 -1
- package/src/assets/web-panel/assets/{index-B81mo1Et.js → index-DBAETXft.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcSUl9fA.js → index-DIJ2qFOC.js} +1 -1
- package/src/assets/web-panel/assets/{index-Q5Tn735s.js → index-Dbq4NMET.js} +1 -1
- package/src/assets/web-panel/assets/{index-GXE0D8Nx.js → index-DkMEF5VT.js} +1 -1
- package/src/assets/web-panel/assets/{index-TWG-_D1M.js → index-DkfQR3hM.js} +1 -1
- package/src/assets/web-panel/assets/{index-BAE3hwev.js → index-DnLucOn9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CgkoH3_g.js → index-DugB_aKa.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRGx3w6T.js → index-DyLUarhZ.js} +3 -3
- package/src/assets/web-panel/assets/{index-BeM4hfht.js → index-T6_6ygJZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DsscwxPE.js → index-Tu8eWqh-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bmr_vxXD.js → index-W7XUgdVL.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoNZlzhQ.js → index-dHZinfko.js} +1 -1
- package/src/assets/web-panel/assets/{index-DqkbHHg8.js → index-jyNgp5FI.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-D-4Hzgej.js → initDefaultProps-Cv4p8e4w.js} +1 -1
- package/src/assets/web-panel/assets/{motion-D4Wl4xU7.js → motion-B8EwmMOw.js} +1 -1
- package/src/assets/web-panel/assets/{move-Cd5eQvL0.js → move-B8R9F8TK.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-B4ilslCW.js → mtc-parser-DoFYz5Bp.js} +1 -1
- package/src/assets/web-panel/assets/{omit-Ckarx-tf.js → omit-CHWYLBjI.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-QJ0bpCmc.js → pickAttrs-CXuPDO77.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CKHm-tq8.js → placementArrow-BaAtUUon.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-CV6a_w_Y.js → responsiveObserve-DCc4zcDD.js} +1 -1
- package/src/assets/web-panel/assets/{slide-mHpeQFgk.js → slide-B_lx89oZ.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DIaQNQWA.js → statusUtils-DE8DPmR0.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-PJpzavV8.js → styleChecker-1ktXOsL_.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-BH7zTw8S.js → useFlexGapSupport-xH89-wjh.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BDNG3JDd.js → useFs-DscvfW5p.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CY2VWdUg.js → usePersonalDataHub-RdMKQta7.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-D0kw9jhz.js → vnode-Z3wTni2T.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-B5g7TN7r.js → zoom-B0iChCsm.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +3 -3
- package/src/commands/agent.js +105 -62
- package/src/commands/background-session.js +100 -10
- package/src/commands/git.js +39 -0
- package/src/commands/mcp.js +5 -0
- package/src/commands/plugin.js +160 -29
- package/src/commands/remote-control.js +295 -110
- package/src/commands/session-group.js +131 -0
- package/src/commands/session-location.js +48 -0
- package/src/commands/session-message.js +171 -0
- package/src/commands/session.js +22 -0
- package/src/commands/sync-providers.js +4 -1
- package/src/commands/update.js +1 -0
- package/src/data/changelog.json +41 -2
- package/src/gateways/ws/ws-agent-handler.js +20 -12
- package/src/harness/jsonl-session-store.js +279 -37
- package/src/harness/mcp-client.js +832 -91
- package/src/harness/session-index.js +15 -4
- package/src/lib/agent-worktree.js +8 -1
- package/src/lib/auto-mode-safety-classifier.js +3 -17
- package/src/lib/background-agent-keeper-protocol.js +26 -6
- package/src/lib/background-agent-supervisor.js +17 -1
- package/src/lib/background-session-transport.js +153 -27
- package/src/lib/browser-evidence.js +1405 -0
- package/src/lib/chrome-connector.js +465 -79
- package/src/lib/claude-project-auto-memory.js +481 -0
- package/src/lib/claude-project-storage-layout.js +104 -0
- package/src/lib/cloud/cloud-client.js +40 -0
- package/src/lib/execution-location-local-supervisor.mjs +226 -0
- package/src/lib/execution-location-runner-lifecycle.js +991 -0
- package/src/lib/execution-location-target-preflight.js +333 -0
- package/src/lib/execution-location-target.js +783 -13
- package/src/lib/file-lines.js +6 -1
- package/src/lib/gateway-client-resilience.js +571 -0
- package/src/lib/gitlab-mr-worktree.js +176 -0
- package/src/lib/hooks-v2-producers.js +119 -15
- package/src/lib/hooks-v2-workspace-context.js +44 -1
- package/src/lib/host-resource-budget.js +295 -0
- package/src/lib/ide-context.js +15 -2
- package/src/lib/llm-config-defaults.js +43 -0
- package/src/lib/mcp-client.js +2 -0
- package/src/lib/mcp-headers-helper.js +47 -5
- package/src/lib/mcp-lifecycle-authority.js +887 -0
- package/src/lib/mcp-oauth.js +156 -26
- package/src/lib/mcp-tls.js +330 -0
- package/src/lib/output-styles.js +12 -0
- package/src/lib/paths.js +260 -32
- package/src/lib/permission-request.js +48 -0
- package/src/lib/plugin-runtime/bin.js +3 -1
- package/src/lib/plugin-runtime/hooks.js +3 -1
- package/src/lib/plugin-runtime/install.js +730 -71
- package/src/lib/plugin-runtime/lsp.js +4 -4
- package/src/lib/plugin-runtime/marketplace-archive-source.js +366 -0
- package/src/lib/plugin-runtime/marketplace-catalog.js +63 -6
- package/src/lib/plugin-runtime/marketplace-command-descriptor.js +343 -0
- package/src/lib/plugin-runtime/marketplace-command-source.js +580 -0
- package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1 -0
- package/src/lib/plugin-runtime/marketplace-source-adapter.js +188 -0
- package/src/lib/plugin-runtime/marketplace-source-cache.js +11 -1
- package/src/lib/plugin-runtime/mcp.js +3 -1
- package/src/lib/plugin-runtime/monitors.js +3 -1
- package/src/lib/plugin-runtime/remote-source.js +583 -58
- package/src/lib/plugin-runtime/settings.js +3 -1
- package/src/lib/plugin-runtime/trust.js +274 -13
- package/src/lib/plugin-security.js +393 -41
- package/src/lib/plugin-source-identity.js +850 -0
- package/src/lib/process-execution-broker/credential-agent.js +210 -41
- package/src/lib/process-execution-broker/index.js +15 -1
- package/src/lib/process-execution-broker/linux-cgroup-v2.js +391 -0
- package/src/lib/process-execution-broker/platform-sandbox.js +50 -28
- package/src/lib/project-mcp-trust.js +165 -10
- package/src/lib/remote-approval-bridge.js +136 -46
- package/src/lib/remote-control.js +217 -31
- package/src/lib/resume-session.js +9 -6
- package/src/lib/session-core-singletons.js +26 -11
- package/src/lib/session-host-lease.js +52 -16
- package/src/lib/session-lifecycle-scope.js +153 -0
- package/src/lib/session-message-fabric.js +1252 -0
- package/src/lib/session-projection.js +286 -3
- package/src/lib/session-runtime-retention.js +155 -0
- package/src/lib/session-store-guard.js +3 -1
- package/src/lib/session-workbench-store.js +466 -0
- package/src/lib/settings-loader.cjs +29 -0
- package/src/lib/spellcheck.js +211 -0
- package/src/lib/sub-agent-context.js +9 -9
- package/src/lib/sync-cli-db.js +485 -129
- package/src/lib/sync-credentials.js +31 -20
- package/src/lib/web-fetch.js +112 -40
- package/src/lib/web-search.js +127 -61
- package/src/lib/workspace-trust.js +793 -0
- package/src/repl/agent-repl.js +175 -19
- package/src/repl/permission-prompt.js +29 -8
- package/src/repl/repl-keybindings.js +55 -0
- package/src/runtime/agent-core.js +362 -20
- package/src/runtime/bootstrap.js +37 -13
- package/src/runtime/coding-agent-contract-shared.cjs +11 -4
- package/src/runtime/headless-runner.js +281 -102
- package/src/runtime/headless-stream.js +199 -30
- package/src/runtime/mcp-config.js +298 -6
- package/src/runtime/policies/agent-policy.js +10 -0
- package/src/runtime/status-diagnostics-lite.js +9 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-4gGkdGgA.js +0 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-B4RLp2zs.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-DPy1XMYh.js +0 -1
- package/src/assets/web-panel/assets/devWarning-CagEcIex.js +0 -1
- package/src/assets/web-panel/assets/index-pzT9PpOU.js +0 -1
- package/src/assets/web-panel/assets/index-z1kFadaX.js +0 -1
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical workspace trust decisions.
|
|
3
|
+
*
|
|
4
|
+
* This module is deliberately narrower than the individual MCP, plugin, and
|
|
5
|
+
* Hooks trust stores. Those stores remain the authoritative evidence and
|
|
6
|
+
* consent records for their respective products; this module gives them one
|
|
7
|
+
* canonical workspace/repository identity, one strict decision lattice, and a
|
|
8
|
+
* redacted audit projection. A missing identity, malformed shared record, or
|
|
9
|
+
* unknown evidence is never promoted to an allow.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import crypto from "node:crypto";
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { getHomeDir } from "./paths.js";
|
|
16
|
+
import { withFileLock } from "./with-file-lock.js";
|
|
17
|
+
import {
|
|
18
|
+
mutateSecurityStore,
|
|
19
|
+
readSecurityStore,
|
|
20
|
+
} from "./durable-security-store.js";
|
|
21
|
+
|
|
22
|
+
export const WORKSPACE_TRUST_SCHEMA_VERSION = 1;
|
|
23
|
+
export const WORKSPACE_TRUST_AUDIT_SCHEMA =
|
|
24
|
+
"chainlesschain.workspace-trust-audit/v1";
|
|
25
|
+
export const WORKSPACE_TRUST_STORE_SCHEMA =
|
|
26
|
+
"chainlesschain.workspace-trust-store/v1";
|
|
27
|
+
|
|
28
|
+
export const WORKSPACE_TRUST_DECISION = Object.freeze({
|
|
29
|
+
ALLOW: "allow",
|
|
30
|
+
ASK: "ask",
|
|
31
|
+
DENY: "deny",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const DECISION_RANK = Object.freeze({
|
|
35
|
+
[WORKSPACE_TRUST_DECISION.ALLOW]: 0,
|
|
36
|
+
[WORKSPACE_TRUST_DECISION.ASK]: 1,
|
|
37
|
+
[WORKSPACE_TRUST_DECISION.DENY]: 2,
|
|
38
|
+
});
|
|
39
|
+
const CONSENT_RE = /^[a-z][a-z0-9_-]{0,63}$/u;
|
|
40
|
+
const SOURCE_RE = /^[a-z][a-z0-9_-]{0,63}$/u;
|
|
41
|
+
const MAX_GIT_METADATA_BYTES = 64 * 1024;
|
|
42
|
+
|
|
43
|
+
export const _deps = {
|
|
44
|
+
fs,
|
|
45
|
+
now: () => new Date().toISOString(),
|
|
46
|
+
withFileLock,
|
|
47
|
+
storePath: () => path.join(getHomeDir(), "workspace-trust-v1.json"),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function trustError(code, message, cause) {
|
|
51
|
+
const error = new Error(message, cause ? { cause } : undefined);
|
|
52
|
+
error.name = "WorkspaceTrustError";
|
|
53
|
+
error.code = code;
|
|
54
|
+
return error;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sha256(label, value) {
|
|
58
|
+
return crypto
|
|
59
|
+
.createHash("sha256")
|
|
60
|
+
.update(`${label}\0`, "utf8")
|
|
61
|
+
.update(String(value), "utf8")
|
|
62
|
+
.digest("hex");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ownString(value, label, max = 4096) {
|
|
66
|
+
if (
|
|
67
|
+
typeof value !== "string" ||
|
|
68
|
+
value.length === 0 ||
|
|
69
|
+
value.length > max ||
|
|
70
|
+
value.includes("\0")
|
|
71
|
+
) {
|
|
72
|
+
throw trustError(
|
|
73
|
+
"CC_WORKSPACE_TRUST_INVALID_INPUT",
|
|
74
|
+
`${label} must be a bounded non-empty string`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function realpathNative(fsApi, target) {
|
|
81
|
+
const resolver = fsApi.realpathSync?.native || fsApi.realpathSync;
|
|
82
|
+
if (typeof resolver !== "function") {
|
|
83
|
+
throw trustError(
|
|
84
|
+
"CC_WORKSPACE_TRUST_IDENTITY_UNAVAILABLE",
|
|
85
|
+
"workspace realpath capability is unavailable",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return resolver(target);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function lstatOptional(fsApi, target) {
|
|
92
|
+
try {
|
|
93
|
+
return fsApi.lstatSync(target);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error?.code === "ENOENT") return null;
|
|
96
|
+
throw trustError(
|
|
97
|
+
"CC_WORKSPACE_TRUST_IDENTITY_UNAVAILABLE",
|
|
98
|
+
"workspace metadata is unavailable",
|
|
99
|
+
error,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function directoryEvidence(fsApi, canonicalPath, label) {
|
|
105
|
+
let stats;
|
|
106
|
+
try {
|
|
107
|
+
stats = fsApi.statSync(canonicalPath, { bigint: true });
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw trustError(
|
|
110
|
+
"CC_WORKSPACE_TRUST_IDENTITY_UNAVAILABLE",
|
|
111
|
+
`${label} metadata is unavailable`,
|
|
112
|
+
error,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (!stats?.isDirectory?.()) {
|
|
116
|
+
throw trustError(
|
|
117
|
+
"CC_WORKSPACE_TRUST_IDENTITY_UNAVAILABLE",
|
|
118
|
+
`${label} must resolve to a directory`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const birthtime = stats.birthtimeNs;
|
|
122
|
+
const ctime = stats.ctimeNs;
|
|
123
|
+
const generation =
|
|
124
|
+
typeof birthtime === "bigint" && birthtime > 0n
|
|
125
|
+
? `birth:${birthtime.toString()}`
|
|
126
|
+
: `ctime:${String(ctime ?? stats.ctimeMs ?? "unknown")}`;
|
|
127
|
+
const device = String(stats.dev ?? "unknown");
|
|
128
|
+
const inode = String(stats.ino ?? "unknown");
|
|
129
|
+
if (device === "unknown" || inode === "unknown") {
|
|
130
|
+
throw trustError(
|
|
131
|
+
"CC_WORKSPACE_TRUST_IDENTITY_UNAVAILABLE",
|
|
132
|
+
`${label} filesystem identity is unavailable`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return Object.freeze({ device, inode, generation });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function readBoundedText(fsApi, target, label) {
|
|
139
|
+
let stats;
|
|
140
|
+
try {
|
|
141
|
+
stats = fsApi.lstatSync(target);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw trustError(
|
|
144
|
+
"CC_WORKSPACE_TRUST_GIT_METADATA_INVALID",
|
|
145
|
+
`${label} is unavailable`,
|
|
146
|
+
error,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (
|
|
150
|
+
!stats.isFile() ||
|
|
151
|
+
stats.isSymbolicLink() ||
|
|
152
|
+
stats.size <= 0 ||
|
|
153
|
+
stats.size > MAX_GIT_METADATA_BYTES
|
|
154
|
+
) {
|
|
155
|
+
throw trustError(
|
|
156
|
+
"CC_WORKSPACE_TRUST_GIT_METADATA_INVALID",
|
|
157
|
+
`${label} must be a bounded regular file`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
return fsApi.readFileSync(target, "utf8");
|
|
162
|
+
} catch (error) {
|
|
163
|
+
throw trustError(
|
|
164
|
+
"CC_WORKSPACE_TRUST_GIT_METADATA_INVALID",
|
|
165
|
+
`${label} could not be read`,
|
|
166
|
+
error,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseGitDirectory(text, baseDirectory, label) {
|
|
172
|
+
// A worktree .git file is one exact record. Do not search arbitrary
|
|
173
|
+
// multiline content for a later `gitdir:` line: that would make metadata
|
|
174
|
+
// parsing depend on attacker-controlled decoration.
|
|
175
|
+
const match = /^gitdir:[ \t]*(.+?)[ \t]*(?:\r?\n)?$/u.exec(String(text));
|
|
176
|
+
if (!match || /[\0\r\n]/u.test(match[1])) {
|
|
177
|
+
throw trustError(
|
|
178
|
+
"CC_WORKSPACE_TRUST_GIT_METADATA_INVALID",
|
|
179
|
+
`${label} is not a valid gitdir record`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return path.resolve(baseDirectory, match[1]);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function resolveGitCommonDirectory(fsApi, gitDirectory) {
|
|
186
|
+
const commonFile = path.join(gitDirectory, "commondir");
|
|
187
|
+
const commonStat = lstatOptional(fsApi, commonFile);
|
|
188
|
+
if (!commonStat) return gitDirectory;
|
|
189
|
+
const text = readBoundedText(fsApi, commonFile, "Git commondir metadata");
|
|
190
|
+
const relative = String(text).trim();
|
|
191
|
+
if (!relative || /[\0\r\n]/u.test(relative)) {
|
|
192
|
+
throw trustError(
|
|
193
|
+
"CC_WORKSPACE_TRUST_GIT_METADATA_INVALID",
|
|
194
|
+
"Git commondir metadata is invalid",
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const resolved = realpathNative(fsApi, path.resolve(gitDirectory, relative));
|
|
198
|
+
directoryEvidence(fsApi, resolved, "Git common directory");
|
|
199
|
+
return resolved;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function discoverRepository(fsApi, canonicalStart) {
|
|
203
|
+
let candidate = canonicalStart;
|
|
204
|
+
for (;;) {
|
|
205
|
+
const dotGit = path.join(candidate, ".git");
|
|
206
|
+
const metadata = lstatOptional(fsApi, dotGit);
|
|
207
|
+
if (metadata) {
|
|
208
|
+
if (metadata.isSymbolicLink()) {
|
|
209
|
+
throw trustError(
|
|
210
|
+
"CC_WORKSPACE_TRUST_GIT_METADATA_INVALID",
|
|
211
|
+
"Git metadata may not be a symbolic link",
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (metadata.isDirectory()) {
|
|
215
|
+
const gitDirectory = realpathNative(fsApi, dotGit);
|
|
216
|
+
directoryEvidence(fsApi, gitDirectory, "Git directory");
|
|
217
|
+
return {
|
|
218
|
+
repositoryRoot: candidate,
|
|
219
|
+
gitDirectory,
|
|
220
|
+
commonDirectory: resolveGitCommonDirectory(fsApi, gitDirectory),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (metadata.isFile()) {
|
|
224
|
+
const target = parseGitDirectory(
|
|
225
|
+
readBoundedText(fsApi, dotGit, "Git worktree metadata"),
|
|
226
|
+
candidate,
|
|
227
|
+
"Git worktree metadata",
|
|
228
|
+
);
|
|
229
|
+
const gitDirectory = realpathNative(fsApi, target);
|
|
230
|
+
directoryEvidence(fsApi, gitDirectory, "Git worktree directory");
|
|
231
|
+
return {
|
|
232
|
+
repositoryRoot: candidate,
|
|
233
|
+
gitDirectory,
|
|
234
|
+
commonDirectory: resolveGitCommonDirectory(fsApi, gitDirectory),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
throw trustError(
|
|
238
|
+
"CC_WORKSPACE_TRUST_GIT_METADATA_INVALID",
|
|
239
|
+
"Git metadata has an unsupported type",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const parent = path.dirname(candidate);
|
|
243
|
+
if (parent === candidate) return null;
|
|
244
|
+
candidate = parent;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function freezeIdentity(value) {
|
|
249
|
+
const identity = {
|
|
250
|
+
schemaVersion: WORKSPACE_TRUST_SCHEMA_VERSION,
|
|
251
|
+
workspaceId: value.workspaceId,
|
|
252
|
+
repositoryId: value.repositoryId,
|
|
253
|
+
kind: value.kind,
|
|
254
|
+
linkedWorktree: value.linkedWorktree,
|
|
255
|
+
};
|
|
256
|
+
Object.defineProperties(identity, {
|
|
257
|
+
canonicalWorkspaceRoot: {
|
|
258
|
+
value: value.canonicalWorkspaceRoot,
|
|
259
|
+
enumerable: false,
|
|
260
|
+
},
|
|
261
|
+
canonicalRepositoryRoot: {
|
|
262
|
+
value: value.canonicalRepositoryRoot,
|
|
263
|
+
enumerable: false,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
return Object.freeze(identity);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function validateIdentity(identity) {
|
|
270
|
+
if (
|
|
271
|
+
!identity ||
|
|
272
|
+
identity.schemaVersion !== WORKSPACE_TRUST_SCHEMA_VERSION ||
|
|
273
|
+
!/^[a-f0-9]{64}$/u.test(identity.workspaceId || "") ||
|
|
274
|
+
!/^[a-f0-9]{64}$/u.test(identity.repositoryId || "") ||
|
|
275
|
+
!new Set(["git", "directory"]).has(identity.kind) ||
|
|
276
|
+
typeof identity.linkedWorktree !== "boolean"
|
|
277
|
+
) {
|
|
278
|
+
throw trustError(
|
|
279
|
+
"CC_WORKSPACE_TRUST_INVALID_IDENTITY",
|
|
280
|
+
"workspace trust identity is invalid",
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
return identity;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Resolve one canonical workspace and repository identity without invoking
|
|
288
|
+
* Git. Repository IDs bind Git linked worktrees to their common Git
|
|
289
|
+
* directory; workspace IDs remain unique per worktree/root. IDs intentionally
|
|
290
|
+
* derive from stable filesystem identity rather than the spelling of a path.
|
|
291
|
+
*/
|
|
292
|
+
export function resolveCanonicalWorkspaceRepoIdentity(
|
|
293
|
+
workspaceRoot,
|
|
294
|
+
opts = {},
|
|
295
|
+
) {
|
|
296
|
+
const fsApi = opts.fs || _deps.fs;
|
|
297
|
+
const requested = ownString(workspaceRoot, "workspace root");
|
|
298
|
+
const canonicalStart = realpathNative(fsApi, path.resolve(requested));
|
|
299
|
+
directoryEvidence(fsApi, canonicalStart, "workspace root");
|
|
300
|
+
const repository = discoverRepository(fsApi, canonicalStart);
|
|
301
|
+
const canonicalWorkspaceRoot = repository?.repositoryRoot || canonicalStart;
|
|
302
|
+
const workspaceEvidence = directoryEvidence(
|
|
303
|
+
fsApi,
|
|
304
|
+
canonicalWorkspaceRoot,
|
|
305
|
+
"workspace root",
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
if (!repository) {
|
|
309
|
+
const workspaceKey = `${workspaceEvidence.device}\0${workspaceEvidence.inode}\0${workspaceEvidence.generation}`;
|
|
310
|
+
return freezeIdentity({
|
|
311
|
+
workspaceId: sha256(
|
|
312
|
+
"chainlesschain.workspace-trust.workspace.v1",
|
|
313
|
+
workspaceKey,
|
|
314
|
+
),
|
|
315
|
+
repositoryId: sha256(
|
|
316
|
+
"chainlesschain.workspace-trust.directory-repository.v1",
|
|
317
|
+
workspaceKey,
|
|
318
|
+
),
|
|
319
|
+
kind: "directory",
|
|
320
|
+
linkedWorktree: false,
|
|
321
|
+
canonicalWorkspaceRoot,
|
|
322
|
+
canonicalRepositoryRoot: canonicalWorkspaceRoot,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const commonEvidence = directoryEvidence(
|
|
327
|
+
fsApi,
|
|
328
|
+
repository.commonDirectory,
|
|
329
|
+
"Git common directory",
|
|
330
|
+
);
|
|
331
|
+
const workspaceKey = `${workspaceEvidence.device}\0${workspaceEvidence.inode}\0${workspaceEvidence.generation}`;
|
|
332
|
+
const repositoryKey = `${commonEvidence.device}\0${commonEvidence.inode}\0${commonEvidence.generation}`;
|
|
333
|
+
return freezeIdentity({
|
|
334
|
+
workspaceId: sha256(
|
|
335
|
+
"chainlesschain.workspace-trust.git-worktree.v1",
|
|
336
|
+
workspaceKey,
|
|
337
|
+
),
|
|
338
|
+
repositoryId: sha256(
|
|
339
|
+
"chainlesschain.workspace-trust.git-repository.v1",
|
|
340
|
+
repositoryKey,
|
|
341
|
+
),
|
|
342
|
+
kind: "git",
|
|
343
|
+
linkedWorktree: repository.gitDirectory !== repository.commonDirectory,
|
|
344
|
+
canonicalWorkspaceRoot,
|
|
345
|
+
canonicalRepositoryRoot: canonicalWorkspaceRoot,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Return a relocation-stable subject for a file contained by a workspace. */
|
|
350
|
+
export function workspaceTrustPathSubject(identity, file, opts = {}) {
|
|
351
|
+
const validIdentity = validateIdentity(identity);
|
|
352
|
+
const fsApi = opts.fs || _deps.fs;
|
|
353
|
+
const canonical = realpathNative(
|
|
354
|
+
fsApi,
|
|
355
|
+
path.resolve(ownString(file, "file")),
|
|
356
|
+
);
|
|
357
|
+
const relative = path.relative(
|
|
358
|
+
validIdentity.canonicalWorkspaceRoot,
|
|
359
|
+
canonical,
|
|
360
|
+
);
|
|
361
|
+
if (
|
|
362
|
+
!relative ||
|
|
363
|
+
relative.startsWith("..") ||
|
|
364
|
+
path.isAbsolute(relative) ||
|
|
365
|
+
relative.includes("\0")
|
|
366
|
+
) {
|
|
367
|
+
throw trustError(
|
|
368
|
+
"CC_WORKSPACE_TRUST_SUBJECT_OUTSIDE_WORKSPACE",
|
|
369
|
+
"trust subject is outside the canonical workspace",
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return relative.split(path.sep).join("/");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function normalizeDecision(value) {
|
|
376
|
+
return Object.prototype.hasOwnProperty.call(DECISION_RANK, value)
|
|
377
|
+
? value
|
|
378
|
+
: WORKSPACE_TRUST_DECISION.DENY;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function normalizeSource(value) {
|
|
382
|
+
const source = String(value || "")
|
|
383
|
+
.trim()
|
|
384
|
+
.toLowerCase();
|
|
385
|
+
return SOURCE_RE.test(source) ? source : "invalid";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function normalizeConsent(value) {
|
|
389
|
+
const consent = String(value || "")
|
|
390
|
+
.trim()
|
|
391
|
+
.toLowerCase();
|
|
392
|
+
return CONSENT_RE.test(consent) ? consent : "unknown";
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function fingerprintDigest(value) {
|
|
396
|
+
if (typeof value === "string" || Buffer.isBuffer(value)) {
|
|
397
|
+
return sha256("chainlesschain.workspace-trust.evidence.v1", value);
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
return sha256(
|
|
401
|
+
"chainlesschain.workspace-trust.evidence.v1",
|
|
402
|
+
JSON.stringify(value),
|
|
403
|
+
);
|
|
404
|
+
} catch {
|
|
405
|
+
return sha256("chainlesschain.workspace-trust.evidence.v1", "invalid");
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function normalizeEvidence(value) {
|
|
410
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
411
|
+
return Object.freeze({
|
|
412
|
+
source: "invalid",
|
|
413
|
+
decision: WORKSPACE_TRUST_DECISION.DENY,
|
|
414
|
+
consent: "unknown",
|
|
415
|
+
fingerprint: fingerprintDigest("invalid"),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
const source = normalizeSource(value.source);
|
|
419
|
+
return Object.freeze({
|
|
420
|
+
source,
|
|
421
|
+
decision:
|
|
422
|
+
source === "invalid"
|
|
423
|
+
? WORKSPACE_TRUST_DECISION.DENY
|
|
424
|
+
: normalizeDecision(value.decision),
|
|
425
|
+
consent: normalizeConsent(value.consent),
|
|
426
|
+
fingerprint: fingerprintDigest(value.fingerprint),
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function finalReason(decision, evidence) {
|
|
431
|
+
if (decision === WORKSPACE_TRUST_DECISION.ALLOW) return "trusted";
|
|
432
|
+
if (decision === WORKSPACE_TRUST_DECISION.ASK) return "consent_required";
|
|
433
|
+
if (evidence.length === 0) return "no_trust_evidence";
|
|
434
|
+
return "trust_denied";
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function freezeDecision(value) {
|
|
438
|
+
return Object.freeze({
|
|
439
|
+
schemaVersion: WORKSPACE_TRUST_SCHEMA_VERSION,
|
|
440
|
+
decision: value.decision,
|
|
441
|
+
reason: value.reason,
|
|
442
|
+
identity: value.identity,
|
|
443
|
+
evidence: Object.freeze(value.evidence),
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Intersect all supplied evidence. `deny` wins over `ask`, which wins over
|
|
449
|
+
* `allow`; absent or malformed evidence is denied. Identity failures are
|
|
450
|
+
* returned as a redaction-safe deny rather than falling back to a path string.
|
|
451
|
+
*/
|
|
452
|
+
export function evaluateWorkspaceTrustDecision(options = {}) {
|
|
453
|
+
let identity;
|
|
454
|
+
try {
|
|
455
|
+
identity = options.identity
|
|
456
|
+
? validateIdentity(options.identity)
|
|
457
|
+
: resolveCanonicalWorkspaceRepoIdentity(options.workspaceRoot, options);
|
|
458
|
+
} catch {
|
|
459
|
+
return freezeDecision({
|
|
460
|
+
decision: WORKSPACE_TRUST_DECISION.DENY,
|
|
461
|
+
reason: "workspace_identity_unavailable",
|
|
462
|
+
identity: null,
|
|
463
|
+
evidence: [],
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
const evidence = Array.isArray(options.evidence)
|
|
467
|
+
? options.evidence.map(normalizeEvidence)
|
|
468
|
+
: [normalizeEvidence(null)];
|
|
469
|
+
let decision = evidence.length
|
|
470
|
+
? WORKSPACE_TRUST_DECISION.ALLOW
|
|
471
|
+
: WORKSPACE_TRUST_DECISION.DENY;
|
|
472
|
+
for (const item of evidence) {
|
|
473
|
+
if (DECISION_RANK[item.decision] > DECISION_RANK[decision]) {
|
|
474
|
+
decision = item.decision;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return freezeDecision({
|
|
478
|
+
decision,
|
|
479
|
+
reason: finalReason(decision, evidence),
|
|
480
|
+
identity,
|
|
481
|
+
evidence,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Legacy grants are never silently widened. A legacy `allow` is converted to
|
|
487
|
+
* `ask` until a source records its canonical v1 consent; any prior ask/deny
|
|
488
|
+
* remains at least as restrictive under the normal decision intersection.
|
|
489
|
+
*/
|
|
490
|
+
export function migrateLegacyWorkspaceTrustDecision(options = {}) {
|
|
491
|
+
const legacy = Array.isArray(options.legacyEvidence)
|
|
492
|
+
? options.legacyEvidence
|
|
493
|
+
: [];
|
|
494
|
+
const evidence = legacy.map((item) => ({
|
|
495
|
+
source: item?.source || "legacy",
|
|
496
|
+
consent: item?.consent || "legacy",
|
|
497
|
+
fingerprint: item?.fingerprint || "legacy",
|
|
498
|
+
decision:
|
|
499
|
+
normalizeDecision(item?.decision) === WORKSPACE_TRUST_DECISION.ALLOW
|
|
500
|
+
? WORKSPACE_TRUST_DECISION.ASK
|
|
501
|
+
: normalizeDecision(item?.decision),
|
|
502
|
+
}));
|
|
503
|
+
return evaluateWorkspaceTrustDecision({
|
|
504
|
+
...options,
|
|
505
|
+
evidence: [...evidence, ...(options.evidence || [])],
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Project a decision for logs/events without workspace paths, commands, URLs, or secrets. */
|
|
510
|
+
export function projectWorkspaceTrustAudit(decision) {
|
|
511
|
+
const value = decision && typeof decision === "object" ? decision : {};
|
|
512
|
+
const identity = value.identity;
|
|
513
|
+
const evidence = Array.isArray(value.evidence) ? value.evidence : [];
|
|
514
|
+
return Object.freeze({
|
|
515
|
+
schema_version: WORKSPACE_TRUST_AUDIT_SCHEMA,
|
|
516
|
+
decision: normalizeDecision(value.decision),
|
|
517
|
+
reason:
|
|
518
|
+
typeof value.reason === "string" && /^[a-z_]{1,64}$/u.test(value.reason)
|
|
519
|
+
? value.reason
|
|
520
|
+
: "trust_denied",
|
|
521
|
+
workspace_id:
|
|
522
|
+
identity && /^[a-f0-9]{64}$/u.test(identity.workspaceId || "")
|
|
523
|
+
? identity.workspaceId
|
|
524
|
+
: null,
|
|
525
|
+
repository_id:
|
|
526
|
+
identity && /^[a-f0-9]{64}$/u.test(identity.repositoryId || "")
|
|
527
|
+
? identity.repositoryId
|
|
528
|
+
: null,
|
|
529
|
+
workspace_kind: identity?.kind || null,
|
|
530
|
+
linked_worktree: identity?.linkedWorktree === true,
|
|
531
|
+
evidence: Object.freeze(
|
|
532
|
+
evidence.map((item) =>
|
|
533
|
+
Object.freeze({
|
|
534
|
+
source: normalizeSource(item?.source),
|
|
535
|
+
decision: normalizeDecision(item?.decision),
|
|
536
|
+
consent: normalizeConsent(item?.consent),
|
|
537
|
+
fingerprint:
|
|
538
|
+
typeof item?.fingerprint === "string" &&
|
|
539
|
+
/^[a-f0-9]{64}$/u.test(item.fingerprint)
|
|
540
|
+
? item.fingerprint
|
|
541
|
+
: fingerprintDigest("invalid"),
|
|
542
|
+
}),
|
|
543
|
+
),
|
|
544
|
+
),
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function workspaceTrustStorePath(options = {}) {
|
|
549
|
+
return options.storePath || _deps.storePath();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function subjectDigest(value) {
|
|
553
|
+
return sha256(
|
|
554
|
+
"chainlesschain.workspace-trust.subject.v1",
|
|
555
|
+
ownString(value, "trust subject"),
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function recordKey(identity, source, subject) {
|
|
560
|
+
return sha256(
|
|
561
|
+
"chainlesschain.workspace-trust.record.v1",
|
|
562
|
+
`${identity.workspaceId}\0${identity.repositoryId}\0${source}\0${subjectDigest(subject)}`,
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function trustedStoreRecords(store) {
|
|
567
|
+
if (!store || typeof store !== "object" || Array.isArray(store)) {
|
|
568
|
+
throw trustError(
|
|
569
|
+
"CC_WORKSPACE_TRUST_STORE_INVALID",
|
|
570
|
+
"workspace trust store is invalid",
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
if (Object.keys(store).length === 0) return {};
|
|
574
|
+
if (
|
|
575
|
+
store.schemaVersion !== WORKSPACE_TRUST_SCHEMA_VERSION ||
|
|
576
|
+
store.schema !== WORKSPACE_TRUST_STORE_SCHEMA ||
|
|
577
|
+
!store.records ||
|
|
578
|
+
typeof store.records !== "object" ||
|
|
579
|
+
Array.isArray(store.records)
|
|
580
|
+
) {
|
|
581
|
+
throw trustError(
|
|
582
|
+
"CC_WORKSPACE_TRUST_STORE_INVALID",
|
|
583
|
+
"workspace trust store has an unsupported schema",
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
return store.records;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function validRecord(record) {
|
|
590
|
+
return Boolean(
|
|
591
|
+
record &&
|
|
592
|
+
typeof record === "object" &&
|
|
593
|
+
!Array.isArray(record) &&
|
|
594
|
+
record.schemaVersion === WORKSPACE_TRUST_SCHEMA_VERSION &&
|
|
595
|
+
/^[a-f0-9]{64}$/u.test(record.workspaceId || "") &&
|
|
596
|
+
/^[a-f0-9]{64}$/u.test(record.repositoryId || "") &&
|
|
597
|
+
SOURCE_RE.test(record.source || "") &&
|
|
598
|
+
/^[a-f0-9]{64}$/u.test(record.subject || "") &&
|
|
599
|
+
/^[a-f0-9]{64}$/u.test(record.evidenceFingerprint || "") &&
|
|
600
|
+
record.decision === WORKSPACE_TRUST_DECISION.ALLOW &&
|
|
601
|
+
CONSENT_RE.test(record.consent || "") &&
|
|
602
|
+
typeof record.recordedAt === "string",
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function decisionForRecordState(
|
|
607
|
+
identity,
|
|
608
|
+
source,
|
|
609
|
+
subject,
|
|
610
|
+
fingerprint,
|
|
611
|
+
record,
|
|
612
|
+
records,
|
|
613
|
+
) {
|
|
614
|
+
const subjectHash = subjectDigest(subject);
|
|
615
|
+
const exact =
|
|
616
|
+
validRecord(record) &&
|
|
617
|
+
record.workspaceId === identity.workspaceId &&
|
|
618
|
+
record.repositoryId === identity.repositoryId &&
|
|
619
|
+
record.source === source &&
|
|
620
|
+
record.subject === subjectHash &&
|
|
621
|
+
record.evidenceFingerprint === fingerprint;
|
|
622
|
+
if (exact) return "trusted";
|
|
623
|
+
const seenPrior = Object.values(records).some(
|
|
624
|
+
(value) =>
|
|
625
|
+
validRecord(value) &&
|
|
626
|
+
value.source === source &&
|
|
627
|
+
value.subject === subjectHash,
|
|
628
|
+
);
|
|
629
|
+
return seenPrior || record ? "changed" : "first-use";
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function decisionForStatus(status) {
|
|
633
|
+
if (status === "trusted") return WORKSPACE_TRUST_DECISION.ALLOW;
|
|
634
|
+
if (status === "first-use") return WORKSPACE_TRUST_DECISION.ASK;
|
|
635
|
+
return WORKSPACE_TRUST_DECISION.DENY;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function resultForStatus({ identity, source, subject, fingerprint, status }) {
|
|
639
|
+
const decision = evaluateWorkspaceTrustDecision({
|
|
640
|
+
identity,
|
|
641
|
+
evidence: [
|
|
642
|
+
{
|
|
643
|
+
source,
|
|
644
|
+
consent: status === "trusted" ? "explicit" : "missing",
|
|
645
|
+
fingerprint,
|
|
646
|
+
decision: decisionForStatus(status),
|
|
647
|
+
},
|
|
648
|
+
],
|
|
649
|
+
});
|
|
650
|
+
return Object.freeze({
|
|
651
|
+
status,
|
|
652
|
+
identity,
|
|
653
|
+
decision: decision.decision,
|
|
654
|
+
audit: projectWorkspaceTrustAudit(decision),
|
|
655
|
+
subject: subjectDigest(subject),
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** Check canonical, source-specific consent stored in the shared trust ledger. */
|
|
660
|
+
export function checkRecordedWorkspaceTrust(options = {}) {
|
|
661
|
+
const identity = options.identity
|
|
662
|
+
? validateIdentity(options.identity)
|
|
663
|
+
: resolveCanonicalWorkspaceRepoIdentity(options.workspaceRoot, options);
|
|
664
|
+
const source = normalizeSource(options.source);
|
|
665
|
+
const subject = ownString(options.subject, "trust subject");
|
|
666
|
+
const fingerprint = fingerprintDigest(options.evidenceFingerprint);
|
|
667
|
+
if (source === "invalid") {
|
|
668
|
+
return resultForStatus({
|
|
669
|
+
identity,
|
|
670
|
+
source,
|
|
671
|
+
subject,
|
|
672
|
+
fingerprint,
|
|
673
|
+
status: "changed",
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
const records = trustedStoreRecords(
|
|
677
|
+
readSecurityStore(workspaceTrustStorePath(options), "workspace trust"),
|
|
678
|
+
);
|
|
679
|
+
const key = recordKey(identity, source, subject);
|
|
680
|
+
return resultForStatus({
|
|
681
|
+
identity,
|
|
682
|
+
source,
|
|
683
|
+
subject,
|
|
684
|
+
fingerprint,
|
|
685
|
+
status: decisionForRecordState(
|
|
686
|
+
identity,
|
|
687
|
+
source,
|
|
688
|
+
subject,
|
|
689
|
+
fingerprint,
|
|
690
|
+
records[key],
|
|
691
|
+
records,
|
|
692
|
+
),
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** Record explicit consent only after the caller has retained its own evidence. */
|
|
697
|
+
export function recordWorkspaceTrustConsent(options = {}) {
|
|
698
|
+
const identity = options.identity
|
|
699
|
+
? validateIdentity(options.identity)
|
|
700
|
+
: resolveCanonicalWorkspaceRepoIdentity(options.workspaceRoot, options);
|
|
701
|
+
const source = normalizeSource(options.source);
|
|
702
|
+
const subject = ownString(options.subject, "trust subject");
|
|
703
|
+
const consent = normalizeConsent(options.consent || "explicit");
|
|
704
|
+
const fingerprint = fingerprintDigest(options.evidenceFingerprint);
|
|
705
|
+
if (source === "invalid" || consent === "unknown") {
|
|
706
|
+
throw trustError(
|
|
707
|
+
"CC_WORKSPACE_TRUST_INVALID_INPUT",
|
|
708
|
+
"workspace trust source or consent is invalid",
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
const target = workspaceTrustStorePath(options);
|
|
712
|
+
const key = recordKey(identity, source, subject);
|
|
713
|
+
mutateSecurityStore(
|
|
714
|
+
target,
|
|
715
|
+
"workspace trust",
|
|
716
|
+
(store) => {
|
|
717
|
+
const records = trustedStoreRecords(store);
|
|
718
|
+
if (Object.keys(store).length === 0) {
|
|
719
|
+
store.schemaVersion = WORKSPACE_TRUST_SCHEMA_VERSION;
|
|
720
|
+
store.schema = WORKSPACE_TRUST_STORE_SCHEMA;
|
|
721
|
+
store.records = {};
|
|
722
|
+
}
|
|
723
|
+
// Validate the pre-existing shape before replacing one record. This
|
|
724
|
+
// prevents a malformed/corrupt ledger from being silently repaired into
|
|
725
|
+
// a new authorization grant.
|
|
726
|
+
for (const value of Object.values(records)) {
|
|
727
|
+
if (!validRecord(value)) {
|
|
728
|
+
throw trustError(
|
|
729
|
+
"CC_WORKSPACE_TRUST_STORE_INVALID",
|
|
730
|
+
"workspace trust store contains an invalid record",
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
store.records[key] = {
|
|
735
|
+
schemaVersion: WORKSPACE_TRUST_SCHEMA_VERSION,
|
|
736
|
+
workspaceId: identity.workspaceId,
|
|
737
|
+
repositoryId: identity.repositoryId,
|
|
738
|
+
source,
|
|
739
|
+
subject: subjectDigest(subject),
|
|
740
|
+
evidenceFingerprint: fingerprint,
|
|
741
|
+
decision: WORKSPACE_TRUST_DECISION.ALLOW,
|
|
742
|
+
consent,
|
|
743
|
+
recordedAt: String(options.now || _deps.now()),
|
|
744
|
+
};
|
|
745
|
+
return true;
|
|
746
|
+
},
|
|
747
|
+
{ lock: _deps.withFileLock },
|
|
748
|
+
);
|
|
749
|
+
return resultForStatus({
|
|
750
|
+
identity,
|
|
751
|
+
source,
|
|
752
|
+
subject,
|
|
753
|
+
fingerprint,
|
|
754
|
+
status: "trusted",
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/** Revoke one canonical consent record. A missing record is a safe no-op. */
|
|
759
|
+
export function revokeRecordedWorkspaceTrust(options = {}) {
|
|
760
|
+
const identity = options.identity
|
|
761
|
+
? validateIdentity(options.identity)
|
|
762
|
+
: resolveCanonicalWorkspaceRepoIdentity(options.workspaceRoot, options);
|
|
763
|
+
const source = normalizeSource(options.source);
|
|
764
|
+
const subject = ownString(options.subject, "trust subject");
|
|
765
|
+
if (source === "invalid") {
|
|
766
|
+
throw trustError(
|
|
767
|
+
"CC_WORKSPACE_TRUST_INVALID_INPUT",
|
|
768
|
+
"workspace trust source is invalid",
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
const target = workspaceTrustStorePath(options);
|
|
772
|
+
const key = recordKey(identity, source, subject);
|
|
773
|
+
return mutateSecurityStore(
|
|
774
|
+
target,
|
|
775
|
+
"workspace trust",
|
|
776
|
+
(store) => {
|
|
777
|
+
const records = trustedStoreRecords(store);
|
|
778
|
+
if (Object.keys(store).length === 0) return false;
|
|
779
|
+
for (const value of Object.values(records)) {
|
|
780
|
+
if (!validRecord(value)) {
|
|
781
|
+
throw trustError(
|
|
782
|
+
"CC_WORKSPACE_TRUST_STORE_INVALID",
|
|
783
|
+
"workspace trust store contains an invalid record",
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
const existed = Object.prototype.hasOwnProperty.call(store.records, key);
|
|
788
|
+
delete store.records[key];
|
|
789
|
+
return existed;
|
|
790
|
+
},
|
|
791
|
+
{ lock: _deps.withFileLock },
|
|
792
|
+
);
|
|
793
|
+
}
|