chainlesschain 0.162.188 → 0.162.193
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/README.md +61 -11
- package/bin/chainlesschain.js +7 -52
- package/completions/_cc +209 -0
- package/completions/cc.bash +18 -0
- package/completions/cc.fish +13 -0
- package/completions/cc.ps1 +226 -0
- package/package.json +10 -2
- package/src/assets/web-panel/assets/{AIOps-Brr1LVcE.js → AIOps-DBLkaTje.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DQU74DKG.js → ActionButton-BAy-Qebo.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CYp9178y.js → Analytics-Dfb1Mlau.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DW4SCYsW.js → AppLayout-BbFkN8m0.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CYlD_ed1.js → Artifacts-kM4-cEEH.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CWl1rZOi.js → Audit-BSQfYWf3.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BOR-LPsa.js → BackgroundAgents-CWDbI_fG.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DLpG7j1X.js → Backup-BxLPbq_Q.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-B35K9wdr.js → BaseInput-DuIbdrp6.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CwUJb9gB.js → Chat-NEmjGahU.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-1v6aMS1m.js → ChatBubbleRenderer-DfJEo-5_.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-pi3-tPNW.js → Checkbox-CaoeG3Ns.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-FyNF35eL.js → Codegen-BFf_g3xY.js} +1 -1
- package/src/assets/web-panel/assets/{Col-tGt_JDEr.js → Col-CNOd8mdQ.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CC7LonQX.js → Community-BhpDqz-i.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BGXrHHjE.js → Compact-CQ42T7Z6.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DSW0TBZ8.js → Compliance-DI5PcLMN.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Bhk7rbvE.js → Cowork-wYYZe0wR.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DxyiQ2NZ.js → Cron-Dlq03DD_.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-B2ZkxHok.js → Crosschain-BzqS8PP3.js} +1 -1
- package/src/assets/web-panel/assets/{DID-yOJV99eJ.js → DID-It55Yem7.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CKsttz7D.js → Dashboard-9HkH6oxn.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CVFldXSL.js → Dropdown-00e2YWRB.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Bufv638J.js → EmailListRenderer-By-ugRDL.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-4ncjoIjL.js → FamilyGuardDashboard-C396yu5m.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DsknEDoX.js → Federation-BME5sbZY.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Cqe5jWXA.js → FormItemContext-B4TEZCr-.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-NAJaFKxc.js → GenericCardRenderer-_9CX9ieY.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CszKQpnl.js → Git-BrS6WVSr.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BXGodXi6.js → Governance-BMdCWnKF.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CkoYuG2c.js → Inference-Bcp3vzcA.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DFlEr_T0.js → KnowledgeGraph-DlLuqGY5.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CVaENddA.js → Logs-D-W_6x09.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-DElGPxCZ.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-CDlLT0fw.js → Marketplace-Dzn2guvI.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DdwOyszN.js → McpTools-uPict-KL.js} +6 -6
- package/src/assets/web-panel/assets/{Memory-DKQnkKuG.js → Memory-BCw43kOb.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DGseqlzJ.js → MobileBridge-DmV8Wv7F.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-COBbEOlj.js → MobileProjects-C-HaCmSM.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-O61Vwyli.js → Mtc-C6jxJ34Y.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-Dw2SyasS.js → MtcAudit-Za8ExwhQ.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-hSu_l8kf.js → Multisig-KKDbuEgb.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CX1Epurr.js → NLProgramming-Dj97zbCk.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-R9xX8o8_.js → Notes-DBXvMraU.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-jtSNLCuK.js → NotificationSettings-DOxITLYl.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-7DVjZwMM.js +1 -0
- package/src/assets/web-panel/assets/{Organization-Pu2WO-U3.js → Organization-D1i73mjb.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BjnqUyFp.js → Overflow-kW6Q3rvg.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-jjkvz44c.js → P2P-C-v_8p2v.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-tUbbY64W.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-D4yDpivd.js → Permissions-DkHxEZ58.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-0qsL6iH1.js → PersonalDataHub-DWuMPGrK.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-Cx7Ybrk2.js → Pipeline-30IYXWWm.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-PbfR_M_V.js → Privacy-DZNQ4RrH.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CyV6Gsub.js → ProjectInit-De2MCElf.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-C7KZ2Wdk.js → ProjectSettings-5uHk9wsC.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-DQDjO-yK.js → Projects-XjDfrFLb.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DyFVBBMX.js → Providers-Bgwj6gcE.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CzB1BC9_.js → QrScannerModal-gKql9umv.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-Bk636mC1.js → QuickAsk-BWvmwkh7.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DoWxHIT3.js → Recommend-Uok4hZPd.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-oOVIv9wq.js → RemoteSession-DLJzTk9a.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-BucoReGG.js → Reputation-ZVwIPMSO.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DzmOdJU7.js → Row-CN04HxWR.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-C1tK7KGU.js → RssFeed-BdSrEG4g.js} +3 -3
- package/src/assets/web-panel/assets/{Search-CSnInOqk.js → Search-5PwVd0IR.js} +1 -1
- package/src/assets/web-panel/assets/{Security-dlejr15l.js → Security-DdBo4zjN.js} +3 -3
- package/src/assets/web-panel/assets/{Services-BioPZuNh.js → Services-D3letU7M.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DWMibwpC.js → Skeleton--Jb83f1v.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-BAAnx1B2.js → Skills-Bg-CulHX.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-jWNp4hNO.js → Sla-7QOa7S-X.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-y5kqKha-.js → SpeechSettings-DQwppjRw.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-B9GNsc3e.js → SyncSettings-dNPQGL-s.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BDBYKRen.js → Tasks-Ggs-Z48q.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DRiE5eaN.js → Templates-78TYHgRO.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BGlAvExF.js → Tenant-xHwwv9vO.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-B8uzrbNY.js → Terminal-BJXSJfm4.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-Df48iveY.js → TimelineRenderer-fLbjRhJw.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CcmHndN6.js → Tokens-oMSBSBmB.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DPkWxmQ2.js → Trigger-CfnY_wX_.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DTb5a5AV.js → Trust-AdyTgYKO.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CBmg5vch.js → UkeySign-C5K_81dS.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B0mZrz4O.js → VideoEditing-vcGVDqki.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Bxs7_mpd.js → Wallet-hhOc3hlY.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-C8x3cMxU.js → WebAuthn-CVloC11s.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-DPrNznrP.js → WorkflowEditor-CYeE32R5.js} +1 -1
- package/src/assets/web-panel/assets/{chat-D3bYWu7S.js → chat-CNROR5bN.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DY-PwMu4.js → colors-Cjx-cnwU.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-LDk10Kw6.js → compact-item-DOIfelvD.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-C-2MZnFq.js → createContext-DfA7UZ8u.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-ChcCBGs7.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BQIBjcnB.js → hasIn-BMSCzcfM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQVb2dWp.js → index-5-HXgOX5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BdXnYMCZ.js → index-AWg-0M1S.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cx6D_dx2.js → index-B4iYzpkL.js} +1 -1
- package/src/assets/web-panel/assets/{index-_HRO_cIu.js → index-B5Udeb_6.js} +1 -1
- package/src/assets/web-panel/assets/{index-DlRYqt3S.js → index-B7gf77CJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BrnRO9yE.js → index-BEMwBBHk.js} +1 -1
- package/src/assets/web-panel/assets/{index-VCh5zNJo.js → index-BM1y_9hM.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRI3ngNg.js → index-BUonmKSE.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3GNvExG.js → index-BbQ2eQ8H.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSvo_bp0.js → index-BemJ2u_E.js} +1 -1
- package/src/assets/web-panel/assets/{index-L6qixyLc.js → index-BolaOQxx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BI4KgcYC.js → index-BxOk9vkG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CY4KnaO9.js → index-By61ojUx.js} +1 -1
- package/src/assets/web-panel/assets/{index-L3smcoQX.js → index-C2K9SNVX.js} +1 -1
- package/src/assets/web-panel/assets/{index-BX2b9Orc.js → index-C3L5gdiD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cz4b6Z_7.js → index-C7xn0Nub.js} +1 -1
- package/src/assets/web-panel/assets/{index-COG61sRv.js → index-CAnvhwg6.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7FC8cP2.js → index-CEKMcOyU.js} +1 -1
- package/src/assets/web-panel/assets/{index-BN-xdSWZ.js → index-CNC5onm4.js} +1 -1
- package/src/assets/web-panel/assets/{index-DY2FIrmz.js → index-CVL27kXC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiTvBAr-.js → index-CVm1_i3t.js} +1 -1
- package/src/assets/web-panel/assets/{index-DhRt9cYJ.js → index-CdYBye8B.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5pgtZnY.js → index-ClNMrqEY.js} +1 -1
- package/src/assets/web-panel/assets/{index-7xKu7Fns.js → index-Cs6l7uRu.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4K1weKc.js → index-CshQbilz.js} +1 -1
- package/src/assets/web-panel/assets/{index-k7C_oDCO.js → index-Cyweh0Hy.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3Oq-DLL.js → index-DBpWXFed.js} +3 -3
- package/src/assets/web-panel/assets/{index-DYyLJPlF.js → index-DHtpHZAQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dh-tkpKQ.js → index-DLmn0BoK.js} +1 -1
- package/src/assets/web-panel/assets/{index-8JC9BJa1.js → index-DPAWBrMf.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dd2Km97U.js → index-DjEsxO5h.js} +1 -1
- package/src/assets/web-panel/assets/{index-KFZO_lcx.js → index-FBc_CJBd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTIZpPtG.js → index-Icdoqmzt.js} +1 -1
- package/src/assets/web-panel/assets/index-Q2IajL54.js +1 -0
- package/src/assets/web-panel/assets/index-bRkHgOMc.js +1 -0
- package/src/assets/web-panel/assets/{index-lmm9p-jm.js → index-dCd8cSUq.js} +1 -1
- package/src/assets/web-panel/assets/{index-HasPt4-h.js → index-m2vG9gG_.js} +1 -1
- package/src/assets/web-panel/assets/{index-CaslYch6.js → index-mTpHoEzr.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbR6ROOa.js → index-ti_lnTWl.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-zQPSHf2C.js → initDefaultProps-Bu8k7H1Y.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CKCStGVk.js → motion-s3iVK41k.js} +1 -1
- package/src/assets/web-panel/assets/{move-BufVDGYF.js → move-B6B-E5Jz.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DcaFxy5D.js → mtc-parser-BiYgTidA.js} +1 -1
- package/src/assets/web-panel/assets/{omit-EScpkGdo.js → omit-pU1RdUDA.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-72hG_qfT.js → pickAttrs-Cdoq_SeM.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CMFWiH20.js → placementArrow-DIuvAn2K.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DKQHS41p.js → responsiveObserve-8w16YR2D.js} +1 -1
- package/src/assets/web-panel/assets/{slide-QVAFHTIQ.js → slide-VZ01cZe3.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DIz38e2G.js → statusUtils-lh8YMN8Z.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-zK5cyLby.js → styleChecker-CmDyreec.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DhZngk62.js → useFlexGapSupport-hiJWMU5y.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BvV2cvkg.js → useFs-Thnu-Iaj.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CRmxb9l3.js → usePersonalDataHub-LBdOxjwY.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CgQIbobP.js → vnode-Cg6lFY_X.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-D4XrrO0R.js → zoom-DVfsR1YO.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/cli-help.js +181 -0
- package/src/command-help-index.json +181 -0
- package/src/command-manifest.json +1107 -175
- package/src/command-surface-policy.js +197 -0
- package/src/commands/agent.js +98 -33
- package/src/commands/artifacts.js +400 -2
- package/src/commands/background-session.js +41 -3
- package/src/commands/checkpoint-managed.js +1059 -0
- package/src/commands/checkpoint-restore-recovery.js +1476 -0
- package/src/commands/checkpoint.js +950 -22
- package/src/commands/compact.js +101 -25
- package/src/commands/config.js +506 -20
- package/src/commands/doctor.js +4 -1
- package/src/commands/hub.js +1 -1
- package/src/commands/mcp.js +688 -24
- package/src/commands/pack.js +16 -2
- package/src/commands/plugin.js +20 -12
- package/src/commands/session-mcp-recovery.js +218 -0
- package/src/commands/session.js +470 -110
- package/src/commands/setup.js +36 -10
- package/src/commands/skill.js +181 -110
- package/src/commands/status.js +21 -5
- package/src/commands/team-distributed.js +3324 -0
- package/src/commands/team.js +1130 -74
- package/src/commands/update.js +68 -6
- package/src/constants.js +11 -0
- package/src/data/changelog.json +26 -0
- package/src/gateways/ws/action-protocol.js +10 -0
- package/src/gateways/ws/session-protocol.js +555 -27
- package/src/gateways/ws/ws-agent-handler.js +541 -14
- package/src/gateways/ws/ws-session-gateway.js +129 -22
- package/src/gateways/ws/ws-session-state.js +431 -0
- package/src/harness/background-task-manager.js +233 -36
- package/src/harness/jsonl-session-store.js +2551 -167
- package/src/harness/mcp-client.js +802 -61
- package/src/harness/prompt-compressor.js +227 -27
- package/src/harness/session-index.js +89 -38
- package/src/harness/session-list-index.js +254 -0
- package/src/harness/structured-handoff.js +410 -0
- package/src/lazy-dispatch.js +747 -84
- package/src/lib/__tests__/logger.test.js +50 -2
- package/src/lib/advisor-runtime.js +1047 -0
- package/src/lib/agent-core.js +2 -0
- package/src/lib/agent-sandbox.js +120 -26
- package/src/lib/agent-session-export.js +20 -0
- package/src/lib/agent-team/task-lease.js +271 -3
- package/src/lib/agent-team/team-adjudication.js +1501 -0
- package/src/lib/agent-team/team-budget.js +4 -1
- package/src/lib/agent-team/team-control-store.js +582 -0
- package/src/lib/agent-team/team-distributed-queue.js +5246 -0
- package/src/lib/agent-team/team-process-checkpoint.js +211 -0
- package/src/lib/agent-team/team-runner.js +543 -24
- package/src/lib/agent-team/team-worktree.js +1403 -95
- package/src/lib/artifact-store.js +131 -8
- package/src/lib/background-agent-supervisor.js +185 -13
- package/src/lib/background-interaction-journal.js +37 -9
- package/src/lib/background-launch-profile.js +1058 -0
- package/src/lib/checkpoint-restore-already-completed-controller.js +902 -0
- package/src/lib/checkpoint-restore-orchestrator.js +987 -0
- package/src/lib/checkpoint-restore-partial-rollback-controller.js +1953 -0
- package/src/lib/checkpoint-restore-recovery-controller.js +689 -0
- package/src/lib/checkpoint-restore-recovery.js +884 -0
- package/src/lib/checkpoint-restore-retention.js +126 -0
- package/src/lib/checkpoint-restore-saga.js +5079 -0
- package/src/lib/checkpoint-restore-session-recovery.js +1178 -0
- package/src/lib/checkpoint-restore-workspace-target-verifier.js +270 -0
- package/src/lib/checkpoint-store.js +1688 -97
- package/src/lib/checkpoint-timeline-authority.js +522 -0
- package/src/lib/checkpoint-timeline.js +642 -0
- package/src/lib/config-keys.js +18 -119
- package/src/lib/config-manager.js +446 -27
- package/src/lib/config-redaction.js +57 -0
- package/src/lib/config-schema.js +853 -0
- package/src/lib/cowork-mcp-tools.js +5 -0
- package/src/lib/cowork-task-runner.js +458 -63
- package/src/lib/delivery-coordinator.js +1234 -0
- package/src/lib/delivery-evidence.js +450 -0
- package/src/lib/doctor-checkup.js +199 -14
- package/src/lib/executable-path.js +34 -0
- package/src/lib/fatal-handler.js +16 -0
- package/src/lib/file-checkpoint.js +4909 -186
- package/src/lib/file-lines.js +125 -0
- package/src/lib/headless-config-command.js +16 -5
- package/src/lib/hook-runner.cjs +139 -38
- package/src/lib/hooks-v2-workspace-context.js +10 -0
- package/src/lib/impacted-gate-selector.js +424 -0
- package/src/lib/interaction-adapter.js +62 -0
- package/src/lib/jsonl-session-store.js +1 -35
- package/src/lib/logger.js +66 -14
- package/src/lib/managed-checkpoint-render.js +60 -0
- package/src/lib/managed-tool-checkpoint.js +194 -0
- package/src/lib/mcp-call-ledger-store.js +1069 -0
- package/src/lib/mcp-call-ledger.js +747 -0
- package/src/lib/mcp-client.js +5 -0
- package/src/lib/mcp-conflict-scheduler.js +468 -0
- package/src/lib/mcp-effect-contract.js +102 -0
- package/src/lib/mcp-headers-helper-trust.js +145 -0
- package/src/lib/mcp-headers-helper.js +616 -0
- package/src/lib/mcp-host-recovery-runtime.js +131 -0
- package/src/lib/mcp-ledger-recovery-admission.js +1065 -0
- package/src/lib/mcp-recovery-adjudication.js +443 -0
- package/src/lib/output-context.js +86 -0
- package/src/lib/packer/file-identity.js +170 -0
- package/src/lib/packer/index.js +3 -0
- package/src/lib/packer/native-update-state.js +277 -0
- package/src/lib/packer/pack-update-applier.js +1983 -136
- package/src/lib/packer/pack-update-checker.js +34 -0
- package/src/lib/packer/pack-update-downloader.js +875 -86
- package/src/lib/packer/pack-update-signature.js +86 -0
- package/src/lib/packer/pkg-config-generator.js +6 -0
- package/src/lib/paths.js +65 -10
- package/src/lib/plan-mode.js +831 -14
- package/src/lib/plan-persistence.js +540 -0
- package/src/lib/plugin-runtime/hooks.js +227 -69
- package/src/lib/plugin-runtime/plugin-options.js +189 -44
- package/src/lib/pr-automation-policy.js +146 -8
- package/src/lib/process-execution-broker/credential-agent.js +7 -0
- package/src/lib/process-execution-broker/index.js +491 -13
- package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +28 -1
- package/src/lib/process-execution-broker/linux-generic-bwrap.js +28 -4
- package/src/lib/process-execution-broker/platform-sandbox.js +92 -70
- 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 +284 -19
- package/src/lib/process-execution-broker/workspace-transaction.js +4459 -0
- package/src/lib/project-instructions.js +165 -29
- package/src/lib/project-mcp-trust.js +67 -0
- package/src/lib/public-ide-capability-manifest.js +272 -0
- package/src/lib/public-ide-capability-validation.js +351 -0
- package/src/lib/secret-store.js +267 -43
- package/src/lib/secure-file-identity.js +433 -0
- package/src/lib/secure-fs.js +1089 -0
- package/src/lib/security-audit.js +58 -0
- package/src/lib/service-manager.js +34 -13
- package/src/lib/session-budget-runtime.js +1452 -0
- package/src/lib/session-host-snapshot.js +388 -0
- package/src/lib/session-message-provenance.js +429 -0
- package/src/lib/session-projection.js +804 -0
- package/src/lib/session-resource-budget.js +1495 -0
- package/src/lib/settings-hooks.cjs +138 -22
- package/src/lib/settings-loader.cjs +25 -2
- package/src/lib/side-effect-ledger-store.js +45 -23
- package/src/lib/skill-execution-identity.js +217 -0
- package/src/lib/skill-loader.js +299 -18
- package/src/lib/sub-agent-context.js +236 -28
- package/src/lib/sub-agent-registry.js +5 -0
- package/src/lib/subagent-contract.js +94 -0
- package/src/lib/todo-manager.js +185 -18
- package/src/lib/todo-persistence.js +417 -0
- package/src/lib/turn-binding-store.js +41 -23
- package/src/lib/turn-binding.js +160 -2
- package/src/lib/with-file-lock.js +165 -24
- package/src/lib/ws-chat-handler.js +21 -0
- package/src/program-base.js +18 -0
- package/src/repl/advisor-command.js +103 -0
- package/src/repl/agent-repl.js +2175 -146
- package/src/repl/btw-command.js +93 -10
- package/src/repl/clipboard-image.js +119 -0
- package/src/repl/config-summary.js +64 -108
- package/src/repl/prompt-editor.js +233 -0
- package/src/repl/prompt-interactions.js +344 -0
- package/src/repl/prompt-stash.js +354 -0
- package/src/repl/prompt-suggestions.js +312 -0
- package/src/repl/repl-keybindings.js +227 -0
- package/src/repl/session-recap.js +217 -0
- package/src/repl/slash-command-registry.js +22 -0
- package/src/repl/terminal-layout.js +88 -0
- package/src/runtime/agent-core.js +1630 -447
- package/src/runtime/coding-agent-contract-shared.cjs +13 -1
- package/src/runtime/coding-agent-managed-tool-policy.cjs +7 -2
- package/src/runtime/diagnostics.js +32 -13
- package/src/runtime/headless-runner.js +310 -118
- package/src/runtime/headless-stream.js +356 -95
- package/src/runtime/mcp-config.js +148 -14
- package/src/runtime/policies/agent-policy.js +14 -0
- package/src/runtime/status-diagnostics-lite.js +137 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-7iWGwZ-r.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BU2Yqq5H.js +0 -1
- package/src/assets/web-panel/assets/PdhVaultBrowser-69ca_WeR.js +0 -7
- package/src/assets/web-panel/assets/devWarning-B4dXIJqD.js +0 -1
- package/src/assets/web-panel/assets/index-C-C7DaG6.js +0 -1
- package/src/assets/web-panel/assets/index-DkfWzWtd.js +0 -1
|
@@ -0,0 +1,1089 @@
|
|
|
1
|
+
/** Owner-only filesystem helpers for configuration, credentials and sessions. */
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import {
|
|
6
|
+
isAbsolute as isNativeAbsolute,
|
|
7
|
+
parse,
|
|
8
|
+
resolve,
|
|
9
|
+
win32,
|
|
10
|
+
posix,
|
|
11
|
+
} from "node:path";
|
|
12
|
+
|
|
13
|
+
export const PRIVATE_DIRECTORY_MODE = 0o700;
|
|
14
|
+
export const PRIVATE_FILE_MODE = 0o600;
|
|
15
|
+
|
|
16
|
+
function samePath(left, right, caseInsensitive = false) {
|
|
17
|
+
const trim = (value) => String(value).replace(/[\\/]+$/, "");
|
|
18
|
+
const a = trim(left);
|
|
19
|
+
const b = trim(right);
|
|
20
|
+
return caseInsensitive ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Guard every ACL/chmod repair entry point, not just paths.js callers. */
|
|
24
|
+
export function assertSafeOwnerOnlyPath(target, platform = process.platform) {
|
|
25
|
+
const value = String(target || "").trim();
|
|
26
|
+
if (!value) throw new Error("Owner-only storage path must not be empty");
|
|
27
|
+
const pathApi =
|
|
28
|
+
platform === "win32"
|
|
29
|
+
? win32
|
|
30
|
+
: platform === "darwin" || platform === "linux"
|
|
31
|
+
? posix
|
|
32
|
+
: { isAbsolute: isNativeAbsolute, parse, resolve };
|
|
33
|
+
const native = pathApi.resolve(value);
|
|
34
|
+
const nativeRoot = pathApi.parse(native).root;
|
|
35
|
+
const posixPath = posix.isAbsolute(value) ? posix.normalize(value) : null;
|
|
36
|
+
const windowsPath = win32.isAbsolute(value) ? win32.normalize(value) : null;
|
|
37
|
+
const isAbsolute = pathApi.isAbsolute(value);
|
|
38
|
+
const windowsLiteral = value.replaceAll("/", "\\");
|
|
39
|
+
const normalizedWindowsLiteral = String(windowsPath || windowsLiteral);
|
|
40
|
+
const extendedWindowsRoot =
|
|
41
|
+
/^\\\\\?\\[A-Za-z]:\\?$/.test(normalizedWindowsLiteral) ||
|
|
42
|
+
/^\\\\\?\\UNC\\[^\\]+\\[^\\]+\\?$/.test(normalizedWindowsLiteral);
|
|
43
|
+
const normalizedWindowsLower = normalizedWindowsLiteral.toLowerCase();
|
|
44
|
+
const forbiddenWindowsDevice =
|
|
45
|
+
normalizedWindowsLower.startsWith("\\\\?\\") ||
|
|
46
|
+
normalizedWindowsLower.startsWith("\\\\.\\");
|
|
47
|
+
const isRoot =
|
|
48
|
+
samePath(native, nativeRoot, platform === "win32") ||
|
|
49
|
+
(posixPath !== null && samePath(posixPath, posix.parse(posixPath).root)) ||
|
|
50
|
+
(windowsPath !== null &&
|
|
51
|
+
samePath(windowsPath, win32.parse(windowsPath).root, true)) ||
|
|
52
|
+
/^[A-Za-z]:$/.test(value) ||
|
|
53
|
+
extendedWindowsRoot ||
|
|
54
|
+
forbiddenWindowsDevice;
|
|
55
|
+
if (isRoot) {
|
|
56
|
+
const error = new Error(
|
|
57
|
+
`Refusing owner-only permission changes on a filesystem root: ${value}`,
|
|
58
|
+
);
|
|
59
|
+
error.code = "CONFIG_HOME_UNSAFE";
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
const caseInsensitive = ["win32", "darwin"].includes(platform);
|
|
63
|
+
const protectedLocation = samePath(
|
|
64
|
+
native,
|
|
65
|
+
pathApi.resolve(homedir()),
|
|
66
|
+
caseInsensitive,
|
|
67
|
+
);
|
|
68
|
+
if (protectedLocation || !isAbsolute) {
|
|
69
|
+
const error = new Error(
|
|
70
|
+
`Refusing owner-only permission changes on a broad protected directory: ${value}`,
|
|
71
|
+
);
|
|
72
|
+
error.code = "CONFIG_HOME_UNSAFE";
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
return target;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const TRUSTED_DARWIN_SYSTEM_ALIASES = new Map([["/var", "/private/var"]]);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* macOS exposes its real temporary-directory tree through the root-owned
|
|
82
|
+
* `/var -> private/var` compatibility alias. Rejecting that immutable system
|
|
83
|
+
* alias makes every `os.tmpdir()`-backed session fail, while accepting generic
|
|
84
|
+
* links would re-open the chmod/DACL traversal attack this guard prevents.
|
|
85
|
+
*
|
|
86
|
+
* Keep the exception deliberately narrow: exact alias, exact canonical target,
|
|
87
|
+
* root ownership on both entries, and a non-writable canonical directory.
|
|
88
|
+
*/
|
|
89
|
+
function isTrustedDarwinSystemAlias(current, entry, deps, platform) {
|
|
90
|
+
if (platform !== "darwin") return false;
|
|
91
|
+
const expected = TRUSTED_DARWIN_SYSTEM_ALIASES.get(current);
|
|
92
|
+
if (!expected || Number(entry?.uid) !== 0) return false;
|
|
93
|
+
try {
|
|
94
|
+
const canonical = deps.realpathSync(current);
|
|
95
|
+
if (!samePath(canonical, expected)) return false;
|
|
96
|
+
const target = deps.lstatSync(canonical);
|
|
97
|
+
const targetMode = Number(target.mode);
|
|
98
|
+
return (
|
|
99
|
+
!target.isSymbolicLink() &&
|
|
100
|
+
target.isDirectory() &&
|
|
101
|
+
Number(target.uid) === 0 &&
|
|
102
|
+
Number.isInteger(targetMode) &&
|
|
103
|
+
(targetMode & 0o022) === 0
|
|
104
|
+
);
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function readWindowsDirectoryEntry(target, deps) {
|
|
111
|
+
if (typeof deps.readdirSync !== "function") return null;
|
|
112
|
+
const parent = win32.dirname(target);
|
|
113
|
+
if (!parent || samePath(parent, target, true)) return null;
|
|
114
|
+
let entries;
|
|
115
|
+
try {
|
|
116
|
+
entries = deps.readdirSync(parent, { withFileTypes: true });
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (!Array.isArray(entries)) return null;
|
|
121
|
+
const name = win32.basename(target);
|
|
122
|
+
const matches = entries.filter(
|
|
123
|
+
(candidate) => String(candidate?.name || "") === name,
|
|
124
|
+
);
|
|
125
|
+
if (matches.length !== 1) return null;
|
|
126
|
+
const [entry] = matches;
|
|
127
|
+
if (
|
|
128
|
+
typeof entry.isSymbolicLink !== "function" ||
|
|
129
|
+
typeof entry.isDirectory !== "function" ||
|
|
130
|
+
typeof entry.isFile !== "function"
|
|
131
|
+
) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const isSymbolicLink = Boolean(entry.isSymbolicLink());
|
|
136
|
+
const isDirectory = Boolean(entry.isDirectory());
|
|
137
|
+
const isFile = Boolean(entry.isFile());
|
|
138
|
+
if (!isSymbolicLink && isDirectory === isFile) return null;
|
|
139
|
+
return {
|
|
140
|
+
exists: true,
|
|
141
|
+
isDirectory,
|
|
142
|
+
isFile,
|
|
143
|
+
isSymbolicLink,
|
|
144
|
+
};
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function windowsUnreliablePathStatError(target, cause) {
|
|
151
|
+
const error = new Error(
|
|
152
|
+
`Windows path metadata could not reliably classify an existing path: ${target}`,
|
|
153
|
+
cause ? { cause } : undefined,
|
|
154
|
+
);
|
|
155
|
+
error.code = "WINDOWS_PATH_STAT_UNRELIABLE";
|
|
156
|
+
return error;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function assertNoLinkTraversal(
|
|
160
|
+
target,
|
|
161
|
+
deps = fs,
|
|
162
|
+
checkedPaths = null,
|
|
163
|
+
platform = process.platform,
|
|
164
|
+
) {
|
|
165
|
+
// Dependency-injected cross-platform tests must walk the target platform's
|
|
166
|
+
// path grammar, not the host running Vitest (for example `/var` on Windows).
|
|
167
|
+
const pathApi =
|
|
168
|
+
platform === "win32"
|
|
169
|
+
? win32
|
|
170
|
+
: platform === "darwin" || platform === "linux"
|
|
171
|
+
? posix
|
|
172
|
+
: { resolve, parse };
|
|
173
|
+
const absolute = pathApi.resolve(String(target));
|
|
174
|
+
const root = pathApi.parse(absolute).root;
|
|
175
|
+
const segments = absolute
|
|
176
|
+
.slice(root.length)
|
|
177
|
+
.split(/[\\/]+/)
|
|
178
|
+
.filter(Boolean);
|
|
179
|
+
let current = root;
|
|
180
|
+
for (const segment of segments) {
|
|
181
|
+
current = pathApi.resolve(current, segment);
|
|
182
|
+
const cacheKey =
|
|
183
|
+
process.platform === "win32" ? current.toLowerCase() : current;
|
|
184
|
+
if (checkedPaths?.has(cacheKey)) continue;
|
|
185
|
+
let entry;
|
|
186
|
+
try {
|
|
187
|
+
entry = deps.lstatSync(current);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (
|
|
190
|
+
platform === "win32" &&
|
|
191
|
+
["ENOENT", "EPERM", "EACCES"].includes(error?.code)
|
|
192
|
+
) {
|
|
193
|
+
const directoryEntry = readWindowsDirectoryEntry(current, deps);
|
|
194
|
+
if (directoryEntry) {
|
|
195
|
+
if (directoryEntry.isSymbolicLink) {
|
|
196
|
+
const unsafe = new Error(
|
|
197
|
+
`Refusing owner-only permission changes through a symbolic link or junction: ${current}`,
|
|
198
|
+
);
|
|
199
|
+
unsafe.code = "CONFIG_HOME_UNSAFE";
|
|
200
|
+
throw unsafe;
|
|
201
|
+
}
|
|
202
|
+
checkedPaths?.add(cacheKey);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
throw windowsUnreliablePathStatError(current, error);
|
|
206
|
+
}
|
|
207
|
+
if (error?.code === "ENOENT") break;
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
if (
|
|
211
|
+
entry.isSymbolicLink() &&
|
|
212
|
+
!isTrustedDarwinSystemAlias(current, entry, deps, platform)
|
|
213
|
+
) {
|
|
214
|
+
const error = new Error(
|
|
215
|
+
`Refusing owner-only permission changes through a symbolic link or junction: ${current}`,
|
|
216
|
+
);
|
|
217
|
+
error.code = "CONFIG_HOME_UNSAFE";
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
checkedPaths?.add(cacheKey);
|
|
221
|
+
}
|
|
222
|
+
return target;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const WINDOWS_ACL_SCRIPT = String.raw`
|
|
226
|
+
param([string]$target, [string]$operation)
|
|
227
|
+
$ErrorActionPreference = 'Stop'
|
|
228
|
+
$utf8 = [System.Text.UTF8Encoding]::new($false)
|
|
229
|
+
[Console]::OutputEncoding = $utf8
|
|
230
|
+
$sections =
|
|
231
|
+
[System.Security.AccessControl.AccessControlSections]::Access -bor
|
|
232
|
+
[System.Security.AccessControl.AccessControlSections]::Owner
|
|
233
|
+
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
|
234
|
+
$sid = $identity.User
|
|
235
|
+
$tokenOwner = $identity.Owner
|
|
236
|
+
|
|
237
|
+
function Assert-CcNoReparseTraversal([string]$path) {
|
|
238
|
+
$cursor = [System.IO.Path]::GetFullPath($path)
|
|
239
|
+
while (-not (Test-Path -LiteralPath $cursor)) {
|
|
240
|
+
$parent = [System.IO.Directory]::GetParent($cursor)
|
|
241
|
+
if ($null -eq $parent) { return }
|
|
242
|
+
$cursor = $parent.FullName
|
|
243
|
+
}
|
|
244
|
+
$entry = Get-Item -LiteralPath $cursor -Force
|
|
245
|
+
while ($null -ne $entry) {
|
|
246
|
+
if (($entry.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
247
|
+
throw "symbolic links and reparse points are not allowed: $($entry.FullName)"
|
|
248
|
+
}
|
|
249
|
+
$entry = $entry.Parent
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
Assert-CcNoReparseTraversal $target
|
|
254
|
+
$item = Get-Item -LiteralPath $target -Force
|
|
255
|
+
|
|
256
|
+
function Read-CcAcl([string]$path) {
|
|
257
|
+
if ([System.IO.Directory]::Exists($path)) {
|
|
258
|
+
return [System.Security.AccessControl.DirectorySecurity]::new($path, $sections)
|
|
259
|
+
}
|
|
260
|
+
return [System.Security.AccessControl.FileSecurity]::new($path, $sections)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function Write-CcOwnerOnlyAcl($item, [string]$path) {
|
|
264
|
+
$security = Read-CcAcl $path
|
|
265
|
+
$owner = $security.GetOwner([System.Security.Principal.SecurityIdentifier])
|
|
266
|
+
if ($owner.Value -ne $sid.Value) {
|
|
267
|
+
# Elevated/service tokens can create files whose owner is the token's
|
|
268
|
+
# default owner group (for example BUILTIN\Administrators on hosted CI),
|
|
269
|
+
# even though the token user created the object. Only that exact token-owned
|
|
270
|
+
# case may converge to the narrower user SID; every unrelated owner remains
|
|
271
|
+
# fail-closed.
|
|
272
|
+
if ($null -eq $tokenOwner -or $owner.Value -ne $tokenOwner.Value) {
|
|
273
|
+
throw "owner-only ACL repair refuses a path owned by another identity: $path"
|
|
274
|
+
}
|
|
275
|
+
$security.SetOwner($sid)
|
|
276
|
+
}
|
|
277
|
+
$security.SetAccessRuleProtection($true, $false)
|
|
278
|
+
$existingRules = @($security.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]))
|
|
279
|
+
foreach ($existingRule in $existingRules) {
|
|
280
|
+
$security.RemoveAccessRuleAll($existingRule)
|
|
281
|
+
}
|
|
282
|
+
if ($item.PSIsContainer) {
|
|
283
|
+
$inheritance =
|
|
284
|
+
[System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
|
|
285
|
+
[System.Security.AccessControl.InheritanceFlags]::ObjectInherit
|
|
286
|
+
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
|
|
287
|
+
$sid,
|
|
288
|
+
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
289
|
+
$inheritance,
|
|
290
|
+
[System.Security.AccessControl.PropagationFlags]::None,
|
|
291
|
+
[System.Security.AccessControl.AccessControlType]::Allow
|
|
292
|
+
)
|
|
293
|
+
$security.AddAccessRule($rule) | Out-Null
|
|
294
|
+
[System.IO.DirectoryInfo]::new($path).SetAccessControl($security)
|
|
295
|
+
} else {
|
|
296
|
+
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
|
|
297
|
+
$sid,
|
|
298
|
+
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
299
|
+
[System.Security.AccessControl.InheritanceFlags]::None,
|
|
300
|
+
[System.Security.AccessControl.PropagationFlags]::None,
|
|
301
|
+
[System.Security.AccessControl.AccessControlType]::Allow
|
|
302
|
+
)
|
|
303
|
+
$security.AddAccessRule($rule) | Out-Null
|
|
304
|
+
[System.IO.FileInfo]::new($path).SetAccessControl($security)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if ($operation -eq 'repair') {
|
|
309
|
+
Write-CcOwnerOnlyAcl $item $target
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
$acl = Read-CcAcl $target
|
|
313
|
+
$owner = $acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value
|
|
314
|
+
$rules = @($acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]))
|
|
315
|
+
$protectionOk = -not $item.PSIsContainer -or $acl.AreAccessRulesProtected
|
|
316
|
+
$ownerOnly =
|
|
317
|
+
$protectionOk -and
|
|
318
|
+
$owner -eq $sid.Value -and
|
|
319
|
+
$rules.Count -eq 1
|
|
320
|
+
if ($ownerOnly) {
|
|
321
|
+
foreach ($rule in $rules) {
|
|
322
|
+
$full = ($rule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -eq [System.Security.AccessControl.FileSystemRights]::FullControl
|
|
323
|
+
if ($item.PSIsContainer) {
|
|
324
|
+
$requiredInheritance =
|
|
325
|
+
[System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
|
|
326
|
+
[System.Security.AccessControl.InheritanceFlags]::ObjectInherit
|
|
327
|
+
$inheritanceOk =
|
|
328
|
+
($rule.InheritanceFlags -band $requiredInheritance) -eq $requiredInheritance
|
|
329
|
+
} else {
|
|
330
|
+
$inheritanceOk =
|
|
331
|
+
$rule.InheritanceFlags -eq [System.Security.AccessControl.InheritanceFlags]::None
|
|
332
|
+
}
|
|
333
|
+
$propagationOk =
|
|
334
|
+
$rule.PropagationFlags -eq [System.Security.AccessControl.PropagationFlags]::None
|
|
335
|
+
if ($rule.IdentityReference.Value -ne $sid.Value -or
|
|
336
|
+
$rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or
|
|
337
|
+
($item.PSIsContainer -and $rule.IsInherited) -or
|
|
338
|
+
-not $inheritanceOk -or
|
|
339
|
+
-not $propagationOk -or
|
|
340
|
+
-not $full) {
|
|
341
|
+
$ownerOnly = $false
|
|
342
|
+
break
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
[pscustomobject]@{
|
|
347
|
+
ownerOnly = [bool]$ownerOnly
|
|
348
|
+
isDirectory = [bool]$item.PSIsContainer
|
|
349
|
+
ownerSid = $owner
|
|
350
|
+
currentSid = $sid.Value
|
|
351
|
+
tokenOwnerSid = if ($null -eq $tokenOwner) { $null } else { $tokenOwner.Value }
|
|
352
|
+
protected = [bool]$acl.AreAccessRulesProtected
|
|
353
|
+
aceCount = $rules.Count
|
|
354
|
+
} | ConvertTo-Json -Compress
|
|
355
|
+
if (-not $ownerOnly) { exit 4 }
|
|
356
|
+
`;
|
|
357
|
+
|
|
358
|
+
const WINDOWS_ACL_BATCH_SCRIPT = String.raw`
|
|
359
|
+
$ErrorActionPreference = 'Stop'
|
|
360
|
+
$utf8 = [System.Text.UTF8Encoding]::new($false)
|
|
361
|
+
[Console]::InputEncoding = $utf8
|
|
362
|
+
[Console]::OutputEncoding = $utf8
|
|
363
|
+
$sections =
|
|
364
|
+
[System.Security.AccessControl.AccessControlSections]::Access -bor
|
|
365
|
+
[System.Security.AccessControl.AccessControlSections]::Owner
|
|
366
|
+
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
|
367
|
+
$sid = $identity.User
|
|
368
|
+
$tokenOwner = $identity.Owner
|
|
369
|
+
$request = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
370
|
+
$operation = [string]$request.operation
|
|
371
|
+
$targets = @($request.targets)
|
|
372
|
+
$expectedKind = [string]$request.expectedKind
|
|
373
|
+
if ($operation -ne 'inspect' -and $operation -ne 'repair' -and $operation -ne 'preflight') {
|
|
374
|
+
throw "unsupported ACL batch operation: $operation"
|
|
375
|
+
}
|
|
376
|
+
if ($expectedKind -ne '' -and $expectedKind -ne 'file' -and $expectedKind -ne 'directory') {
|
|
377
|
+
throw "unsupported expected path kind: $expectedKind"
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function Assert-CcNoReparseTraversal([string]$path) {
|
|
381
|
+
$cursor = [System.IO.Path]::GetFullPath($path)
|
|
382
|
+
while (-not (Test-Path -LiteralPath $cursor)) {
|
|
383
|
+
$parent = [System.IO.Directory]::GetParent($cursor)
|
|
384
|
+
if ($null -eq $parent) { return }
|
|
385
|
+
$cursor = $parent.FullName
|
|
386
|
+
}
|
|
387
|
+
$entry = Get-Item -LiteralPath $cursor -Force
|
|
388
|
+
while ($null -ne $entry) {
|
|
389
|
+
if (($entry.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
390
|
+
throw "symbolic links and reparse points are not allowed: $($entry.FullName)"
|
|
391
|
+
}
|
|
392
|
+
$entry = $entry.Parent
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
# A repair is all-or-nothing with respect to path safety. Preflight every
|
|
397
|
+
# existing target before the first SetAccessControl call so a junction root
|
|
398
|
+
# cannot fail after a regular-looking descendant has already been modified.
|
|
399
|
+
if ($operation -eq 'repair' -or $operation -eq 'preflight') {
|
|
400
|
+
$preflightFailures = @()
|
|
401
|
+
foreach ($target in $targets) {
|
|
402
|
+
try {
|
|
403
|
+
Assert-CcNoReparseTraversal $target
|
|
404
|
+
$candidate = Get-Item -LiteralPath $target -Force
|
|
405
|
+
$kindMismatch =
|
|
406
|
+
($expectedKind -eq 'file' -and $candidate.PSIsContainer) -or
|
|
407
|
+
($expectedKind -eq 'directory' -and -not $candidate.PSIsContainer)
|
|
408
|
+
if ($kindMismatch) {
|
|
409
|
+
$preflightFailures += [pscustomobject]@{
|
|
410
|
+
target = [string]$target
|
|
411
|
+
error = "expected $expectedKind but found $(if ($candidate.PSIsContainer) { 'directory' } else { 'file' })"
|
|
412
|
+
errorCode = 'EXPECTED_KIND_MISMATCH'
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
if ($_.CategoryInfo.Category -ne [System.Management.Automation.ErrorCategory]::ObjectNotFound) {
|
|
417
|
+
$preflightFailures += [pscustomobject]@{
|
|
418
|
+
target = [string]$target
|
|
419
|
+
error = $_.Exception.Message
|
|
420
|
+
errorCode = 'PATH_PREFLIGHT_FAILED'
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if ($preflightFailures.Count -gt 0) {
|
|
426
|
+
$firstFailure = $preflightFailures[0]
|
|
427
|
+
$output = @($targets | ForEach-Object {
|
|
428
|
+
[pscustomobject]@{
|
|
429
|
+
target = [string]$_
|
|
430
|
+
exists = [bool](Test-Path -LiteralPath $_)
|
|
431
|
+
isDirectory = $null
|
|
432
|
+
ok = $false
|
|
433
|
+
error = "ACL batch preflight failed at $($firstFailure.target): $($firstFailure.error)"
|
|
434
|
+
errorCode = $firstFailure.errorCode
|
|
435
|
+
}
|
|
436
|
+
})
|
|
437
|
+
[Console]::Write((ConvertTo-Json -InputObject $output -Compress -Depth 4))
|
|
438
|
+
exit 4
|
|
439
|
+
}
|
|
440
|
+
if ($operation -eq 'preflight') {
|
|
441
|
+
$output = @($targets | ForEach-Object {
|
|
442
|
+
$preflightTarget = [string]$_
|
|
443
|
+
$nativeItem = $null
|
|
444
|
+
try {
|
|
445
|
+
$nativeItem = Get-Item -LiteralPath $preflightTarget -Force
|
|
446
|
+
} catch {
|
|
447
|
+
if ($_.CategoryInfo.Category -ne [System.Management.Automation.ErrorCategory]::ObjectNotFound) {
|
|
448
|
+
throw
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
[pscustomobject]@{
|
|
452
|
+
target = $preflightTarget
|
|
453
|
+
exists = [bool]($null -ne $nativeItem)
|
|
454
|
+
isDirectory = if ($null -eq $nativeItem) { $null } else { [bool]$nativeItem.PSIsContainer }
|
|
455
|
+
ok = $true
|
|
456
|
+
error = $null
|
|
457
|
+
}
|
|
458
|
+
})
|
|
459
|
+
[Console]::Write((ConvertTo-Json -InputObject $output -Compress -Depth 4))
|
|
460
|
+
exit 0
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function Read-CcAcl([string]$path) {
|
|
465
|
+
if ([System.IO.Directory]::Exists($path)) {
|
|
466
|
+
return [System.Security.AccessControl.DirectorySecurity]::new($path, $sections)
|
|
467
|
+
}
|
|
468
|
+
return [System.Security.AccessControl.FileSecurity]::new($path, $sections)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function Write-CcOwnerOnlyAcl($item, [string]$path) {
|
|
472
|
+
$security = Read-CcAcl $path
|
|
473
|
+
$owner = $security.GetOwner([System.Security.Principal.SecurityIdentifier])
|
|
474
|
+
if ($owner.Value -ne $sid.Value) {
|
|
475
|
+
if ($null -eq $tokenOwner -or $owner.Value -ne $tokenOwner.Value) {
|
|
476
|
+
throw "owner-only ACL repair refuses a path owned by another identity: $path"
|
|
477
|
+
}
|
|
478
|
+
$security.SetOwner($sid)
|
|
479
|
+
}
|
|
480
|
+
$security.SetAccessRuleProtection($true, $false)
|
|
481
|
+
$existingRules = @($security.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]))
|
|
482
|
+
foreach ($existingRule in $existingRules) {
|
|
483
|
+
$security.RemoveAccessRuleAll($existingRule)
|
|
484
|
+
}
|
|
485
|
+
if ($item.PSIsContainer) {
|
|
486
|
+
$inheritance =
|
|
487
|
+
[System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
|
|
488
|
+
[System.Security.AccessControl.InheritanceFlags]::ObjectInherit
|
|
489
|
+
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
|
|
490
|
+
$sid,
|
|
491
|
+
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
492
|
+
$inheritance,
|
|
493
|
+
[System.Security.AccessControl.PropagationFlags]::None,
|
|
494
|
+
[System.Security.AccessControl.AccessControlType]::Allow
|
|
495
|
+
)
|
|
496
|
+
$security.AddAccessRule($rule) | Out-Null
|
|
497
|
+
[System.IO.DirectoryInfo]::new($path).SetAccessControl($security)
|
|
498
|
+
} else {
|
|
499
|
+
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
|
|
500
|
+
$sid,
|
|
501
|
+
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
502
|
+
[System.Security.AccessControl.InheritanceFlags]::None,
|
|
503
|
+
[System.Security.AccessControl.PropagationFlags]::None,
|
|
504
|
+
[System.Security.AccessControl.AccessControlType]::Allow
|
|
505
|
+
)
|
|
506
|
+
$security.AddAccessRule($rule) | Out-Null
|
|
507
|
+
[System.IO.FileInfo]::new($path).SetAccessControl($security)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
$results = foreach ($target in $targets) {
|
|
512
|
+
$item = $null
|
|
513
|
+
try {
|
|
514
|
+
Assert-CcNoReparseTraversal $target
|
|
515
|
+
$item = Get-Item -LiteralPath $target -Force
|
|
516
|
+
$kindMismatch =
|
|
517
|
+
($expectedKind -eq 'file' -and $item.PSIsContainer) -or
|
|
518
|
+
($expectedKind -eq 'directory' -and -not $item.PSIsContainer)
|
|
519
|
+
if ($kindMismatch) {
|
|
520
|
+
[pscustomobject]@{
|
|
521
|
+
target = [string]$target
|
|
522
|
+
exists = $true
|
|
523
|
+
isDirectory = [bool]$item.PSIsContainer
|
|
524
|
+
ok = $false
|
|
525
|
+
error = "expected $expectedKind but found $(if ($item.PSIsContainer) { 'directory' } else { 'file' })"
|
|
526
|
+
errorCode = 'EXPECTED_KIND_MISMATCH'
|
|
527
|
+
}
|
|
528
|
+
continue
|
|
529
|
+
}
|
|
530
|
+
if ($operation -eq 'repair') {
|
|
531
|
+
Write-CcOwnerOnlyAcl $item $target
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
$acl = Read-CcAcl $target
|
|
535
|
+
$owner = $acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value
|
|
536
|
+
$rules = @($acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]))
|
|
537
|
+
$protectionOk = -not $item.PSIsContainer -or $acl.AreAccessRulesProtected
|
|
538
|
+
$ownerOnly =
|
|
539
|
+
$protectionOk -and
|
|
540
|
+
$owner -eq $sid.Value -and
|
|
541
|
+
$rules.Count -eq 1
|
|
542
|
+
if ($ownerOnly) {
|
|
543
|
+
foreach ($rule in $rules) {
|
|
544
|
+
$full = ($rule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -eq [System.Security.AccessControl.FileSystemRights]::FullControl
|
|
545
|
+
if ($item.PSIsContainer) {
|
|
546
|
+
$requiredInheritance =
|
|
547
|
+
[System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
|
|
548
|
+
[System.Security.AccessControl.InheritanceFlags]::ObjectInherit
|
|
549
|
+
$inheritanceOk =
|
|
550
|
+
($rule.InheritanceFlags -band $requiredInheritance) -eq $requiredInheritance
|
|
551
|
+
} else {
|
|
552
|
+
$inheritanceOk =
|
|
553
|
+
$rule.InheritanceFlags -eq [System.Security.AccessControl.InheritanceFlags]::None
|
|
554
|
+
}
|
|
555
|
+
$propagationOk =
|
|
556
|
+
$rule.PropagationFlags -eq [System.Security.AccessControl.PropagationFlags]::None
|
|
557
|
+
if ($rule.IdentityReference.Value -ne $sid.Value -or
|
|
558
|
+
$rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or
|
|
559
|
+
($item.PSIsContainer -and $rule.IsInherited) -or
|
|
560
|
+
-not $inheritanceOk -or
|
|
561
|
+
-not $propagationOk -or
|
|
562
|
+
-not $full) {
|
|
563
|
+
$ownerOnly = $false
|
|
564
|
+
break
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
[pscustomobject]@{
|
|
569
|
+
target = [string]$target
|
|
570
|
+
exists = $true
|
|
571
|
+
isDirectory = [bool]$item.PSIsContainer
|
|
572
|
+
ok = [bool]$ownerOnly
|
|
573
|
+
ownerSid = $owner
|
|
574
|
+
currentSid = $sid.Value
|
|
575
|
+
tokenOwnerSid = if ($null -eq $tokenOwner) { $null } else { $tokenOwner.Value }
|
|
576
|
+
protected = [bool]$acl.AreAccessRulesProtected
|
|
577
|
+
aceCount = $rules.Count
|
|
578
|
+
error = if ($ownerOnly) { $null } else { 'path is not owner-only' }
|
|
579
|
+
errorCode = $null
|
|
580
|
+
}
|
|
581
|
+
} catch {
|
|
582
|
+
[pscustomobject]@{
|
|
583
|
+
target = [string]$target
|
|
584
|
+
exists = [bool](($null -ne $item) -or (Test-Path -LiteralPath $target))
|
|
585
|
+
isDirectory = $null
|
|
586
|
+
ok = $false
|
|
587
|
+
error = $_.Exception.Message
|
|
588
|
+
errorCode = 'PATH_OPERATION_FAILED'
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
$output = [array]@($results)
|
|
593
|
+
[Console]::Write((ConvertTo-Json -InputObject $output -Compress -Depth 4))
|
|
594
|
+
if (@($output | Where-Object { -not $_.ok }).Count -gt 0) { exit 4 }
|
|
595
|
+
`;
|
|
596
|
+
|
|
597
|
+
export const _deps = {
|
|
598
|
+
fs,
|
|
599
|
+
spawnSync,
|
|
600
|
+
platform: () => process.platform,
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
// ACL repair starts a short PowerShell process on Windows. Cache successful
|
|
604
|
+
// production repairs by path for this process; explicit injected-dependency
|
|
605
|
+
// tests and repairPrivatePath() always execute and verify the operation.
|
|
606
|
+
const securedWindowsPaths = new Set();
|
|
607
|
+
const WINDOWS_ACL_BATCH_SIZE = 500;
|
|
608
|
+
|
|
609
|
+
function repairWindowsAclOnce(target, deps, options) {
|
|
610
|
+
const cacheable = !options.deps;
|
|
611
|
+
if (cacheable && securedWindowsPaths.has(target)) {
|
|
612
|
+
return { ok: true, platform: "win32", cached: true };
|
|
613
|
+
}
|
|
614
|
+
const result = windowsAcl(target, "repair", deps);
|
|
615
|
+
if (result.ok && cacheable) securedWindowsPaths.add(target);
|
|
616
|
+
return result;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function windowsAcl(target, operation, deps) {
|
|
620
|
+
const result = deps.spawnSync(
|
|
621
|
+
"powershell.exe",
|
|
622
|
+
[
|
|
623
|
+
"-NoLogo",
|
|
624
|
+
"-NoProfile",
|
|
625
|
+
"-NonInteractive",
|
|
626
|
+
"-ExecutionPolicy",
|
|
627
|
+
"Bypass",
|
|
628
|
+
"-Command",
|
|
629
|
+
`& { ${WINDOWS_ACL_SCRIPT} }`,
|
|
630
|
+
target,
|
|
631
|
+
operation,
|
|
632
|
+
],
|
|
633
|
+
{ encoding: "utf8", windowsHide: true, timeout: 15000 },
|
|
634
|
+
);
|
|
635
|
+
let details = null;
|
|
636
|
+
try {
|
|
637
|
+
details = JSON.parse(String(result?.stdout || "").trim());
|
|
638
|
+
} catch {
|
|
639
|
+
details = null;
|
|
640
|
+
}
|
|
641
|
+
if (result?.error) {
|
|
642
|
+
return { ok: false, platform: "win32", error: result.error.message };
|
|
643
|
+
}
|
|
644
|
+
if (result?.status !== 0 || details?.ownerOnly !== true) {
|
|
645
|
+
return {
|
|
646
|
+
ok: false,
|
|
647
|
+
platform: "win32",
|
|
648
|
+
error:
|
|
649
|
+
String(result?.stderr || "").trim() ||
|
|
650
|
+
`owner-only ACL verification exited ${result?.status}`,
|
|
651
|
+
details,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
return { ok: true, platform: "win32", details };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function windowsAclBatch(targets, operation, deps, expectedKind = null) {
|
|
658
|
+
if (targets.length === 0) return [];
|
|
659
|
+
const result = deps.spawnSync(
|
|
660
|
+
"powershell.exe",
|
|
661
|
+
[
|
|
662
|
+
"-NoLogo",
|
|
663
|
+
"-NoProfile",
|
|
664
|
+
"-NonInteractive",
|
|
665
|
+
"-ExecutionPolicy",
|
|
666
|
+
"Bypass",
|
|
667
|
+
"-Command",
|
|
668
|
+
`& { ${WINDOWS_ACL_BATCH_SCRIPT} }`,
|
|
669
|
+
],
|
|
670
|
+
{
|
|
671
|
+
encoding: "utf8",
|
|
672
|
+
input: JSON.stringify({
|
|
673
|
+
operation,
|
|
674
|
+
targets,
|
|
675
|
+
...(expectedKind ? { expectedKind } : {}),
|
|
676
|
+
}),
|
|
677
|
+
windowsHide: true,
|
|
678
|
+
timeout: 30000,
|
|
679
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
680
|
+
},
|
|
681
|
+
);
|
|
682
|
+
let details = [];
|
|
683
|
+
try {
|
|
684
|
+
const parsed = JSON.parse(String(result?.stdout || "").trim());
|
|
685
|
+
details = Array.isArray(parsed) ? parsed : [parsed];
|
|
686
|
+
} catch {
|
|
687
|
+
details = [];
|
|
688
|
+
}
|
|
689
|
+
const anyFailed = details.some((entry) => entry?.ok !== true);
|
|
690
|
+
const statusOk = result?.status === 0 || (result?.status === 4 && anyFailed);
|
|
691
|
+
const uniqueDetailTargets = new Set(details.map((entry) => entry?.target));
|
|
692
|
+
if (
|
|
693
|
+
result?.error ||
|
|
694
|
+
!statusOk ||
|
|
695
|
+
details.length !== targets.length ||
|
|
696
|
+
uniqueDetailTargets.size !== targets.length
|
|
697
|
+
) {
|
|
698
|
+
const error =
|
|
699
|
+
result?.error?.message ||
|
|
700
|
+
String(result?.stderr || "").trim() ||
|
|
701
|
+
"owner-only ACL batch verification failed";
|
|
702
|
+
return targets.map((target) => ({
|
|
703
|
+
target,
|
|
704
|
+
ok: false,
|
|
705
|
+
platform: "win32",
|
|
706
|
+
error,
|
|
707
|
+
}));
|
|
708
|
+
}
|
|
709
|
+
const byTarget = new Map(details.map((entry) => [entry.target, entry]));
|
|
710
|
+
return targets.map((target) => {
|
|
711
|
+
const entry = byTarget.get(target);
|
|
712
|
+
return {
|
|
713
|
+
target,
|
|
714
|
+
exists: entry?.exists !== false,
|
|
715
|
+
ok: entry?.ok === true,
|
|
716
|
+
platform: "win32",
|
|
717
|
+
details: entry || null,
|
|
718
|
+
...(entry?.errorCode ? { errorCode: entry.errorCode } : {}),
|
|
719
|
+
...(entry?.ok === true
|
|
720
|
+
? {}
|
|
721
|
+
: {
|
|
722
|
+
error:
|
|
723
|
+
entry?.error ||
|
|
724
|
+
String(result?.stderr || "").trim() ||
|
|
725
|
+
`owner-only ACL batch verification exited ${result?.status}`,
|
|
726
|
+
}),
|
|
727
|
+
};
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function isWindowsPathStatUnreliable(error, platform) {
|
|
732
|
+
return (
|
|
733
|
+
platform === "win32" &&
|
|
734
|
+
(error?.code === "EPERM" ||
|
|
735
|
+
error?.code === "EACCES" ||
|
|
736
|
+
error?.code === "ENOENT" ||
|
|
737
|
+
error?.code === "WINDOWS_PATH_STAT_UNRELIABLE")
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function preflightWindowsPaths(targets, deps) {
|
|
742
|
+
const results = [];
|
|
743
|
+
for (
|
|
744
|
+
let offset = 0;
|
|
745
|
+
offset < targets.length;
|
|
746
|
+
offset += WINDOWS_ACL_BATCH_SIZE
|
|
747
|
+
) {
|
|
748
|
+
results.push(
|
|
749
|
+
...windowsAclBatch(
|
|
750
|
+
targets.slice(offset, offset + WINDOWS_ACL_BATCH_SIZE),
|
|
751
|
+
"preflight",
|
|
752
|
+
deps,
|
|
753
|
+
),
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
const failed = results.find((entry) => entry.ok !== true);
|
|
757
|
+
if (failed) {
|
|
758
|
+
const error = new Error(
|
|
759
|
+
`Could not verify Windows path ancestors for ${failed.target}: ${failed.error || "native preflight failed"}`,
|
|
760
|
+
);
|
|
761
|
+
error.code = "CONFIG_HOME_UNSAFE";
|
|
762
|
+
throw error;
|
|
763
|
+
}
|
|
764
|
+
return results;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function privatePathEntryFromNativeEvidence(target, nativeResult) {
|
|
768
|
+
if (!nativeResult || !samePath(nativeResult.target, target, true)) {
|
|
769
|
+
const unsafe = new Error(
|
|
770
|
+
`Native Windows path evidence did not match requested target: ${target}`,
|
|
771
|
+
);
|
|
772
|
+
unsafe.code = "CONFIG_HOME_UNSAFE";
|
|
773
|
+
throw unsafe;
|
|
774
|
+
}
|
|
775
|
+
if (nativeResult.exists === false) {
|
|
776
|
+
return { exists: false, entry: null };
|
|
777
|
+
}
|
|
778
|
+
const isDirectory = nativeResult.details?.isDirectory;
|
|
779
|
+
if (typeof isDirectory !== "boolean") {
|
|
780
|
+
const unsafe = new Error(
|
|
781
|
+
`Could not classify Windows path type after native preflight: ${target}`,
|
|
782
|
+
);
|
|
783
|
+
unsafe.code = "CONFIG_HOME_UNSAFE";
|
|
784
|
+
throw unsafe;
|
|
785
|
+
}
|
|
786
|
+
return {
|
|
787
|
+
exists: true,
|
|
788
|
+
entry: {
|
|
789
|
+
isDirectory: () => isDirectory,
|
|
790
|
+
isSymbolicLink: () => false,
|
|
791
|
+
},
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function readPrivatePathEntry(target, deps, platform, nativeEvidence = null) {
|
|
796
|
+
if (platform === "win32" && nativeEvidence) {
|
|
797
|
+
return privatePathEntryFromNativeEvidence(target, nativeEvidence);
|
|
798
|
+
}
|
|
799
|
+
try {
|
|
800
|
+
return { exists: true, entry: deps.fs.lstatSync(target) };
|
|
801
|
+
} catch (error) {
|
|
802
|
+
if (platform !== "win32") {
|
|
803
|
+
if (error?.code === "ENOENT") return { exists: false, entry: null };
|
|
804
|
+
throw error;
|
|
805
|
+
}
|
|
806
|
+
if (!isWindowsPathStatUnreliable(error, platform)) throw error;
|
|
807
|
+
const directoryEntry = readWindowsDirectoryEntry(target, deps.fs);
|
|
808
|
+
if (directoryEntry) {
|
|
809
|
+
return {
|
|
810
|
+
exists: true,
|
|
811
|
+
entry: {
|
|
812
|
+
isDirectory: () => directoryEntry.isDirectory,
|
|
813
|
+
isSymbolicLink: () => directoryEntry.isSymbolicLink,
|
|
814
|
+
},
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
const [nativeResult] = preflightWindowsPaths([String(target)], deps);
|
|
818
|
+
return privatePathEntryFromNativeEvidence(target, nativeResult);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function assertNoLinkTraversalWithWindowsFallback(
|
|
823
|
+
target,
|
|
824
|
+
deps,
|
|
825
|
+
checkedPaths,
|
|
826
|
+
platform,
|
|
827
|
+
) {
|
|
828
|
+
try {
|
|
829
|
+
assertNoLinkTraversal(target, deps.fs, checkedPaths, platform);
|
|
830
|
+
return null;
|
|
831
|
+
} catch (error) {
|
|
832
|
+
if (!isWindowsPathStatUnreliable(error, platform)) throw error;
|
|
833
|
+
return preflightWindowsPaths([String(target)], deps)[0];
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
export function inspectPrivatePaths(targets, options = {}) {
|
|
838
|
+
const uniqueTargets = [...new Set((targets || []).map(String))];
|
|
839
|
+
const deps = { ..._deps, ...(options.deps || {}) };
|
|
840
|
+
if ((options.platform || deps.platform()) === "win32") {
|
|
841
|
+
const results = [];
|
|
842
|
+
for (
|
|
843
|
+
let offset = 0;
|
|
844
|
+
offset < uniqueTargets.length;
|
|
845
|
+
offset += WINDOWS_ACL_BATCH_SIZE
|
|
846
|
+
) {
|
|
847
|
+
results.push(
|
|
848
|
+
...windowsAclBatch(
|
|
849
|
+
uniqueTargets.slice(offset, offset + WINDOWS_ACL_BATCH_SIZE),
|
|
850
|
+
"inspect",
|
|
851
|
+
deps,
|
|
852
|
+
),
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
return results;
|
|
856
|
+
}
|
|
857
|
+
return uniqueTargets.map((target) => ({
|
|
858
|
+
target,
|
|
859
|
+
...inspectPrivatePath(target, { ...options, deps }),
|
|
860
|
+
}));
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
export function repairPrivatePaths(targets, options = {}) {
|
|
864
|
+
const uniqueTargets = [...new Set((targets || []).map(String))];
|
|
865
|
+
const deps = { ..._deps, ...(options.deps || {}) };
|
|
866
|
+
const platform = options.platform || deps.platform();
|
|
867
|
+
const checkedPaths = new Set();
|
|
868
|
+
let nativeWindowsPreflightRequired = false;
|
|
869
|
+
// Complete every path-safety check before starting a batched mutation. This
|
|
870
|
+
// prevents a linked trust root from failing only after a descendant changed.
|
|
871
|
+
for (const target of uniqueTargets) {
|
|
872
|
+
assertSafeOwnerOnlyPath(target, platform);
|
|
873
|
+
}
|
|
874
|
+
for (const target of uniqueTargets) {
|
|
875
|
+
try {
|
|
876
|
+
assertNoLinkTraversal(target, deps.fs, checkedPaths, platform);
|
|
877
|
+
} catch (error) {
|
|
878
|
+
if (!isWindowsPathStatUnreliable(error, platform)) throw error;
|
|
879
|
+
nativeWindowsPreflightRequired = true;
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (nativeWindowsPreflightRequired) {
|
|
884
|
+
preflightWindowsPaths(uniqueTargets, deps);
|
|
885
|
+
}
|
|
886
|
+
if (platform === "win32") {
|
|
887
|
+
const results = [];
|
|
888
|
+
for (
|
|
889
|
+
let offset = 0;
|
|
890
|
+
offset < uniqueTargets.length;
|
|
891
|
+
offset += WINDOWS_ACL_BATCH_SIZE
|
|
892
|
+
) {
|
|
893
|
+
results.push(
|
|
894
|
+
...windowsAclBatch(
|
|
895
|
+
uniqueTargets.slice(offset, offset + WINDOWS_ACL_BATCH_SIZE),
|
|
896
|
+
"repair",
|
|
897
|
+
deps,
|
|
898
|
+
),
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
const failed = results.filter(
|
|
902
|
+
(entry) => !entry.ok && entry.exists !== false,
|
|
903
|
+
);
|
|
904
|
+
if (failed.length > 0) {
|
|
905
|
+
throw new Error(
|
|
906
|
+
`Could not repair owner-only ACL for ${failed[0].target}: ${failed[0].error}`,
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
if (!options.deps) {
|
|
910
|
+
for (const result of results) {
|
|
911
|
+
if (result.ok && result.exists !== false) {
|
|
912
|
+
securedWindowsPaths.add(result.target);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return results;
|
|
917
|
+
}
|
|
918
|
+
return uniqueTargets.map((target) => {
|
|
919
|
+
try {
|
|
920
|
+
return {
|
|
921
|
+
target,
|
|
922
|
+
...repairPrivatePathAfterPreflight(target, options, deps),
|
|
923
|
+
};
|
|
924
|
+
} catch (error) {
|
|
925
|
+
if (error?.code === "ENOENT") {
|
|
926
|
+
return { target, ok: true, exists: false, skipped: true };
|
|
927
|
+
}
|
|
928
|
+
throw error;
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
export function inspectPrivatePath(target, options = {}) {
|
|
934
|
+
const deps = { ..._deps, ...(options.deps || {}) };
|
|
935
|
+
const platform = options.platform || deps.platform();
|
|
936
|
+
if (platform === "win32") {
|
|
937
|
+
return windowsAclBatch([String(target)], "inspect", deps)[0];
|
|
938
|
+
}
|
|
939
|
+
let stat;
|
|
940
|
+
try {
|
|
941
|
+
stat = deps.fs.lstatSync(target);
|
|
942
|
+
} catch (error) {
|
|
943
|
+
return { ok: false, exists: false, error: error.message };
|
|
944
|
+
}
|
|
945
|
+
if (stat.isSymbolicLink()) {
|
|
946
|
+
return { ok: false, exists: true, error: "symbolic links are not allowed" };
|
|
947
|
+
}
|
|
948
|
+
const expected = stat.isDirectory()
|
|
949
|
+
? PRIVATE_DIRECTORY_MODE
|
|
950
|
+
: PRIVATE_FILE_MODE;
|
|
951
|
+
const actual = stat.mode & 0o777;
|
|
952
|
+
const ownerOk =
|
|
953
|
+
typeof process.getuid !== "function" || stat.uid === process.getuid();
|
|
954
|
+
return {
|
|
955
|
+
ok: actual === expected && ownerOk,
|
|
956
|
+
exists: true,
|
|
957
|
+
platform: options.platform || deps.platform(),
|
|
958
|
+
expectedMode: expected,
|
|
959
|
+
actualMode: actual,
|
|
960
|
+
ownerOk,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
export function repairPrivatePath(target, options = {}) {
|
|
965
|
+
const deps = { ..._deps, ...(options.deps || {}) };
|
|
966
|
+
const platform = options.platform || deps.platform();
|
|
967
|
+
assertSafeOwnerOnlyPath(target, platform);
|
|
968
|
+
assertNoLinkTraversalWithWindowsFallback(target, deps, null, platform);
|
|
969
|
+
return repairPrivatePathAfterPreflight(target, options, deps);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function repairPrivatePathAfterPreflight(target, options, deps) {
|
|
973
|
+
if ((options.platform || deps.platform()) === "win32") {
|
|
974
|
+
const result = windowsAcl(target, "repair", deps);
|
|
975
|
+
if (!result.ok)
|
|
976
|
+
throw new Error(result.error || "owner-only ACL repair failed");
|
|
977
|
+
if (!options.deps) securedWindowsPaths.add(target);
|
|
978
|
+
return result;
|
|
979
|
+
}
|
|
980
|
+
const stat = deps.fs.lstatSync(target);
|
|
981
|
+
if (stat.isSymbolicLink()) {
|
|
982
|
+
throw new Error(
|
|
983
|
+
`Refusing to change permissions through a symbolic link: ${target}`,
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
deps.fs.chmodSync(
|
|
987
|
+
target,
|
|
988
|
+
stat.isDirectory() ? PRIVATE_DIRECTORY_MODE : PRIVATE_FILE_MODE,
|
|
989
|
+
);
|
|
990
|
+
const result = inspectPrivatePath(target, { ...options, deps });
|
|
991
|
+
if (!result.ok)
|
|
992
|
+
throw new Error(`Could not verify owner-only permissions: ${target}`);
|
|
993
|
+
return result;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
export function ensurePrivateDirectory(target, options = {}) {
|
|
997
|
+
const deps = { ..._deps, ...(options.deps || {}) };
|
|
998
|
+
const platform = options.platform || deps.platform();
|
|
999
|
+
assertSafeOwnerOnlyPath(target, platform);
|
|
1000
|
+
const nativeEvidence = assertNoLinkTraversalWithWindowsFallback(
|
|
1001
|
+
target,
|
|
1002
|
+
deps,
|
|
1003
|
+
null,
|
|
1004
|
+
platform,
|
|
1005
|
+
);
|
|
1006
|
+
let existed = deps.fs.existsSync(target);
|
|
1007
|
+
if (existed) {
|
|
1008
|
+
const pathEntry = readPrivatePathEntry(
|
|
1009
|
+
target,
|
|
1010
|
+
deps,
|
|
1011
|
+
platform,
|
|
1012
|
+
nativeEvidence,
|
|
1013
|
+
);
|
|
1014
|
+
existed = pathEntry.exists;
|
|
1015
|
+
const existing = pathEntry.entry;
|
|
1016
|
+
if (existed && existing.isSymbolicLink()) {
|
|
1017
|
+
throw new Error(
|
|
1018
|
+
`Refusing owner-only directory through a symbolic link: ${target}`,
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
if (
|
|
1022
|
+
existed &&
|
|
1023
|
+
typeof existing.isDirectory === "function" &&
|
|
1024
|
+
!existing.isDirectory()
|
|
1025
|
+
) {
|
|
1026
|
+
throw new Error(`Expected a directory for owner-only storage: ${target}`);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
deps.fs.mkdirSync(target, {
|
|
1030
|
+
recursive: true,
|
|
1031
|
+
mode: PRIVATE_DIRECTORY_MODE,
|
|
1032
|
+
});
|
|
1033
|
+
if ((options.platform || deps.platform()) !== "win32") {
|
|
1034
|
+
deps.fs.chmodSync(target, PRIVATE_DIRECTORY_MODE);
|
|
1035
|
+
} else if (
|
|
1036
|
+
options.applyWindowsAcl === true ||
|
|
1037
|
+
(!existed && options.applyWindowsAcl !== false)
|
|
1038
|
+
) {
|
|
1039
|
+
const result = repairWindowsAclOnce(target, deps, options);
|
|
1040
|
+
if (!result.ok && options.failIfUnavailable) {
|
|
1041
|
+
throw new Error(result.error || `Could not secure ${target}`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return target;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
export function ensurePrivateFile(target, options = {}) {
|
|
1048
|
+
const deps = { ..._deps, ...(options.deps || {}) };
|
|
1049
|
+
const platform = options.platform || deps.platform();
|
|
1050
|
+
assertSafeOwnerOnlyPath(target, platform);
|
|
1051
|
+
const nativeEvidence = assertNoLinkTraversalWithWindowsFallback(
|
|
1052
|
+
target,
|
|
1053
|
+
deps,
|
|
1054
|
+
null,
|
|
1055
|
+
platform,
|
|
1056
|
+
);
|
|
1057
|
+
const pathEntry = readPrivatePathEntry(
|
|
1058
|
+
target,
|
|
1059
|
+
deps,
|
|
1060
|
+
platform,
|
|
1061
|
+
nativeEvidence,
|
|
1062
|
+
);
|
|
1063
|
+
if (!pathEntry.exists) return target;
|
|
1064
|
+
const existing = pathEntry.entry;
|
|
1065
|
+
if (existing.isSymbolicLink()) {
|
|
1066
|
+
throw new Error(
|
|
1067
|
+
`Refusing owner-only file through a symbolic link: ${target}`,
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
if (typeof existing.isDirectory === "function" && existing.isDirectory()) {
|
|
1071
|
+
throw new Error(`Expected a file for owner-only storage: ${target}`);
|
|
1072
|
+
}
|
|
1073
|
+
if (platform !== "win32") {
|
|
1074
|
+
deps.fs.chmodSync(target, PRIVATE_FILE_MODE);
|
|
1075
|
+
} else if (
|
|
1076
|
+
options.applyWindowsAcl === true &&
|
|
1077
|
+
(options.deps || !securedWindowsPaths.has(target))
|
|
1078
|
+
) {
|
|
1079
|
+
const [result] = windowsAclBatch([String(target)], "repair", deps, "file");
|
|
1080
|
+
if (result.errorCode === "EXPECTED_KIND_MISMATCH") {
|
|
1081
|
+
throw new Error(`Expected a file for owner-only storage: ${target}`);
|
|
1082
|
+
}
|
|
1083
|
+
if (!result.ok && result.exists !== false && options.failIfUnavailable) {
|
|
1084
|
+
throw new Error(result.error || `Could not secure ${target}`);
|
|
1085
|
+
}
|
|
1086
|
+
if (result.ok && !options.deps) securedWindowsPaths.add(target);
|
|
1087
|
+
}
|
|
1088
|
+
return target;
|
|
1089
|
+
}
|