chainlesschain 0.163.2 → 0.163.4
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 +3 -2
- package/scripts/command-lifecycle-report.mjs +330 -38
- package/src/assets/web-panel/assets/{AIOps-DnGsmsqJ.js → AIOps-DB5X7qqV.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CrO6fcQ7.js → ActionButton-DxHRtKLk.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DsBdOaP2.js → Analytics-DIH5QrxT.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DmQslKUB.js → AppLayout-uu0oxVz7.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-BvyrT_QQ.js → Artifacts-BNSMiW9Y.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-prndoLiC.js → Audit-DcAzB4_x.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DOTFyA55.js → BackgroundAgents-ClSsvjIc.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-9zOnPp_8.js → Backup-BMciY0js.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-C4PC6sAG.js → BaseInput-Hn3b57Sh.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BBAF-W9v.js → Chat-CoSlC53V.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CWhvvloO.js → ChatBubbleRenderer-CFevJ3So.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BI9iculd.js → Checkbox-B4L4Eu4w.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-3DP9BCKb.js → Codegen-C77FgRxF.js} +1 -1
- package/src/assets/web-panel/assets/{Col-df-2TnJX.js → Col-BrLtJLH3.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CHs8gG_A.js → Community-Be7pXXhA.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CDfF6q7V.js → Compact-DuGPecjo.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CthrDnO7.js → Compliance-CzS66mjP.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-HjIqsWJw.js → Cowork-CEhjCoyK.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-BKc196fb.js → Cron-D7ir8Vja.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CJnjdc8W.js → Crosschain-BcXmuLxf.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BHUy-8Q2.js → DID-Cto1dtBy.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-QDB7sKeY.js → Dashboard-B7KjgwOX.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DStcVK11.js → Dropdown-COMqxijg.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-BwSUskMJ.js → EmailListRenderer-BZrya-nf.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DgF_kurT.js → FamilyGuardDashboard-AmVeMq0J.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-k8wvv-wN.js → Federation-kZU8c-27.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-C11qoq2r.js → FormItemContext-sOV22v2-.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BIIpr9kg.js → GenericCardRenderer-BUuLtGCz.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BlA1exTe.js → Git-K-ZxsLsH.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CYa1ZYM4.js → Governance-D3OEnawI.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-6_kKVIt6.js → Inference-Bll0gRmP.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-CB4AwxYy.js → KnowledgeGraph-DBXwjr53.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-xf7nHFAe.js → Logs-CEzPwEfP.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-Bf3DEqP6.js → MarkdownRenderer-CLmCbHx6.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-aPg8e4Jt.js → Marketplace-BYyRyOPz.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-C-TNlVUf.js → McpTools-DVu8w_rM.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-DMTPIpGa.js → Memory-tenuZ_hO.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BxyvveRq.js → MobileBridge-Bxgy4ozS.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-DyPCrAyQ.js → MobileProjects-Dgx1D6sc.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BdhqNXp6.js → Mtc-CRfJ8r8j.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-Cz-HFxzW.js → MtcAudit-BBJq4Aiz.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-CIUaFNsx.js → Multisig-B3ii9lai.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CWe6-J17.js → NLProgramming-FIi-Cubv.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-vYAKeviU.js → Notes-DIq8MPf1.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-DQL9-2d7.js → NotificationSettings-BFOD9eGd.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DnXj1jBn.js → OrderTableRenderer--41u7vre.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-B3spU8AN.js → Organization-B_0AWztW.js} +2 -2
- package/src/assets/web-panel/assets/{Overflow-DgEwo7vm.js → Overflow-B6I7TuXv.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-hujMRH8X.js → P2P-Cdf6qens.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-C_qhdg9h.js → PdhVaultBrowser-B_0FcQJY.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-vp7LMcSf.js → Permissions-6nFBjsQp.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-Dxr5Hgnq.js → PersonalDataHub-nQYV7xpn.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-DEPZqqH4.js → Pipeline-CBVod5jm.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DTpDiKPe.js → Privacy-D52jW1Nt.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-jDJsRWmF.js → ProjectInit-Bdx3BYhf.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-Dhor5RWH.js → ProjectSettings-b7xr4DUa.js} +2 -2
- package/src/assets/web-panel/assets/Projects-DhfifCKx.js +1 -0
- package/src/assets/web-panel/assets/Providers-Dy_PgDMr.js +1 -0
- package/src/assets/web-panel/assets/{QrScannerModal-CjIw-wIb.js → QrScannerModal-FQKSmc3e.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DX6jcqoi.js → QuickAsk-CSTrhzcF.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DopXkhar.js → Recommend-CxMABOUh.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-BLvqJE9c.js → RemoteSession-Cv2T8yRa.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-DhRj2Me7.js → Reputation-CE1k4Vvu.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DQ53vuPq.js → Row-BNgNcAV_.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BslvwiBD.js → RssFeed-5ZiBZ3ph.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DJKBDNKZ.js → Search-mDOIF1UV.js} +1 -1
- package/src/assets/web-panel/assets/{Security-COhpxHER.js → Security-CtuCTC-e.js} +3 -3
- package/src/assets/web-panel/assets/{Services-dbulD6cI.js → Services-DxFO2Zg7.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-ciIVNfhC.js → Skeleton-CKmKkH9y.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-AiRTkTCk.js → Skills-Bmzl9qkk.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DDMcWYBc.js → Sla-9RqaWHOC.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BPxWbpXO.js → SpeechSettings-CGdzFIYw.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BoEOXCyg.js → SyncSettings-CvNuCeBT.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-B_sLdcnw.js +1 -0
- package/src/assets/web-panel/assets/{Templates-UowY8Ixe.js → Templates-D9_YijdK.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DauSLQXN.js → Tenant-Cuc7BUSP.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DBj7Jneq.js → Terminal-6VYRnJgd.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-UMrPwoZC.js → TimelineRenderer-GtK8xd8Y.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-4ea96-bn.js → Tokens-BZ94Lt5W.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger--ISCqt4b.js → Trigger-DEcZgDaX.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-D4GVimRJ.js → Trust-CC3z9THe.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-Ci-nXRQc.js → UkeySign-Cvtw8cV3.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-Cjk7mKPw.js → VideoEditing-CL4e6Bn3.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BgOLOO3Z.js → Wallet-ycEemP27.js} +2 -2
- package/src/assets/web-panel/assets/{WebAuthn-CCX0K29i.js → WebAuthn-DgZ4X_aw.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-Ch3zgIsE.js → WorkflowEditor-D0qMhSv7.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DKOU_2Vb.js → chat-DsAg01Zn.js} +1 -1
- package/src/assets/web-panel/assets/{colors-6OVo4u3C.js → colors-B0oqAhrQ.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BH78Hlsq.js → compact-item-CmX0Rabz.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CFRpSbbC.js → createContext-Be4cn1Ub.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CP2qzG6D.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DNQhgcVi.js → hasIn-D5v12kVq.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkN3iTTp.js → index-0eWLkQKT.js} +1 -1
- package/src/assets/web-panel/assets/{index-CkNEMANR.js → index-301ogenv.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3RtTT4K.js → index-5LGY0JbG.js} +1 -1
- package/src/assets/web-panel/assets/{index-dDkoL4Pe.js → index-7VKDn-t1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHa4WyTZ.js → index-8WvYGnON.js} +1 -1
- package/src/assets/web-panel/assets/{index-CaYqY35Z.js → index-B1YGjOWC.js} +1 -1
- package/src/assets/web-panel/assets/{index-bKm3TUDp.js → index-B3mUHuVc.js} +1 -1
- package/src/assets/web-panel/assets/{index--rq90rL9.js → index-BA4oulaP.js} +1 -1
- package/src/assets/web-panel/assets/{index-DaDgdJYj.js → index-BBREnn1V.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_Z-YoxX.js → index-BBvyHguy.js} +1 -1
- package/src/assets/web-panel/assets/{index-CkqHgG-b.js → index-BKmsS2jj.js} +1 -1
- package/src/assets/web-panel/assets/{index-CrcLHN2F.js → index-BRIqLy1m.js} +1 -1
- package/src/assets/web-panel/assets/{index-BaVE6E8y.js → index-BXGSbelg.js} +3 -3
- package/src/assets/web-panel/assets/{index-BYrnm5Ob.js → index-BaE0Ui07.js} +1 -1
- package/src/assets/web-panel/assets/{index-BDSR7ecX.js → index-BoAIAZ5C.js} +1 -1
- package/src/assets/web-panel/assets/{index-CNuCHHQT.js → index-BqltWojQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Eag8e_mA.js → index-CGiIyTZw.js} +1 -1
- package/src/assets/web-panel/assets/index-CKB0vbJU.js +1 -0
- package/src/assets/web-panel/assets/{index-BepBTpa8.js → index-CZ2wUEgr.js} +1 -1
- package/src/assets/web-panel/assets/{index-HNGGVF6o.js → index-CZpfzcuy.js} +1 -1
- package/src/assets/web-panel/assets/{index-AuBhLqOm.js → index-Ckrk3pIV.js} +1 -1
- package/src/assets/web-panel/assets/{index-7TC3S8SN.js → index-CqoCZ5vR.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFPJ8yzF.js → index-CyWBir7N.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bb6kDrUx.js → index-CyyGTMKd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKN2noIF.js → index-D0BSkxYQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Du0rnksC.js → index-D5YVopyr.js} +1 -1
- package/src/assets/web-panel/assets/{index-BxRy5LPR.js → index-DKUBj36_.js} +1 -1
- package/src/assets/web-panel/assets/{index-sm7W8AXy.js → index-DM724NgU.js} +1 -1
- package/src/assets/web-panel/assets/{index-BujsOZfc.js → index-DQRsQen0.js} +1 -1
- package/src/assets/web-panel/assets/index-DgoXmwqB.js +1 -0
- package/src/assets/web-panel/assets/{index-Y9MJv28e.js → index-DikJ0Qqe.js} +1 -1
- package/src/assets/web-panel/assets/{index-caye7AOE.js → index-Dn2dSdXx.js} +1 -1
- package/src/assets/web-panel/assets/{index-cdf7fd7d.js → index-DyUaFWEk.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnnXQot_.js → index-Hfe_dEGx.js} +1 -1
- package/src/assets/web-panel/assets/{index-DvHGtNnF.js → index-_JjRLSD0.js} +1 -1
- package/src/assets/web-panel/assets/{index-nbfHl1oV.js → index-frSojpHj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cu3L4r5l.js → index-jK3ax6Z6.js} +1 -1
- package/src/assets/web-panel/assets/{index-D84id2Y4.js → index-qMuZ-NRR.js} +1 -1
- package/src/assets/web-panel/assets/{index-hPY0fFrd.js → index-q_fQ94SE.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BZ9w6jPL.js → initDefaultProps-D4VdhJ9_.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BnwUiOho.js → motion-CZJA0MQW.js} +1 -1
- package/src/assets/web-panel/assets/{move-ByUKJnNm.js → move-BqEs7Sb1.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-V7yK1KEl.js → mtc-parser-BpHEtsu1.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BEXBBt4j.js → omit-BW94tY9W.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-qDm_46Gi.js → pickAttrs-BnBjPWP9.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BIbhEo_0.js → placementArrow-uUkjQ_hu.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-NbPbDgA_.js → responsiveObserve-B-klUqXK.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BnUWglZ0.js → slide-CvY1cNAs.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BX8_wZI5.js → statusUtils-Ix5sEDT2.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BV4OycQX.js → styleChecker-C8qcXOne.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-Od_H5llY.js → useFlexGapSupport-KNF6Kj8f.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BWI-PyiX.js → useFs-DGJr345n.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BqMuhG3g.js → usePersonalDataHub-BboN99rI.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DNd23z6f.js → vnode-Bgt8Vgte.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-Cjaxnm7l.js → zoom-BhJtErgt.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +2 -2
- package/src/commands/agenda.js +6 -5
- package/src/commands/agent.js +237 -75
- package/src/commands/agents.js +6 -0
- package/src/commands/artifacts.js +98 -0
- package/src/commands/background-session.js +11 -1
- package/src/commands/mcp.js +218 -31
- package/src/commands/review.js +221 -22
- package/src/commands/session-mcp-recovery.js +5 -5
- package/src/data/changelog.json +27 -1
- package/src/gateways/ws/ws-agent-handler.js +492 -14
- package/src/harness/jsonl-session-store.js +39 -1
- package/src/harness/mcp-client.js +414 -71
- package/src/harness/prompt-compressor.js +24 -0
- package/src/harness/provider-backed-compaction.js +205 -0
- package/src/harness/worktree-isolator.js +26 -3
- package/src/lib/agent-worktree.js +41 -2
- package/src/lib/app-preview.js +3 -0
- package/src/lib/background-agent-supervisor.js +790 -186
- package/src/lib/background-command-argv.js +318 -0
- package/src/lib/background-launch-profile.js +1 -0
- package/src/lib/background-phase-reporter.js +22 -2
- package/src/lib/background-worktree-policy.js +109 -0
- package/src/lib/command-lifecycle-report.js +1340 -145
- package/src/lib/cowork-mcp-tools.js +51 -6
- package/src/lib/cowork-task-runner.js +1 -0
- package/src/lib/delivery-coordinator.js +30 -5
- package/src/lib/delivery-production-adapter.js +1777 -0
- package/src/lib/delivery-production-runner.js +240 -0
- package/src/lib/durable-security-store.js +29 -17
- package/src/lib/execution-trace/trace-context.cjs +179 -0
- package/src/lib/execution-trace/trace-context.js +8 -175
- package/src/lib/mcp-call-ledger-store.js +58 -6
- package/src/lib/mcp-headers-helper.js +76 -8
- package/src/lib/mcp-host-recovery-runtime.js +2 -0
- package/src/lib/mcp-ledger-recovery-admission.js +39 -3
- package/src/lib/mcp-recovery-adjudication.js +256 -19
- package/src/lib/mcp-sandbox-policy.js +184 -0
- package/src/lib/mcp-stdio-capsule-builder-worker.cjs +495 -0
- package/src/lib/mcp-stdio-executable-identity.js +33 -2
- package/src/lib/mcp-stdio-execution-authority.js +163 -0
- package/src/lib/mcp-stdio-immutable-vfs-resolver.cjs +830 -0
- package/src/lib/mcp-stdio-package-materialization.js +721 -333
- package/src/lib/mcp-stdio-workspace-authority.js +252 -0
- package/src/lib/observability/index.js +2 -0
- package/src/lib/observability/otlp-exporter.js +2 -2
- package/src/lib/packer/pack-update-applier.js +8 -1
- package/src/lib/packer/pkg-config-generator.js +34 -0
- package/src/lib/plugin-runtime/sandbox-policy.js +42 -2
- package/src/lib/process-execution-broker/index.js +213 -109
- package/src/lib/process-execution-broker/platform-sandbox.js +1024 -169
- package/src/lib/process-tree-termination.js +17 -1
- package/src/lib/scheduler-kernel/contract.js +315 -0
- package/src/lib/scheduler-kernel/store.js +1345 -0
- package/src/lib/session-host-lease.js +403 -30
- package/src/lib/session-persistence-failure.js +4 -1
- package/src/lib/skill-mcp.js +65 -4
- package/src/lib/sub-agent-context.js +8 -0
- package/src/lib/web-fetch.js +4 -4
- package/src/repl/agent-repl.js +285 -38
- package/src/runtime/agent-core.js +1044 -90
- package/src/runtime/agent-runtime.js +61 -8
- package/src/runtime/headless-runner.js +260 -166
- package/src/runtime/headless-stream.js +254 -25
- package/src/runtime/mcp-config.js +235 -28
- package/src/workers/background-agent-worker.js +492 -71
- package/src/assets/web-panel/assets/Projects-08xUDZGt.js +0 -1
- package/src/assets/web-panel/assets/Providers-BGs47s1a.js +0 -1
- package/src/assets/web-panel/assets/Tasks-CEYFJwmr.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DEMblAIm.js +0 -1
- package/src/assets/web-panel/assets/index-BTvq-36D.js +0 -1
- package/src/assets/web-panel/assets/index-hUpvk6Jl.js +0 -1
|
@@ -18,6 +18,7 @@ import fs from "fs";
|
|
|
18
18
|
import path from "path";
|
|
19
19
|
import broker from "../lib/process-execution-broker/index.js";
|
|
20
20
|
import os from "os";
|
|
21
|
+
import { randomUUID } from "node:crypto";
|
|
21
22
|
import { isProxy } from "node:util/types";
|
|
22
23
|
import sharedCodingAgentPolicy from "./coding-agent-policy.cjs";
|
|
23
24
|
import sharedShellPolicy from "./coding-agent-shell-policy.cjs";
|
|
@@ -127,6 +128,10 @@ import {
|
|
|
127
128
|
} from "../harness/structured-handoff.js";
|
|
128
129
|
import { isMcpRpcError } from "../harness/mcp-client.js";
|
|
129
130
|
import { projectCanonicalResumeMessages } from "../lib/session-message-provenance.js";
|
|
131
|
+
import {
|
|
132
|
+
pathMatchesOpenedFileIdentitySync,
|
|
133
|
+
sameOpenedFileIdentity,
|
|
134
|
+
} from "../lib/packer/file-identity.js";
|
|
130
135
|
|
|
131
136
|
export { formatProviderHttpError };
|
|
132
137
|
|
|
@@ -847,6 +852,22 @@ async function requestInteractivePermission(
|
|
|
847
852
|
cwd,
|
|
848
853
|
confirmArgs,
|
|
849
854
|
) {
|
|
855
|
+
if (context.hermeticExecution === true) {
|
|
856
|
+
// The coordinator-owned exact scope is itself the complete mutation
|
|
857
|
+
// authority for this hermetic run. It may authorize a sensitive *allowed*
|
|
858
|
+
// file, but it never authorizes credential reads or any tool outside the
|
|
859
|
+
// exact file-tool ceiling (those were rejected by the pure preflight).
|
|
860
|
+
if (
|
|
861
|
+
context.fileMutationScope != null &&
|
|
862
|
+
GUARDED_FILE_MUTATION_TOOLS.has(name)
|
|
863
|
+
) {
|
|
864
|
+
return true;
|
|
865
|
+
}
|
|
866
|
+
const confirm = context.permissionConfirm || context.shellConfirm || null;
|
|
867
|
+
return typeof confirm === "function"
|
|
868
|
+
? Boolean(await confirm(confirmArgs))
|
|
869
|
+
: false;
|
|
870
|
+
}
|
|
850
871
|
const hooksV2 = await executeHooksV2Event(
|
|
851
872
|
"PermissionRequest",
|
|
852
873
|
{
|
|
@@ -1386,6 +1407,306 @@ const GUARDED_FILE_MUTATION_TOOLS = new Set([
|
|
|
1386
1407
|
...IDE_DIFF_EDIT_TOOLS,
|
|
1387
1408
|
"notebook_edit",
|
|
1388
1409
|
]);
|
|
1410
|
+
const EXACT_FILE_MUTATION_SCOPE_TOOL_NAMES = new Set([
|
|
1411
|
+
"read_file",
|
|
1412
|
+
"list_dir",
|
|
1413
|
+
"write_file",
|
|
1414
|
+
"edit_file",
|
|
1415
|
+
"edit_file_hashed",
|
|
1416
|
+
]);
|
|
1417
|
+
const EXACT_FILE_MUTATION_OPENED_STATE_FIELDS = Object.freeze([
|
|
1418
|
+
"mode",
|
|
1419
|
+
"nlink",
|
|
1420
|
+
"size",
|
|
1421
|
+
"mtimeNs",
|
|
1422
|
+
"ctimeNs",
|
|
1423
|
+
]);
|
|
1424
|
+
const EXACT_FILE_MUTATION_RENAME_STABLE_STATE_FIELDS = Object.freeze([
|
|
1425
|
+
"mode",
|
|
1426
|
+
"nlink",
|
|
1427
|
+
"size",
|
|
1428
|
+
"mtimeNs",
|
|
1429
|
+
]);
|
|
1430
|
+
const NORMALIZED_EXACT_FILE_MUTATION_SCOPES = new WeakSet();
|
|
1431
|
+
const EXACT_FILE_MUTATION_SCOPE_STATES = new WeakMap();
|
|
1432
|
+
const WINDOWS_RESERVED_PATH_SEGMENT =
|
|
1433
|
+
/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/iu;
|
|
1434
|
+
|
|
1435
|
+
function sameCanonicalPath(left, right) {
|
|
1436
|
+
return (
|
|
1437
|
+
typeof left === "string" &&
|
|
1438
|
+
typeof right === "string" &&
|
|
1439
|
+
path.relative(left, right) === "" &&
|
|
1440
|
+
path.relative(right, left) === ""
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function exactRepoRelativeFilePath(value) {
|
|
1445
|
+
const segments = typeof value === "string" ? value.split("/") : [];
|
|
1446
|
+
if (
|
|
1447
|
+
typeof value !== "string" ||
|
|
1448
|
+
value.length === 0 ||
|
|
1449
|
+
value.includes("\0") ||
|
|
1450
|
+
value.includes("\\") ||
|
|
1451
|
+
path.posix.isAbsolute(value) ||
|
|
1452
|
+
/^[A-Za-z]:/u.test(value) ||
|
|
1453
|
+
path.posix.normalize(value) !== value ||
|
|
1454
|
+
value === "." ||
|
|
1455
|
+
value === ".." ||
|
|
1456
|
+
value.startsWith("../") ||
|
|
1457
|
+
segments.some(
|
|
1458
|
+
(segment) =>
|
|
1459
|
+
segment === "" ||
|
|
1460
|
+
segment === "." ||
|
|
1461
|
+
segment === ".." ||
|
|
1462
|
+
/[<>:"|?*]/u.test(segment) ||
|
|
1463
|
+
[...segment].some((character) => character.charCodeAt(0) <= 0x1f) ||
|
|
1464
|
+
/[ .]$/u.test(segment) ||
|
|
1465
|
+
WINDOWS_RESERVED_PATH_SEGMENT.test(segment),
|
|
1466
|
+
)
|
|
1467
|
+
) {
|
|
1468
|
+
throw new Error(
|
|
1469
|
+
"path must be one portable canonical repo-relative file path",
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
return value;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function canonicalRealpath(candidate) {
|
|
1476
|
+
const realpath = fs.realpathSync.native || fs.realpathSync;
|
|
1477
|
+
return path.resolve(realpath(candidate));
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
function filesystemIdentity(stats) {
|
|
1481
|
+
const mtimeNs =
|
|
1482
|
+
stats.mtimeNs != null
|
|
1483
|
+
? String(stats.mtimeNs)
|
|
1484
|
+
: String(Math.trunc(Number(stats.mtimeMs) * 1_000_000));
|
|
1485
|
+
return Object.freeze({
|
|
1486
|
+
dev: String(stats.dev),
|
|
1487
|
+
ino: String(stats.ino),
|
|
1488
|
+
type: stats.isFile() ? "file" : stats.isDirectory() ? "directory" : "other",
|
|
1489
|
+
nlink: String(stats.nlink),
|
|
1490
|
+
size: String(stats.size),
|
|
1491
|
+
mtimeNs,
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function sameFilesystemObjectIdentity(left, right) {
|
|
1496
|
+
return Boolean(
|
|
1497
|
+
left &&
|
|
1498
|
+
right &&
|
|
1499
|
+
left.dev === right.dev &&
|
|
1500
|
+
left.ino === right.ino &&
|
|
1501
|
+
left.type === right.type,
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
function sameFilesystemIdentity(left, right, { content = false } = {}) {
|
|
1506
|
+
return Boolean(
|
|
1507
|
+
sameFilesystemObjectIdentity(left, right) &&
|
|
1508
|
+
left.nlink === right.nlink &&
|
|
1509
|
+
(!content || (left.size === right.size && left.mtimeNs === right.mtimeNs)),
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function lstatIdentity(candidate) {
|
|
1514
|
+
const stats = fs.lstatSync(candidate, { bigint: true });
|
|
1515
|
+
if (stats.isSymbolicLink()) {
|
|
1516
|
+
throw new Error(`filesystem alias is not allowed: ${candidate}`);
|
|
1517
|
+
}
|
|
1518
|
+
return { stats, identity: filesystemIdentity(stats) };
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function exactBindingState(canonicalRoot, relativePath, absolutePath) {
|
|
1522
|
+
const ancestors = [];
|
|
1523
|
+
let current = canonicalRoot;
|
|
1524
|
+
const root = lstatIdentity(current);
|
|
1525
|
+
if (!root.stats.isDirectory()) {
|
|
1526
|
+
throw new Error("mutation scope worktree root must be a directory");
|
|
1527
|
+
}
|
|
1528
|
+
ancestors.push(Object.freeze({ path: current, identity: root.identity }));
|
|
1529
|
+
const segments = relativePath.split("/");
|
|
1530
|
+
for (const segment of segments.slice(0, -1)) {
|
|
1531
|
+
current = path.join(current, segment);
|
|
1532
|
+
const ancestor = lstatIdentity(current);
|
|
1533
|
+
if (!ancestor.stats.isDirectory()) {
|
|
1534
|
+
throw new Error(`mutation scope parent is not a directory: ${current}`);
|
|
1535
|
+
}
|
|
1536
|
+
ancestors.push(
|
|
1537
|
+
Object.freeze({ path: current, identity: ancestor.identity }),
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
const target = lstatIdentity(absolutePath);
|
|
1541
|
+
if (!target.stats.isFile()) {
|
|
1542
|
+
throw new Error(
|
|
1543
|
+
`mutation scope target must be an existing regular file: ${relativePath}`,
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
if (target.stats.nlink !== 1n) {
|
|
1547
|
+
throw new Error(
|
|
1548
|
+
`mutation scope target must not have hard links: ${relativePath}`,
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
return {
|
|
1552
|
+
ancestors: Object.freeze(ancestors),
|
|
1553
|
+
fileIdentity: target.identity,
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function verifyExactScopeAncestors(state) {
|
|
1558
|
+
for (const ancestor of state.ancestors) {
|
|
1559
|
+
const current = lstatIdentity(ancestor.path);
|
|
1560
|
+
// Directory link counts are not portable identity fields. APFS counts all
|
|
1561
|
+
// directory entries, so creating our own staging file legitimately changes
|
|
1562
|
+
// nlink. Device + inode + directory type still fail closed on replacement.
|
|
1563
|
+
if (
|
|
1564
|
+
!current.stats.isDirectory() ||
|
|
1565
|
+
!sameFilesystemObjectIdentity(current.identity, ancestor.identity)
|
|
1566
|
+
) {
|
|
1567
|
+
throw new Error(
|
|
1568
|
+
`mutation scope ancestor changed identity: ${ancestor.path}`,
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
function verifyExactBindingState(scope, binding) {
|
|
1575
|
+
const state = EXACT_FILE_MUTATION_SCOPE_STATES.get(scope)?.get(
|
|
1576
|
+
binding.relativePath,
|
|
1577
|
+
);
|
|
1578
|
+
if (!state) {
|
|
1579
|
+
throw new Error("mutation scope identity state is unavailable");
|
|
1580
|
+
}
|
|
1581
|
+
verifyExactScopeAncestors(state);
|
|
1582
|
+
const current = lstatIdentity(binding.absolutePath);
|
|
1583
|
+
if (
|
|
1584
|
+
!current.stats.isFile() ||
|
|
1585
|
+
current.stats.nlink !== 1n ||
|
|
1586
|
+
!sameFilesystemIdentity(current.identity, state.fileIdentity, {
|
|
1587
|
+
content: true,
|
|
1588
|
+
})
|
|
1589
|
+
) {
|
|
1590
|
+
throw new Error(
|
|
1591
|
+
`mutation scope file changed identity or content: ${binding.relativePath}`,
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
return state;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* Bind an exact mutation scope to one canonical worktree and a fixed set of
|
|
1599
|
+
* repo-relative file identities. This is intentionally stricter than the
|
|
1600
|
+
* general workspace guard: aliases through symlinks/junctions are rejected,
|
|
1601
|
+
* even when they resolve to another location inside the same worktree.
|
|
1602
|
+
*/
|
|
1603
|
+
export function normalizeExactFileMutationScope(
|
|
1604
|
+
scope,
|
|
1605
|
+
{ cwd = process.cwd() } = {},
|
|
1606
|
+
) {
|
|
1607
|
+
if (scope == null) return null;
|
|
1608
|
+
if (NORMALIZED_EXACT_FILE_MUTATION_SCOPES.has(scope)) {
|
|
1609
|
+
const canonicalCwd = canonicalRealpath(cwd);
|
|
1610
|
+
if (!sameCanonicalPath(canonicalCwd, scope.worktreeRoot)) {
|
|
1611
|
+
throw new Error("mutation scope worktree does not match the run cwd");
|
|
1612
|
+
}
|
|
1613
|
+
return scope;
|
|
1614
|
+
}
|
|
1615
|
+
if (
|
|
1616
|
+
!scope ||
|
|
1617
|
+
typeof scope !== "object" ||
|
|
1618
|
+
Array.isArray(scope) ||
|
|
1619
|
+
scope.exact !== true
|
|
1620
|
+
) {
|
|
1621
|
+
throw new Error("mutation scope must be an exact object");
|
|
1622
|
+
}
|
|
1623
|
+
if (
|
|
1624
|
+
typeof scope.worktreeRoot !== "string" ||
|
|
1625
|
+
scope.worktreeRoot.length === 0 ||
|
|
1626
|
+
scope.worktreeRoot.includes("\0") ||
|
|
1627
|
+
!path.isAbsolute(scope.worktreeRoot) ||
|
|
1628
|
+
path.normalize(scope.worktreeRoot) !== scope.worktreeRoot
|
|
1629
|
+
) {
|
|
1630
|
+
throw new Error(
|
|
1631
|
+
"mutation scope worktree root must be canonical and absolute",
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
const canonicalRoot = canonicalRealpath(scope.worktreeRoot);
|
|
1635
|
+
if (!sameCanonicalPath(canonicalRoot, scope.worktreeRoot)) {
|
|
1636
|
+
throw new Error("mutation scope worktree root must be its real path");
|
|
1637
|
+
}
|
|
1638
|
+
if (!fs.statSync(canonicalRoot).isDirectory()) {
|
|
1639
|
+
throw new Error("mutation scope worktree root must be a directory");
|
|
1640
|
+
}
|
|
1641
|
+
const canonicalCwd = canonicalRealpath(cwd);
|
|
1642
|
+
if (!sameCanonicalPath(canonicalCwd, canonicalRoot)) {
|
|
1643
|
+
throw new Error("mutation scope worktree does not match the run cwd");
|
|
1644
|
+
}
|
|
1645
|
+
if (!Array.isArray(scope.allowedPaths) || scope.allowedPaths.length === 0) {
|
|
1646
|
+
throw new Error("mutation scope requires at least one allowed path");
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const allowedPaths = scope.allowedPaths.map(exactRepoRelativeFilePath);
|
|
1650
|
+
if (new Set(allowedPaths).size !== allowedPaths.length) {
|
|
1651
|
+
throw new Error("mutation scope allowed paths must be unique");
|
|
1652
|
+
}
|
|
1653
|
+
const bindings = allowedPaths.map((relativePath) => {
|
|
1654
|
+
const absolutePath = path.resolve(
|
|
1655
|
+
canonicalRoot,
|
|
1656
|
+
...relativePath.split("/"),
|
|
1657
|
+
);
|
|
1658
|
+
const verdict = resolveSandboxPolicyPath(relativePath, {
|
|
1659
|
+
access: "write",
|
|
1660
|
+
cwd: canonicalRoot,
|
|
1661
|
+
workspaceRoots: [canonicalRoot],
|
|
1662
|
+
sandbox: null,
|
|
1663
|
+
});
|
|
1664
|
+
if (!verdict.ok) {
|
|
1665
|
+
throw new Error(
|
|
1666
|
+
`mutation scope path "${relativePath}" is unsafe: ${verdict.error}`,
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
if (
|
|
1670
|
+
!sameCanonicalPath(verdict.path, absolutePath) ||
|
|
1671
|
+
!sameCanonicalPath(verdict.canonicalPath, absolutePath)
|
|
1672
|
+
) {
|
|
1673
|
+
throw new Error(
|
|
1674
|
+
`mutation scope path "${relativePath}" traverses a filesystem alias`,
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
if (!fs.existsSync(absolutePath)) {
|
|
1678
|
+
throw new Error(
|
|
1679
|
+
`mutation scope target must already exist: ${relativePath}`,
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
return Object.freeze({
|
|
1683
|
+
relativePath,
|
|
1684
|
+
absolutePath,
|
|
1685
|
+
canonicalPath: verdict.canonicalPath,
|
|
1686
|
+
});
|
|
1687
|
+
});
|
|
1688
|
+
const normalized = Object.freeze({
|
|
1689
|
+
exact: true,
|
|
1690
|
+
worktreeRoot: canonicalRoot,
|
|
1691
|
+
allowedPaths: Object.freeze([...allowedPaths]),
|
|
1692
|
+
bindings: Object.freeze(bindings),
|
|
1693
|
+
});
|
|
1694
|
+
NORMALIZED_EXACT_FILE_MUTATION_SCOPES.add(normalized);
|
|
1695
|
+
EXACT_FILE_MUTATION_SCOPE_STATES.set(
|
|
1696
|
+
normalized,
|
|
1697
|
+
new Map(
|
|
1698
|
+
bindings.map((binding) => [
|
|
1699
|
+
binding.relativePath,
|
|
1700
|
+
exactBindingState(
|
|
1701
|
+
canonicalRoot,
|
|
1702
|
+
binding.relativePath,
|
|
1703
|
+
binding.absolutePath,
|
|
1704
|
+
),
|
|
1705
|
+
]),
|
|
1706
|
+
),
|
|
1707
|
+
);
|
|
1708
|
+
return normalized;
|
|
1709
|
+
}
|
|
1389
1710
|
|
|
1390
1711
|
function agentFileToolPathRequests(name, args = {}, workspaceRoots = []) {
|
|
1391
1712
|
const one = (argument, access) =>
|
|
@@ -1477,6 +1798,129 @@ function fileMutationPaths(name, args = {}) {
|
|
|
1477
1798
|
].filter(Boolean);
|
|
1478
1799
|
}
|
|
1479
1800
|
|
|
1801
|
+
function guardExactFileMutationScope(name, args, context, cwd) {
|
|
1802
|
+
if (context.fileMutationScope == null) {
|
|
1803
|
+
return null;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
if (!EXACT_FILE_MUTATION_SCOPE_TOOL_NAMES.has(name)) {
|
|
1807
|
+
return {
|
|
1808
|
+
error: `[File Mutation Scope] Tool "${name}" is outside the hermetic file-tool ceiling.`,
|
|
1809
|
+
policy: {
|
|
1810
|
+
decision: "deny",
|
|
1811
|
+
via: "exact-file-mutation-scope",
|
|
1812
|
+
reason: "tool-not-allowed",
|
|
1813
|
+
},
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
if (!GUARDED_FILE_MUTATION_TOOLS.has(name)) return null;
|
|
1817
|
+
|
|
1818
|
+
let scope;
|
|
1819
|
+
try {
|
|
1820
|
+
scope = normalizeExactFileMutationScope(context.fileMutationScope, { cwd });
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
return {
|
|
1823
|
+
error: `[File Mutation Scope] Invalid exact scope: ${error.message}.`,
|
|
1824
|
+
policy: {
|
|
1825
|
+
decision: "deny",
|
|
1826
|
+
via: "exact-file-mutation-scope",
|
|
1827
|
+
reason: "invalid-scope",
|
|
1828
|
+
},
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
const requestedPaths = fileMutationPaths(name, args);
|
|
1833
|
+
if (requestedPaths.length === 0) {
|
|
1834
|
+
return {
|
|
1835
|
+
error: `[File Mutation Scope] ${name} did not provide a mutation path.`,
|
|
1836
|
+
policy: {
|
|
1837
|
+
decision: "deny",
|
|
1838
|
+
via: "exact-file-mutation-scope",
|
|
1839
|
+
reason: "invalid-path",
|
|
1840
|
+
},
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
for (const requestedPath of requestedPaths) {
|
|
1845
|
+
let relativePath;
|
|
1846
|
+
try {
|
|
1847
|
+
relativePath = exactRepoRelativeFilePath(requestedPath);
|
|
1848
|
+
} catch (error) {
|
|
1849
|
+
return {
|
|
1850
|
+
error:
|
|
1851
|
+
`[File Mutation Scope] ${name} path "${requestedPath}" was blocked: ` +
|
|
1852
|
+
`${error.message}.`,
|
|
1853
|
+
policy: {
|
|
1854
|
+
decision: "deny",
|
|
1855
|
+
via: "exact-file-mutation-scope",
|
|
1856
|
+
reason: "non-canonical-path",
|
|
1857
|
+
},
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
const binding = scope.bindings.find(
|
|
1861
|
+
(candidate) => candidate.relativePath === relativePath,
|
|
1862
|
+
);
|
|
1863
|
+
if (!binding) {
|
|
1864
|
+
return {
|
|
1865
|
+
error: `[File Mutation Scope] ${name} path "${requestedPath}" is not in the exact allowed file set.`,
|
|
1866
|
+
policy: {
|
|
1867
|
+
decision: "deny",
|
|
1868
|
+
via: "exact-file-mutation-scope",
|
|
1869
|
+
reason: "path-not-allowed",
|
|
1870
|
+
},
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
const verdict = resolveSandboxPolicyPath(relativePath, {
|
|
1874
|
+
access: "write",
|
|
1875
|
+
cwd: scope.worktreeRoot,
|
|
1876
|
+
workspaceRoots: [scope.worktreeRoot],
|
|
1877
|
+
sandbox: null,
|
|
1878
|
+
});
|
|
1879
|
+
if (
|
|
1880
|
+
!verdict.ok ||
|
|
1881
|
+
!sameCanonicalPath(verdict.path, binding.absolutePath) ||
|
|
1882
|
+
!sameCanonicalPath(verdict.canonicalPath, binding.canonicalPath) ||
|
|
1883
|
+
!sameCanonicalPath(verdict.canonicalPath, binding.absolutePath)
|
|
1884
|
+
) {
|
|
1885
|
+
return {
|
|
1886
|
+
error: `[File Mutation Scope] ${name} path "${requestedPath}" changed physical identity or escaped its exact binding.`,
|
|
1887
|
+
policy: {
|
|
1888
|
+
decision: "deny",
|
|
1889
|
+
via: "exact-file-mutation-scope",
|
|
1890
|
+
reason: verdict.ok ? "path-identity-changed" : verdict.reason,
|
|
1891
|
+
},
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
try {
|
|
1895
|
+
verifyExactBindingState(scope, binding);
|
|
1896
|
+
} catch (error) {
|
|
1897
|
+
return {
|
|
1898
|
+
error: `[File Mutation Scope] ${name} path "${requestedPath}" was blocked: ${error.message}.`,
|
|
1899
|
+
policy: {
|
|
1900
|
+
decision: "deny",
|
|
1901
|
+
via: "exact-file-mutation-scope",
|
|
1902
|
+
reason: "path-identity-changed",
|
|
1903
|
+
},
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return null;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
export function preflightToolExecutionAuthority(name, args, context = {}) {
|
|
1911
|
+
const cwd = context.cwd || process.cwd();
|
|
1912
|
+
if (
|
|
1913
|
+
Array.isArray(context.effectiveAllowedToolNames) &&
|
|
1914
|
+
!context.effectiveAllowedToolNames.includes(name)
|
|
1915
|
+
) {
|
|
1916
|
+
return {
|
|
1917
|
+
error: `[Tool Capability] Tool "${name}" is outside this run's effective tool set.`,
|
|
1918
|
+
policy: { decision: "blocked", via: "effective-tool-set" },
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
return guardExactFileMutationScope(name, args, context, cwd);
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1480
1924
|
/**
|
|
1481
1925
|
* Quote-aware shell tokenizer (mirrors gateways/ws `tokenizeCommand`). The `git`
|
|
1482
1926
|
* tool runs the model-supplied command via argv (spawnSync, NO shell), so a
|
|
@@ -1795,19 +2239,14 @@ export async function executeTool(name, args, context = {}) {
|
|
|
1795
2239
|
const skillLoader = context.skillLoader || _defaultSkillLoader;
|
|
1796
2240
|
const cwd = context.cwd || process.cwd();
|
|
1797
2241
|
const planManager = context.planManager || getPlanModeManager();
|
|
1798
|
-
// The provider receives a filtered
|
|
1799
|
-
//
|
|
1800
|
-
//
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
)
|
|
1806
|
-
return {
|
|
1807
|
-
error: `[Tool Capability] Tool "${name}" is outside this run's effective tool set.`,
|
|
1808
|
-
policy: { decision: "blocked", via: "effective-tool-set" },
|
|
1809
|
-
};
|
|
1810
|
-
}
|
|
2242
|
+
// The provider receives a filtered schema, but an untrusted/buggy model can
|
|
2243
|
+
// still emit an arbitrary tool_call. Reuse the same pure preflight that the
|
|
2244
|
+
// outer loop runs before checkpoints or execution events.
|
|
2245
|
+
const authorityDenial = preflightToolExecutionAuthority(name, args, {
|
|
2246
|
+
...context,
|
|
2247
|
+
cwd,
|
|
2248
|
+
});
|
|
2249
|
+
if (authorityDenial) return authorityDenial;
|
|
1811
2250
|
const localToolDescriptor =
|
|
1812
2251
|
context.externalToolDescriptors &&
|
|
1813
2252
|
typeof context.externalToolDescriptors === "object"
|
|
@@ -1936,7 +2375,8 @@ export async function executeTool(name, args, context = {}) {
|
|
|
1936
2375
|
};
|
|
1937
2376
|
|
|
1938
2377
|
// Persona toolsDisabled guard
|
|
1939
|
-
const persona =
|
|
2378
|
+
const persona =
|
|
2379
|
+
context.hermeticExecution === true ? null : _loadProjectPersona(cwd);
|
|
1940
2380
|
if (persona?.toolsDisabled?.includes(name)) {
|
|
1941
2381
|
return {
|
|
1942
2382
|
error: `Tool "${name}" is disabled by project persona configuration.`,
|
|
@@ -2320,7 +2760,7 @@ export async function executeTool(name, args, context = {}) {
|
|
|
2320
2760
|
// capable: a `block` (exit 2 / {decision:block}) stops the tool here, an
|
|
2321
2761
|
// `ask` routes to the confirmer. Runs after permission resolution so a
|
|
2322
2762
|
// settings deny / host deny short-circuits before any hook process spawns.
|
|
2323
|
-
if (!planReadOnlyFenceActive) {
|
|
2763
|
+
if (!planReadOnlyFenceActive && context.hermeticExecution !== true) {
|
|
2324
2764
|
const hooksV2Pre = await executeHooksV2Event(
|
|
2325
2765
|
"PreToolUse",
|
|
2326
2766
|
{
|
|
@@ -2427,6 +2867,7 @@ export async function executeTool(name, args, context = {}) {
|
|
|
2427
2867
|
mcpHostClient: context.mcpHostClient || context.mcpClient || null,
|
|
2428
2868
|
mcpCallLedger: context.mcpCallLedger || null,
|
|
2429
2869
|
mcpConflictScheduler: context.mcpConflictScheduler || null,
|
|
2870
|
+
mcpDispatchAdmission: context.mcpDispatchAdmission || null,
|
|
2430
2871
|
subtreeInstructionScope:
|
|
2431
2872
|
context.subtreeInstructionScope || context.sessionId || "__legacy__",
|
|
2432
2873
|
shellPolicyOverrides: context.shellPolicyOverrides || null,
|
|
@@ -2453,9 +2894,15 @@ export async function executeTool(name, args, context = {}) {
|
|
|
2453
2894
|
toolAdmission: context.toolAdmission || null,
|
|
2454
2895
|
unattendedActionPolicy: context.unattendedActionPolicy || null,
|
|
2455
2896
|
managedCheckpoint: context.managedCheckpoint === true,
|
|
2897
|
+
fileMutationScope: context.fileMutationScope || null,
|
|
2898
|
+
hermeticExecution: context.hermeticExecution === true,
|
|
2456
2899
|
});
|
|
2457
2900
|
} catch (err) {
|
|
2458
|
-
if (
|
|
2901
|
+
if (
|
|
2902
|
+
hookDb &&
|
|
2903
|
+
!planReadOnlyFenceActive &&
|
|
2904
|
+
context.hermeticExecution !== true
|
|
2905
|
+
) {
|
|
2459
2906
|
try {
|
|
2460
2907
|
await executeHooks(hookDb, HookEvents.ToolError, {
|
|
2461
2908
|
tool: name,
|
|
@@ -2486,7 +2933,11 @@ export async function executeTool(name, args, context = {}) {
|
|
|
2486
2933
|
}
|
|
2487
2934
|
|
|
2488
2935
|
// PostToolUse hook
|
|
2489
|
-
if (
|
|
2936
|
+
if (
|
|
2937
|
+
hookDb &&
|
|
2938
|
+
!planReadOnlyFenceActive &&
|
|
2939
|
+
context.hermeticExecution !== true
|
|
2940
|
+
) {
|
|
2490
2941
|
try {
|
|
2491
2942
|
await executeHooks(hookDb, HookEvents.PostToolUse, {
|
|
2492
2943
|
tool: name,
|
|
@@ -2506,6 +2957,7 @@ export async function executeTool(name, args, context = {}) {
|
|
|
2506
2957
|
// reason is attached as `hookFeedback` to be surfaced back to the model.
|
|
2507
2958
|
if (
|
|
2508
2959
|
!planReadOnlyFenceActive &&
|
|
2960
|
+
context.hermeticExecution !== true &&
|
|
2509
2961
|
context.settingsHooks &&
|
|
2510
2962
|
toolResult &&
|
|
2511
2963
|
typeof toolResult === "object"
|
|
@@ -2691,6 +3143,196 @@ export function writeFileVerified(filePath, content, fsImpl = fs) {
|
|
|
2691
3143
|
return { size: actual };
|
|
2692
3144
|
}
|
|
2693
3145
|
|
|
3146
|
+
function writeFileVerifiedWithinExactScope(
|
|
3147
|
+
fileMutationScope,
|
|
3148
|
+
requestedPath,
|
|
3149
|
+
content,
|
|
3150
|
+
cwd,
|
|
3151
|
+
) {
|
|
3152
|
+
let scope;
|
|
3153
|
+
let relativePath;
|
|
3154
|
+
let binding;
|
|
3155
|
+
let state;
|
|
3156
|
+
try {
|
|
3157
|
+
scope = normalizeExactFileMutationScope(fileMutationScope, { cwd });
|
|
3158
|
+
relativePath = exactRepoRelativeFilePath(requestedPath);
|
|
3159
|
+
binding = scope.bindings.find(
|
|
3160
|
+
(candidate) => candidate.relativePath === relativePath,
|
|
3161
|
+
);
|
|
3162
|
+
if (!binding) throw new Error("path is not in the exact allowed file set");
|
|
3163
|
+
state = verifyExactBindingState(scope, binding);
|
|
3164
|
+
} catch (error) {
|
|
3165
|
+
return {
|
|
3166
|
+
error: `[File Mutation Scope] Bound write refused: ${error.message}`,
|
|
3167
|
+
policy: {
|
|
3168
|
+
decision: "deny",
|
|
3169
|
+
via: "exact-file-mutation-scope",
|
|
3170
|
+
reason: "path-identity-changed",
|
|
3171
|
+
},
|
|
3172
|
+
};
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
const expected = Buffer.byteLength(content, "utf8");
|
|
3176
|
+
let descriptor;
|
|
3177
|
+
let temporaryPath;
|
|
3178
|
+
let temporaryIdentity;
|
|
3179
|
+
let replacementCommitted = false;
|
|
3180
|
+
try {
|
|
3181
|
+
// Never truncate the bound inode: it may be renamed outside the authorized
|
|
3182
|
+
// worktree after the guard. Stage new bytes in the same verified directory
|
|
3183
|
+
// and replace only the authorized pathname atomically. A concurrent rename
|
|
3184
|
+
// can move the old inode, but cannot turn this write into a mutation of it.
|
|
3185
|
+
const source = lstatIdentity(binding.absolutePath);
|
|
3186
|
+
if (
|
|
3187
|
+
!source.stats.isFile() ||
|
|
3188
|
+
source.stats.nlink !== 1n ||
|
|
3189
|
+
!sameFilesystemIdentity(source.identity, state.fileIdentity, {
|
|
3190
|
+
content: true,
|
|
3191
|
+
})
|
|
3192
|
+
) {
|
|
3193
|
+
throw new Error("source file no longer matches the bound identity");
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
const directory = path.dirname(binding.absolutePath);
|
|
3197
|
+
temporaryPath = path.join(
|
|
3198
|
+
directory,
|
|
3199
|
+
`.chainlesschain-fix-${process.pid}-${randomUUID()}.tmp`,
|
|
3200
|
+
);
|
|
3201
|
+
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
3202
|
+
descriptor = fs.openSync(
|
|
3203
|
+
temporaryPath,
|
|
3204
|
+
fs.constants.O_WRONLY |
|
|
3205
|
+
fs.constants.O_CREAT |
|
|
3206
|
+
fs.constants.O_EXCL |
|
|
3207
|
+
noFollow,
|
|
3208
|
+
Number(source.stats.mode & 0o777n),
|
|
3209
|
+
);
|
|
3210
|
+
const openedTemporary = fs.fstatSync(descriptor, { bigint: true });
|
|
3211
|
+
temporaryIdentity = filesystemIdentity(openedTemporary);
|
|
3212
|
+
if (!openedTemporary.isFile() || openedTemporary.nlink !== 1n) {
|
|
3213
|
+
throw new Error("staged file did not open as one regular file");
|
|
3214
|
+
}
|
|
3215
|
+
if (process.platform !== "win32") {
|
|
3216
|
+
fs.fchmodSync(descriptor, Number(source.stats.mode & 0o777n));
|
|
3217
|
+
}
|
|
3218
|
+
fs.writeFileSync(descriptor, content, { encoding: "utf8" });
|
|
3219
|
+
fs.fsyncSync(descriptor);
|
|
3220
|
+
const staged = fs.fstatSync(descriptor, { bigint: true });
|
|
3221
|
+
const stagedIdentity = filesystemIdentity(staged);
|
|
3222
|
+
if (
|
|
3223
|
+
!staged.isFile() ||
|
|
3224
|
+
staged.nlink !== 1n ||
|
|
3225
|
+
!sameFilesystemIdentity(stagedIdentity, temporaryIdentity) ||
|
|
3226
|
+
staged.size !== BigInt(expected)
|
|
3227
|
+
) {
|
|
3228
|
+
throw new Error("staged write verification failed");
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
// Revalidate both the original binding and the staged pathname immediately
|
|
3232
|
+
// before the atomic replacement. O_EXCL plus the descriptor/path identity
|
|
3233
|
+
// comparison prevents a pre-created alias from being substituted here.
|
|
3234
|
+
verifyExactBindingState(scope, binding);
|
|
3235
|
+
const stagedPath = lstatIdentity(temporaryPath);
|
|
3236
|
+
const stagedPathMatches =
|
|
3237
|
+
process.platform === "win32"
|
|
3238
|
+
? pathMatchesOpenedFileIdentitySync(temporaryPath, staged, {
|
|
3239
|
+
stateFields: EXACT_FILE_MUTATION_OPENED_STATE_FIELDS,
|
|
3240
|
+
})
|
|
3241
|
+
: sameFilesystemIdentity(stagedPath.identity, stagedIdentity, {
|
|
3242
|
+
content: true,
|
|
3243
|
+
});
|
|
3244
|
+
if (
|
|
3245
|
+
!stagedPath.stats.isFile() ||
|
|
3246
|
+
stagedPath.stats.nlink !== 1n ||
|
|
3247
|
+
!stagedPathMatches
|
|
3248
|
+
) {
|
|
3249
|
+
throw new Error("staged file path changed identity or content");
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
fs.renameSync(temporaryPath, binding.absolutePath);
|
|
3253
|
+
replacementCommitted = true;
|
|
3254
|
+
temporaryPath = undefined;
|
|
3255
|
+
|
|
3256
|
+
const installed = lstatIdentity(binding.absolutePath);
|
|
3257
|
+
verifyExactScopeAncestors(state);
|
|
3258
|
+
const installedWriter =
|
|
3259
|
+
process.platform === "win32"
|
|
3260
|
+
? fs.fstatSync(descriptor, { bigint: true })
|
|
3261
|
+
: staged;
|
|
3262
|
+
const installedPathMatches =
|
|
3263
|
+
process.platform === "win32"
|
|
3264
|
+
? installedWriter.isFile() &&
|
|
3265
|
+
installedWriter.nlink === 1n &&
|
|
3266
|
+
sameOpenedFileIdentity(
|
|
3267
|
+
staged,
|
|
3268
|
+
installedWriter,
|
|
3269
|
+
EXACT_FILE_MUTATION_RENAME_STABLE_STATE_FIELDS,
|
|
3270
|
+
) &&
|
|
3271
|
+
pathMatchesOpenedFileIdentitySync(
|
|
3272
|
+
binding.absolutePath,
|
|
3273
|
+
installedWriter,
|
|
3274
|
+
{
|
|
3275
|
+
stateFields: EXACT_FILE_MUTATION_OPENED_STATE_FIELDS,
|
|
3276
|
+
},
|
|
3277
|
+
)
|
|
3278
|
+
: sameFilesystemIdentity(installed.identity, stagedIdentity, {
|
|
3279
|
+
content: true,
|
|
3280
|
+
});
|
|
3281
|
+
if (
|
|
3282
|
+
!installed.stats.isFile() ||
|
|
3283
|
+
installed.stats.nlink !== 1n ||
|
|
3284
|
+
!installedPathMatches ||
|
|
3285
|
+
installed.stats.size !== BigInt(expected)
|
|
3286
|
+
) {
|
|
3287
|
+
throw new Error("installed replacement failed identity verification");
|
|
3288
|
+
}
|
|
3289
|
+
state.fileIdentity = installed.identity;
|
|
3290
|
+
|
|
3291
|
+
// POSIX needs a directory fsync to durably publish the rename. Windows
|
|
3292
|
+
// does not support fsync on directory handles through Node.
|
|
3293
|
+
if (process.platform !== "win32") {
|
|
3294
|
+
const directoryDescriptor = fs.openSync(directory, fs.constants.O_RDONLY);
|
|
3295
|
+
try {
|
|
3296
|
+
fs.fsyncSync(directoryDescriptor);
|
|
3297
|
+
} finally {
|
|
3298
|
+
fs.closeSync(directoryDescriptor);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
return { size: Number(installed.stats.size) };
|
|
3302
|
+
} catch (error) {
|
|
3303
|
+
return {
|
|
3304
|
+
error: `[File Mutation Scope] Bound write failed: ${error.message}`,
|
|
3305
|
+
...(replacementCommitted ? { outcomeUnknown: true } : {}),
|
|
3306
|
+
...(temporaryPath
|
|
3307
|
+
? {
|
|
3308
|
+
cleanupRequired: true,
|
|
3309
|
+
unsettledStage: path.basename(temporaryPath),
|
|
3310
|
+
}
|
|
3311
|
+
: {}),
|
|
3312
|
+
policy: {
|
|
3313
|
+
decision: "deny",
|
|
3314
|
+
via: "exact-file-mutation-scope",
|
|
3315
|
+
reason: replacementCommitted
|
|
3316
|
+
? "bound-write-outcome-unknown"
|
|
3317
|
+
: "bound-write-failed",
|
|
3318
|
+
},
|
|
3319
|
+
};
|
|
3320
|
+
} finally {
|
|
3321
|
+
if (descriptor !== undefined) {
|
|
3322
|
+
try {
|
|
3323
|
+
fs.closeSync(descriptor);
|
|
3324
|
+
} catch {
|
|
3325
|
+
/* the write result already records the authoritative failure */
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
// Never unlink a failed staging pathname here. Another process can replace
|
|
3329
|
+
// either that entry or its parent between any identity check and unlink;
|
|
3330
|
+
// preserving an explicitly reported orphan is safer than deleting an
|
|
3331
|
+
// unknown file. A separately authorized identity-aware maintenance pass
|
|
3332
|
+
// may reconcile it later.
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
|
|
2694
3336
|
// ── Edit-concurrency (read-freshness) guard ──────────────────────────────
|
|
2695
3337
|
//
|
|
2696
3338
|
// Per-session map: resolved file path → the mtimeMs the agent last OBSERVED for
|
|
@@ -3592,8 +4234,10 @@ async function executeToolInner(
|
|
|
3592
4234
|
externalToolExecutors,
|
|
3593
4235
|
extraToolDefinitions = null,
|
|
3594
4236
|
mcpClient,
|
|
4237
|
+
mcpHostClient = mcpClient,
|
|
3595
4238
|
mcpCallLedger = null,
|
|
3596
4239
|
mcpConflictScheduler = null,
|
|
4240
|
+
mcpDispatchAdmission = null,
|
|
3597
4241
|
subtreeInstructionScope = "__legacy__",
|
|
3598
4242
|
memoryDb = null,
|
|
3599
4243
|
permanentMemory = null,
|
|
@@ -3618,6 +4262,8 @@ async function executeToolInner(
|
|
|
3618
4262
|
toolAdmission = null,
|
|
3619
4263
|
unattendedActionPolicy = null,
|
|
3620
4264
|
managedCheckpoint = false,
|
|
4265
|
+
fileMutationScope = null,
|
|
4266
|
+
hermeticExecution = false,
|
|
3621
4267
|
// Hook-envelope tracing: this run's trace id, threaded into child loops
|
|
3622
4268
|
// (spawn_sub_agent / isolated run_skill) as their parent_id.
|
|
3623
4269
|
hookTraceId = null,
|
|
@@ -3758,6 +4404,13 @@ async function executeToolInner(
|
|
|
3758
4404
|
}
|
|
3759
4405
|
|
|
3760
4406
|
case "write_file": {
|
|
4407
|
+
const sinkDenial = guardExactFileMutationScope(
|
|
4408
|
+
name,
|
|
4409
|
+
args,
|
|
4410
|
+
{ fileMutationScope },
|
|
4411
|
+
cwd,
|
|
4412
|
+
);
|
|
4413
|
+
if (sinkDenial) return attachDescriptor(sinkDenial);
|
|
3761
4414
|
const filePath = path.resolve(cwd, args.path);
|
|
3762
4415
|
const dir = path.dirname(filePath);
|
|
3763
4416
|
// Overwriting an existing file: refuse if it changed on disk since the
|
|
@@ -3766,11 +4419,18 @@ async function executeToolInner(
|
|
|
3766
4419
|
const stale = _checkFileFreshness(filePath);
|
|
3767
4420
|
if (stale) return attachDescriptor({ error: stale });
|
|
3768
4421
|
}
|
|
3769
|
-
if (!fs.existsSync(dir)) {
|
|
4422
|
+
if (!fileMutationScope && !fs.existsSync(dir)) {
|
|
3770
4423
|
fs.mkdirSync(dir, { recursive: true });
|
|
3771
4424
|
}
|
|
3772
|
-
const wrote =
|
|
3773
|
-
|
|
4425
|
+
const wrote = fileMutationScope
|
|
4426
|
+
? writeFileVerifiedWithinExactScope(
|
|
4427
|
+
fileMutationScope,
|
|
4428
|
+
args.path,
|
|
4429
|
+
args.content,
|
|
4430
|
+
cwd,
|
|
4431
|
+
)
|
|
4432
|
+
: writeFileVerified(filePath, args.content);
|
|
4433
|
+
if (wrote.error) return attachDescriptor(wrote);
|
|
3774
4434
|
_recordFileObservation(filePath);
|
|
3775
4435
|
return attachDescriptor(
|
|
3776
4436
|
await _withSubtreeInstructions(
|
|
@@ -3779,7 +4439,7 @@ async function executeToolInner(
|
|
|
3779
4439
|
filePath,
|
|
3780
4440
|
cwd,
|
|
3781
4441
|
additionalDirectories,
|
|
3782
|
-
managedCheckpoint,
|
|
4442
|
+
managedCheckpoint || hermeticExecution,
|
|
3783
4443
|
),
|
|
3784
4444
|
filePath,
|
|
3785
4445
|
cwd,
|
|
@@ -3890,6 +4550,13 @@ async function executeToolInner(
|
|
|
3890
4550
|
}
|
|
3891
4551
|
|
|
3892
4552
|
case "edit_file": {
|
|
4553
|
+
const sinkDenial = guardExactFileMutationScope(
|
|
4554
|
+
name,
|
|
4555
|
+
args,
|
|
4556
|
+
{ fileMutationScope },
|
|
4557
|
+
cwd,
|
|
4558
|
+
);
|
|
4559
|
+
if (sinkDenial) return attachDescriptor(sinkDenial);
|
|
3893
4560
|
const filePath = path.resolve(cwd, args.path);
|
|
3894
4561
|
if (!fs.existsSync(filePath)) {
|
|
3895
4562
|
return attachDescriptor({ error: `File not found: ${filePath}` });
|
|
@@ -3947,8 +4614,15 @@ async function executeToolInner(
|
|
|
3947
4614
|
occurrences: count,
|
|
3948
4615
|
});
|
|
3949
4616
|
}
|
|
3950
|
-
const wrote =
|
|
3951
|
-
|
|
4617
|
+
const wrote = fileMutationScope
|
|
4618
|
+
? writeFileVerifiedWithinExactScope(
|
|
4619
|
+
fileMutationScope,
|
|
4620
|
+
args.path,
|
|
4621
|
+
newContent,
|
|
4622
|
+
cwd,
|
|
4623
|
+
)
|
|
4624
|
+
: writeFileVerified(filePath, newContent);
|
|
4625
|
+
if (wrote.error) return attachDescriptor(wrote);
|
|
3952
4626
|
_recordFileObservation(filePath);
|
|
3953
4627
|
return attachDescriptor(
|
|
3954
4628
|
await _withSubtreeInstructions(
|
|
@@ -3962,7 +4636,7 @@ async function executeToolInner(
|
|
|
3962
4636
|
filePath,
|
|
3963
4637
|
cwd,
|
|
3964
4638
|
additionalDirectories,
|
|
3965
|
-
managedCheckpoint,
|
|
4639
|
+
managedCheckpoint || hermeticExecution,
|
|
3966
4640
|
),
|
|
3967
4641
|
filePath,
|
|
3968
4642
|
cwd,
|
|
@@ -3972,6 +4646,13 @@ async function executeToolInner(
|
|
|
3972
4646
|
}
|
|
3973
4647
|
|
|
3974
4648
|
case "edit_file_hashed": {
|
|
4649
|
+
const sinkDenial = guardExactFileMutationScope(
|
|
4650
|
+
name,
|
|
4651
|
+
args,
|
|
4652
|
+
{ fileMutationScope },
|
|
4653
|
+
cwd,
|
|
4654
|
+
);
|
|
4655
|
+
if (sinkDenial) return attachDescriptor(sinkDenial);
|
|
3975
4656
|
// Hash-anchored edit (v5.0.2.9, inspired by oh-my-openagent).
|
|
3976
4657
|
// Reference a line by its content hash rather than line number or
|
|
3977
4658
|
// exact string — robust against whitespace drift and concurrent edits.
|
|
@@ -4012,8 +4693,15 @@ async function executeToolInner(
|
|
|
4012
4693
|
...(snippet && { current_snippet: snippet }),
|
|
4013
4694
|
});
|
|
4014
4695
|
}
|
|
4015
|
-
const wrote =
|
|
4016
|
-
|
|
4696
|
+
const wrote = fileMutationScope
|
|
4697
|
+
? writeFileVerifiedWithinExactScope(
|
|
4698
|
+
fileMutationScope,
|
|
4699
|
+
args.path,
|
|
4700
|
+
result.content,
|
|
4701
|
+
cwd,
|
|
4702
|
+
)
|
|
4703
|
+
: writeFileVerified(filePath, result.content);
|
|
4704
|
+
if (wrote.error) return attachDescriptor(wrote);
|
|
4017
4705
|
_recordFileObservation(filePath);
|
|
4018
4706
|
return attachDescriptor(
|
|
4019
4707
|
await _withSubtreeInstructions(
|
|
@@ -4028,7 +4716,7 @@ async function executeToolInner(
|
|
|
4028
4716
|
filePath,
|
|
4029
4717
|
cwd,
|
|
4030
4718
|
additionalDirectories,
|
|
4031
|
-
managedCheckpoint,
|
|
4719
|
+
managedCheckpoint || hermeticExecution,
|
|
4032
4720
|
),
|
|
4033
4721
|
filePath,
|
|
4034
4722
|
cwd,
|
|
@@ -5014,8 +5702,10 @@ async function executeToolInner(
|
|
|
5014
5702
|
// Parent MCP plumbing — a spawn can inherit these into the child,
|
|
5015
5703
|
// filtered by the resolved contract's mcpServers allow-list.
|
|
5016
5704
|
mcpClient,
|
|
5705
|
+
mcpHostClient,
|
|
5017
5706
|
mcpCallLedger,
|
|
5018
5707
|
mcpConflictScheduler,
|
|
5708
|
+
mcpDispatchAdmission,
|
|
5019
5709
|
externalToolDescriptors,
|
|
5020
5710
|
externalToolExecutors,
|
|
5021
5711
|
extraToolDefinitions,
|
|
@@ -6174,6 +6864,21 @@ async function executeToolInner(
|
|
|
6174
6864
|
});
|
|
6175
6865
|
}
|
|
6176
6866
|
|
|
6867
|
+
if (
|
|
6868
|
+
mcpDispatchAdmission !== null &&
|
|
6869
|
+
typeof mcpDispatchAdmission !== "function"
|
|
6870
|
+
) {
|
|
6871
|
+
return attachDescriptor({
|
|
6872
|
+
error:
|
|
6873
|
+
"MCP tool blocked because the host dispatch authority is invalid",
|
|
6874
|
+
policy: {
|
|
6875
|
+
decision: "blocked",
|
|
6876
|
+
via: "mcp-dispatch-admission",
|
|
6877
|
+
code: "CC_MCP_DISPATCH_ADMISSION_INVALID",
|
|
6878
|
+
},
|
|
6879
|
+
});
|
|
6880
|
+
}
|
|
6881
|
+
|
|
6177
6882
|
const schedulerScopes = mcpLedgerScopes(mcpWireInput);
|
|
6178
6883
|
const ledgerEffectContract = mcpLedgerEffectContract(
|
|
6179
6884
|
localToolDescriptor,
|
|
@@ -6233,9 +6938,12 @@ async function executeToolInner(
|
|
|
6233
6938
|
|
|
6234
6939
|
const transportFailure = async (callError, phase = "call") => {
|
|
6235
6940
|
const definitiveRpcFailure = isMcpRpcError(callError);
|
|
6941
|
+
const definitelyNotDispatched =
|
|
6942
|
+
safeMcpProperty(callError, "dispatched") === false;
|
|
6236
6943
|
if (
|
|
6237
6944
|
mcpTransportOutcomeIsUnsafe(ledgerEffectContract) &&
|
|
6238
|
-
!definitiveRpcFailure
|
|
6945
|
+
!definitiveRpcFailure &&
|
|
6946
|
+
!definitelyNotDispatched
|
|
6239
6947
|
) {
|
|
6240
6948
|
const resultAdmissionFailure =
|
|
6241
6949
|
isMcpToolResultAdmissionError(callError);
|
|
@@ -6281,7 +6989,14 @@ async function executeToolInner(
|
|
|
6281
6989
|
localToolExecutor.toolName,
|
|
6282
6990
|
mcpWireInput,
|
|
6283
6991
|
];
|
|
6284
|
-
if (signal)
|
|
6992
|
+
if (signal || mcpDispatchAdmission) {
|
|
6993
|
+
callArguments.push({
|
|
6994
|
+
...(signal ? { signal } : {}),
|
|
6995
|
+
...(mcpDispatchAdmission
|
|
6996
|
+
? { dispatchAdmission: mcpDispatchAdmission }
|
|
6997
|
+
: {}),
|
|
6998
|
+
});
|
|
6999
|
+
}
|
|
6285
7000
|
pendingResult = Reflect.apply(
|
|
6286
7001
|
mcpClient.callTool,
|
|
6287
7002
|
mcpClient,
|
|
@@ -7704,10 +8419,14 @@ async function _executeSpawnSubAgent(args, ctx) {
|
|
|
7704
8419
|
externalToolDescriptors: inheritedMcp.externalToolDescriptors,
|
|
7705
8420
|
externalToolExecutors: inheritedMcp.externalToolExecutors,
|
|
7706
8421
|
mcpClient: inheritedMcp.mcpClient,
|
|
8422
|
+
mcpHostClient: ctx.mcpHostClient || inheritedMcp.mcpClient,
|
|
7707
8423
|
...(ctx.mcpCallLedger ? { mcpCallLedger: ctx.mcpCallLedger } : {}),
|
|
7708
8424
|
...(ctx.mcpConflictScheduler
|
|
7709
8425
|
? { mcpConflictScheduler: ctx.mcpConflictScheduler }
|
|
7710
8426
|
: {}),
|
|
8427
|
+
...(ctx.mcpDispatchAdmission
|
|
8428
|
+
? { mcpDispatchAdmission: ctx.mcpDispatchAdmission }
|
|
8429
|
+
: {}),
|
|
7711
8430
|
}
|
|
7712
8431
|
: {}),
|
|
7713
8432
|
...(inheritedHooks ? { settingsHooks: inheritedHooks } : {}),
|
|
@@ -7997,7 +8716,10 @@ async function _executeSpawnSubAgent(args, ctx) {
|
|
|
7997
8716
|
// ─── LLM chat with tools ─────────────────────────────────────────────────
|
|
7998
8717
|
|
|
7999
8718
|
function getEffectiveToolDefinitions(options = {}) {
|
|
8000
|
-
const persona =
|
|
8719
|
+
const persona =
|
|
8720
|
+
options.hermeticExecution === true
|
|
8721
|
+
? null
|
|
8722
|
+
: _loadProjectPersona(options.cwd);
|
|
8001
8723
|
// Merge every deny source before both schema projection and execution-time
|
|
8002
8724
|
// enforcement. Keeping this in one helper prevents those two fences from
|
|
8003
8725
|
// drifting apart.
|
|
@@ -9484,6 +10206,93 @@ function _compactionTokenUsage(stats) {
|
|
|
9484
10206
|
};
|
|
9485
10207
|
}
|
|
9486
10208
|
|
|
10209
|
+
function _sameMessageSnapshot(messages, expectedMessages) {
|
|
10210
|
+
return (
|
|
10211
|
+
messages.length === expectedMessages.length &&
|
|
10212
|
+
messages.every((message, index) => message === expectedMessages[index])
|
|
10213
|
+
);
|
|
10214
|
+
}
|
|
10215
|
+
|
|
10216
|
+
function _compactionSettlementError(code, message, cause = null) {
|
|
10217
|
+
const error = new Error(message, cause ? { cause } : undefined);
|
|
10218
|
+
error.code = code;
|
|
10219
|
+
return error;
|
|
10220
|
+
}
|
|
10221
|
+
|
|
10222
|
+
/**
|
|
10223
|
+
* Settle one automatic compaction candidate before changing the live array.
|
|
10224
|
+
* Provider work has already completed when this runs. The actual canonical
|
|
10225
|
+
* compare-and-append remains synchronous, so no JavaScript turn can interleave
|
|
10226
|
+
* between the durable CAS and the in-memory replacement.
|
|
10227
|
+
*/
|
|
10228
|
+
async function _settleAutomaticCompaction({
|
|
10229
|
+
messages,
|
|
10230
|
+
liveExpectedMessages,
|
|
10231
|
+
authorityExpectedMessages,
|
|
10232
|
+
compacted,
|
|
10233
|
+
stats,
|
|
10234
|
+
options,
|
|
10235
|
+
}) {
|
|
10236
|
+
if (!_sameMessageSnapshot(messages, liveExpectedMessages)) {
|
|
10237
|
+
throw _compactionSettlementError(
|
|
10238
|
+
"SESSION_REVISION_STALE",
|
|
10239
|
+
"Session messages changed while automatic compaction was running",
|
|
10240
|
+
);
|
|
10241
|
+
}
|
|
10242
|
+
|
|
10243
|
+
let settlement = null;
|
|
10244
|
+
if (typeof options.onCompaction === "function") {
|
|
10245
|
+
if (options.onCompaction.constructor?.name === "AsyncFunction") {
|
|
10246
|
+
throw _compactionSettlementError(
|
|
10247
|
+
"CC_COMPACTION_SETTLEMENT_ASYNC",
|
|
10248
|
+
"Automatic compaction settlement must be synchronous",
|
|
10249
|
+
);
|
|
10250
|
+
}
|
|
10251
|
+
settlement = options.onCompaction(stats, compacted, {
|
|
10252
|
+
expectedMessages: authorityExpectedMessages,
|
|
10253
|
+
liveExpectedMessages,
|
|
10254
|
+
trigger: "auto",
|
|
10255
|
+
});
|
|
10256
|
+
} else if (options.sessionId && options.persistCompaction !== false) {
|
|
10257
|
+
const store = await import("../harness/jsonl-session-store.js");
|
|
10258
|
+
if (!_sameMessageSnapshot(messages, liveExpectedMessages)) {
|
|
10259
|
+
throw _compactionSettlementError(
|
|
10260
|
+
"SESSION_REVISION_STALE",
|
|
10261
|
+
"Session messages changed before automatic compaction settlement",
|
|
10262
|
+
);
|
|
10263
|
+
}
|
|
10264
|
+
if (store.sessionExists(options.sessionId)) {
|
|
10265
|
+
settlement = store.appendCompactEventIfMessagesMatch(
|
|
10266
|
+
options.sessionId,
|
|
10267
|
+
{
|
|
10268
|
+
...stats,
|
|
10269
|
+
trigger: "auto",
|
|
10270
|
+
messages: projectCanonicalResumeMessages(compacted, { strict: true }),
|
|
10271
|
+
},
|
|
10272
|
+
authorityExpectedMessages,
|
|
10273
|
+
);
|
|
10274
|
+
}
|
|
10275
|
+
}
|
|
10276
|
+
|
|
10277
|
+
if (settlement && typeof settlement.then === "function") {
|
|
10278
|
+
// Observe a late rejection but never await/retry an authority settlement
|
|
10279
|
+
// whose commit state may already be unknown.
|
|
10280
|
+
void Promise.resolve(settlement).catch(() => {});
|
|
10281
|
+
throw _compactionSettlementError(
|
|
10282
|
+
"CC_COMPACTION_SETTLEMENT_ASYNC",
|
|
10283
|
+
"Automatic compaction settlement must be synchronous",
|
|
10284
|
+
);
|
|
10285
|
+
}
|
|
10286
|
+
if (!_sameMessageSnapshot(messages, liveExpectedMessages)) {
|
|
10287
|
+
throw _compactionSettlementError(
|
|
10288
|
+
"CC_COMPACTION_LOCAL_STATE_CHANGED",
|
|
10289
|
+
"Live messages changed after automatic compaction settlement",
|
|
10290
|
+
);
|
|
10291
|
+
}
|
|
10292
|
+
messages.splice(0, messages.length, ...compacted);
|
|
10293
|
+
return settlement;
|
|
10294
|
+
}
|
|
10295
|
+
|
|
9487
10296
|
/**
|
|
9488
10297
|
* Run `fn` inside an OpenTelemetry span when `options.recorder` is attached,
|
|
9489
10298
|
* else run it bare (zero overhead on the un-instrumented path). `onResult`
|
|
@@ -9590,10 +10399,44 @@ export async function* agentLoop(messages, options) {
|
|
|
9590
10399
|
const runId =
|
|
9591
10400
|
options.runId ||
|
|
9592
10401
|
`run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
10402
|
+
let automaticCompactionSettlementBlocked = false;
|
|
10403
|
+
|
|
10404
|
+
const fileMutationScope = normalizeExactFileMutationScope(
|
|
10405
|
+
options.fileMutationScope,
|
|
10406
|
+
{ cwd: options.cwd || process.cwd() },
|
|
10407
|
+
);
|
|
10408
|
+
const hermeticExecution = options.hermeticExecution === true;
|
|
10409
|
+
if (
|
|
10410
|
+
fileMutationScope &&
|
|
10411
|
+
(options.exactToolNames !== true || !hermeticExecution)
|
|
10412
|
+
) {
|
|
10413
|
+
throw new Error(
|
|
10414
|
+
"exact file mutation scope requires exactToolNames=true and hermeticExecution=true",
|
|
10415
|
+
);
|
|
10416
|
+
}
|
|
9593
10417
|
|
|
10418
|
+
const effectiveToolOptions = hermeticExecution
|
|
10419
|
+
? {
|
|
10420
|
+
...options,
|
|
10421
|
+
hostManagedToolPolicy: null,
|
|
10422
|
+
extraToolDefinitions: [],
|
|
10423
|
+
}
|
|
10424
|
+
: options;
|
|
9594
10425
|
const effectiveAllowedToolNames = Object.freeze(
|
|
9595
|
-
getEffectiveToolDefinitions(
|
|
10426
|
+
getEffectiveToolDefinitions(effectiveToolOptions).map(
|
|
10427
|
+
(tool) => tool.function.name,
|
|
10428
|
+
),
|
|
9596
10429
|
);
|
|
10430
|
+
if (
|
|
10431
|
+
fileMutationScope &&
|
|
10432
|
+
effectiveAllowedToolNames.some(
|
|
10433
|
+
(name) => !EXACT_FILE_MUTATION_SCOPE_TOOL_NAMES.has(name),
|
|
10434
|
+
)
|
|
10435
|
+
) {
|
|
10436
|
+
throw new Error(
|
|
10437
|
+
"exact file mutation scope contains a tool outside the hermetic file-tool ceiling",
|
|
10438
|
+
);
|
|
10439
|
+
}
|
|
9597
10440
|
const mcpCallLedger =
|
|
9598
10441
|
options.mcpCallLedger ||
|
|
9599
10442
|
createMcpCallLedger({ sink: options.mcpLedgerSink || null });
|
|
@@ -9601,7 +10444,7 @@ export async function* agentLoop(messages, options) {
|
|
|
9601
10444
|
options.mcpConflictScheduler || createMcpConflictScheduler();
|
|
9602
10445
|
|
|
9603
10446
|
const toolContext = {
|
|
9604
|
-
hookDb: options.hookDb || null,
|
|
10447
|
+
hookDb: hermeticExecution ? null : options.hookDb || null,
|
|
9605
10448
|
skillLoader: options.skillLoader || _defaultSkillLoader,
|
|
9606
10449
|
// Hook-envelope tracing (P2 unified event bus): every settings-hook payload
|
|
9607
10450
|
// fired during this run carries trace_id = this run's id; a spawned child
|
|
@@ -9615,6 +10458,8 @@ export async function* agentLoop(messages, options) {
|
|
|
9615
10458
|
// ids/dirNames. Set by the spawn path from the resolved subagent contract.
|
|
9616
10459
|
skillAllowlist: options.skillAllowlist ?? null,
|
|
9617
10460
|
effectiveAllowedToolNames,
|
|
10461
|
+
fileMutationScope,
|
|
10462
|
+
hermeticExecution,
|
|
9618
10463
|
cwd: options.cwd || process.cwd(),
|
|
9619
10464
|
planManager: options.planManager || null,
|
|
9620
10465
|
sessionId: options.sessionId || null,
|
|
@@ -9622,23 +10467,38 @@ export async function* agentLoop(messages, options) {
|
|
|
9622
10467
|
// several model/tool iterations. Hosts may supply their persisted turn id;
|
|
9623
10468
|
// otherwise the run id is the stable in-process binding.
|
|
9624
10469
|
turnId: options.turnId || runId,
|
|
9625
|
-
hostManagedToolPolicy:
|
|
9626
|
-
|
|
9627
|
-
|
|
10470
|
+
hostManagedToolPolicy: hermeticExecution
|
|
10471
|
+
? null
|
|
10472
|
+
: options.hostManagedToolPolicy || null,
|
|
10473
|
+
externalToolDescriptors: hermeticExecution
|
|
10474
|
+
? null
|
|
10475
|
+
: options.externalToolDescriptors || null,
|
|
10476
|
+
externalToolExecutors: hermeticExecution
|
|
10477
|
+
? null
|
|
10478
|
+
: options.externalToolExecutors || null,
|
|
9628
10479
|
// Optional session-level Extension Tier gate. Hosts that can provide
|
|
9629
10480
|
// capability/policy/permission/budget/UI signals opt in with
|
|
9630
10481
|
// { enforce: true }; omitting it preserves the CLI's existing per-call
|
|
9631
10482
|
// permission pipeline for backwards compatibility.
|
|
9632
|
-
toolAdmission: options.toolAdmission || null,
|
|
9633
|
-
unattendedActionPolicy:
|
|
10483
|
+
toolAdmission: hermeticExecution ? null : options.toolAdmission || null,
|
|
10484
|
+
unattendedActionPolicy: hermeticExecution
|
|
10485
|
+
? null
|
|
10486
|
+
: options.unattendedActionPolicy || null,
|
|
9634
10487
|
// MCP tool DEFINITIONS the LLM sees (mcp__server__tool). Threaded here so a
|
|
9635
10488
|
// spawn can inherit the parent's MCP tools into the child (filtered by the
|
|
9636
10489
|
// contract's mcpServers allow-list). Otherwise consumed only at agentLoop.
|
|
9637
|
-
extraToolDefinitions:
|
|
9638
|
-
|
|
9639
|
-
|
|
10490
|
+
extraToolDefinitions: hermeticExecution
|
|
10491
|
+
? null
|
|
10492
|
+
: options.extraToolDefinitions || null,
|
|
10493
|
+
mcpClient: hermeticExecution ? null : options.mcpClient || null,
|
|
10494
|
+
mcpHostClient: hermeticExecution
|
|
10495
|
+
? null
|
|
10496
|
+
: options.mcpHostClient || options.mcpClient || null,
|
|
9640
10497
|
mcpCallLedger,
|
|
9641
10498
|
mcpConflictScheduler,
|
|
10499
|
+
mcpDispatchAdmission: hermeticExecution
|
|
10500
|
+
? null
|
|
10501
|
+
: options.mcpDispatchAdmission || null,
|
|
9642
10502
|
// A loop-local identity prevents one session's lazy instruction commits
|
|
9643
10503
|
// from suppressing first-access delivery in another session at the same
|
|
9644
10504
|
// cwd. Hosts may inject a stable identity across resumed turns.
|
|
@@ -9666,21 +10526,25 @@ export async function* agentLoop(messages, options) {
|
|
|
9666
10526
|
// verification allowlist (npm test / rg / …) is classified through the
|
|
9667
10527
|
// ApprovalGate instead of fast-pathed, so no shell command auto-runs.
|
|
9668
10528
|
classifyAllShell: options.classifyAllShell || false,
|
|
9669
|
-
approvalGate: options.approvalGate || null,
|
|
9670
|
-
shellConfirm: options.shellConfirm || null,
|
|
10529
|
+
approvalGate: hermeticExecution ? null : options.approvalGate || null,
|
|
10530
|
+
shellConfirm: hermeticExecution ? null : options.shellConfirm || null,
|
|
9671
10531
|
// Interactive sessions (the REPL) set this so run_code is gated through the
|
|
9672
10532
|
// ApprovalGate like run_shell — a human can approve. Headless leaves it
|
|
9673
10533
|
// false so run_code keeps its existing per-permission-mode behavior.
|
|
9674
10534
|
interactiveApproval: options.interactiveApproval || false,
|
|
9675
|
-
additionalDirectories:
|
|
10535
|
+
additionalDirectories: hermeticExecution
|
|
10536
|
+
? null
|
|
10537
|
+
: options.additionalDirectories || null,
|
|
9676
10538
|
sandbox: options.sandbox || null,
|
|
9677
|
-
permissionRules: options.permissionRules || null,
|
|
9678
|
-
permissionConfirm:
|
|
9679
|
-
|
|
10539
|
+
permissionRules: hermeticExecution ? null : options.permissionRules || null,
|
|
10540
|
+
permissionConfirm: hermeticExecution
|
|
10541
|
+
? null
|
|
10542
|
+
: options.permissionConfirm || null,
|
|
10543
|
+
settingsHooks: hermeticExecution ? null : options.settingsHooks || null,
|
|
9680
10544
|
// Async-hook supervisor (REPL-owned): lets PostToolUse `async:true` hooks
|
|
9681
10545
|
// run fire-and-forget instead of blocking the tool loop. Optional — when
|
|
9682
10546
|
// absent, async PostToolUse hooks are simply skipped (never run sync).
|
|
9683
|
-
hookSupervisor: options.hookSupervisor || null,
|
|
10547
|
+
hookSupervisor: hermeticExecution ? null : options.hookSupervisor || null,
|
|
9684
10548
|
autoCheckpoint: options.autoCheckpoint || false,
|
|
9685
10549
|
checkpointSession:
|
|
9686
10550
|
options.checkpointSession || options.sessionId || "agent",
|
|
@@ -9730,6 +10594,7 @@ export async function* agentLoop(messages, options) {
|
|
|
9730
10594
|
};
|
|
9731
10595
|
const backgroundSubAgents = toolContext.backgroundSubAgents;
|
|
9732
10596
|
const subAgentUsageSink = toolContext.subAgentUsageSink;
|
|
10597
|
+
const lifecycleHookEmitter = hermeticExecution ? () => {} : emitHooksV2Event;
|
|
9733
10598
|
|
|
9734
10599
|
throwIfAborted(signal);
|
|
9735
10600
|
|
|
@@ -9737,7 +10602,7 @@ export async function* agentLoop(messages, options) {
|
|
|
9737
10602
|
// Before calling the LLM, check if the user's message matches a known
|
|
9738
10603
|
// intent with missing required parameters. If so, interactively fill them
|
|
9739
10604
|
// and append the gathered context to the user message.
|
|
9740
|
-
if (options.slotFiller && options.interaction) {
|
|
10605
|
+
if (!hermeticExecution && options.slotFiller && options.interaction) {
|
|
9741
10606
|
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
9742
10607
|
if (lastUserMsg) {
|
|
9743
10608
|
try {
|
|
@@ -9916,10 +10781,19 @@ export async function* agentLoop(messages, options) {
|
|
|
9916
10781
|
// its full tool_call→tool_result cycle before we loop, so `messages` has no
|
|
9917
10782
|
// dangling call; `preserveToolPairs` then guarantees compaction never
|
|
9918
10783
|
// orphans a tool result. Best-effort — a failure never aborts the run.
|
|
9919
|
-
if (
|
|
10784
|
+
if (
|
|
10785
|
+
options.autoCompact !== false &&
|
|
10786
|
+
!automaticCompactionSettlementBlocked &&
|
|
10787
|
+
messages.length > 4
|
|
10788
|
+
) {
|
|
9920
10789
|
try {
|
|
9921
10790
|
const compactor = await _getAutoCompactor(options);
|
|
9922
10791
|
if (compactor && compactor.shouldAutoCompact(messages)) {
|
|
10792
|
+
// This is the canonical pre-provider authority snapshot. A local
|
|
10793
|
+
// micro-compact may change the working input, but the durable CAS must
|
|
10794
|
+
// still compare against the transcript state that existed before any
|
|
10795
|
+
// in-memory compaction happened.
|
|
10796
|
+
const authorityExpectedMessages = [...messages];
|
|
9923
10797
|
// Cheap surgical pre-pass (Claude-Code microcompact parity): trim old
|
|
9924
10798
|
// large tool results IN PLACE before the disruptive full
|
|
9925
10799
|
// summarization. If the trim brings the context back under threshold,
|
|
@@ -9945,7 +10819,7 @@ export async function* agentLoop(messages, options) {
|
|
|
9945
10819
|
// right before the history would be compacted.
|
|
9946
10820
|
let preCompactBlocked = false;
|
|
9947
10821
|
let preCompactReason = null;
|
|
9948
|
-
if (needFull && options.settingsHooks) {
|
|
10822
|
+
if (!hermeticExecution && needFull && options.settingsHooks) {
|
|
9949
10823
|
try {
|
|
9950
10824
|
const pc = runObserveHooks(
|
|
9951
10825
|
options.settingsHooks,
|
|
@@ -9999,44 +10873,68 @@ export async function* agentLoop(messages, options) {
|
|
|
9999
10873
|
};
|
|
10000
10874
|
}
|
|
10001
10875
|
if (stats.saved > 0 && compacted.length < messages.length) {
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
}
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10876
|
+
if (stats.summaryUsageUnknown === true) {
|
|
10877
|
+
automaticCompactionSettlementBlocked = true;
|
|
10878
|
+
yield {
|
|
10879
|
+
type: "compaction-usage-unknown",
|
|
10880
|
+
runId,
|
|
10881
|
+
provider: stats.summaryProvider || options.provider || null,
|
|
10882
|
+
model: stats.summaryModel || options.model || null,
|
|
10883
|
+
reason:
|
|
10884
|
+
stats.summaryUsageUnknownReason ||
|
|
10885
|
+
"provider_transport_outcome_unknown",
|
|
10886
|
+
source: "semantic-compaction",
|
|
10887
|
+
};
|
|
10888
|
+
// Do not apply the fallback or start another paid model request.
|
|
10889
|
+
// A transport failure may already have incurred unknown usage.
|
|
10890
|
+
return;
|
|
10891
|
+
}
|
|
10892
|
+
const liveExpectedMessages = [...messages];
|
|
10893
|
+
try {
|
|
10894
|
+
await _settleAutomaticCompaction({
|
|
10895
|
+
messages,
|
|
10896
|
+
liveExpectedMessages,
|
|
10897
|
+
authorityExpectedMessages,
|
|
10898
|
+
compacted,
|
|
10899
|
+
stats,
|
|
10900
|
+
options,
|
|
10901
|
+
});
|
|
10902
|
+
} catch (error) {
|
|
10903
|
+
automaticCompactionSettlementBlocked = true;
|
|
10904
|
+
yield {
|
|
10905
|
+
type: "compaction-degraded",
|
|
10906
|
+
runId,
|
|
10907
|
+
reason:
|
|
10908
|
+
error?.code === "SESSION_REVISION_STALE"
|
|
10909
|
+
? "session_messages_changed_during_compaction"
|
|
10910
|
+
: "canonical_compaction_settlement_failed",
|
|
10911
|
+
summaryMode: "none",
|
|
10912
|
+
code: error?.code || "CC_COMPACTION_SETTLEMENT_FAILED",
|
|
10913
|
+
};
|
|
10914
|
+
if (compactionUsage) {
|
|
10915
|
+
yield {
|
|
10916
|
+
type: "token-usage",
|
|
10917
|
+
provider: stats.summaryProvider || options.provider || null,
|
|
10918
|
+
model: stats.summaryModel || options.model || null,
|
|
10919
|
+
usage: compactionUsage,
|
|
10920
|
+
source: "semantic-compaction",
|
|
10921
|
+
runId,
|
|
10922
|
+
};
|
|
10030
10923
|
}
|
|
10924
|
+
// The paid summary call is never retried, and an unknown/stale
|
|
10925
|
+
// canonical settlement cannot be followed by another model call.
|
|
10926
|
+
return;
|
|
10927
|
+
}
|
|
10928
|
+
if (!hermeticExecution) {
|
|
10929
|
+
emitHooksV2Event("PostCompact", {
|
|
10930
|
+
schema_version: 1,
|
|
10931
|
+
trigger: "auto",
|
|
10932
|
+
session_id: options.sessionId || null,
|
|
10933
|
+
messages_after: messages.length,
|
|
10934
|
+
stats,
|
|
10935
|
+
cwd: options.cwd || process.cwd(),
|
|
10936
|
+
});
|
|
10031
10937
|
}
|
|
10032
|
-
emitHooksV2Event("PostCompact", {
|
|
10033
|
-
schema_version: 1,
|
|
10034
|
-
trigger: "auto",
|
|
10035
|
-
session_id: options.sessionId || null,
|
|
10036
|
-
messages_after: messages.length,
|
|
10037
|
-
stats,
|
|
10038
|
-
cwd: options.cwd || process.cwd(),
|
|
10039
|
-
});
|
|
10040
10938
|
yield { type: "compaction", stats, runId };
|
|
10041
10939
|
}
|
|
10042
10940
|
if (compactionUsage) {
|
|
@@ -10077,7 +10975,7 @@ export async function* agentLoop(messages, options) {
|
|
|
10077
10975
|
)) {
|
|
10078
10976
|
// SubagentStop fires at RESULT time for background spawns (the
|
|
10079
10977
|
// spawn-time handle skips it); a block reason rides along as feedback.
|
|
10080
|
-
if (options.settingsHooks) {
|
|
10978
|
+
if (!hermeticExecution && options.settingsHooks) {
|
|
10081
10979
|
try {
|
|
10082
10980
|
const outcome = runObserveHooks(
|
|
10083
10981
|
options.settingsHooks,
|
|
@@ -10118,7 +11016,7 @@ export async function* agentLoop(messages, options) {
|
|
|
10118
11016
|
// prepareCall runs fresh each iteration and returns an ephemeral
|
|
10119
11017
|
// system-message supplement that is NOT persisted to messages history.
|
|
10120
11018
|
let callMessages = messages;
|
|
10121
|
-
if (typeof options.prepareCall === "function") {
|
|
11019
|
+
if (!hermeticExecution && typeof options.prepareCall === "function") {
|
|
10122
11020
|
try {
|
|
10123
11021
|
const hook = await options.prepareCall({
|
|
10124
11022
|
iteration: budget.consumed,
|
|
@@ -10161,7 +11059,7 @@ export async function* agentLoop(messages, options) {
|
|
|
10161
11059
|
"agent.iteration": budget.consumed,
|
|
10162
11060
|
...(modelIdAttrs || {}),
|
|
10163
11061
|
},
|
|
10164
|
-
() => llmCall(callMessages,
|
|
11062
|
+
() => llmCall(callMessages, effectiveToolOptions),
|
|
10165
11063
|
(span, r) => {
|
|
10166
11064
|
const t = _usageTokens(r?.usage);
|
|
10167
11065
|
if (t) {
|
|
@@ -10296,7 +11194,7 @@ export async function* agentLoop(messages, options) {
|
|
|
10296
11194
|
// going instead of stopping — the reason is injected as a new instruction.
|
|
10297
11195
|
// `stop_hook_active` lets the hook avoid an infinite loop; the iteration
|
|
10298
11196
|
// budget is the hard backstop.
|
|
10299
|
-
if (options.settingsHooks) {
|
|
11197
|
+
if (!hermeticExecution && options.settingsHooks) {
|
|
10300
11198
|
let stopOutcome = null;
|
|
10301
11199
|
try {
|
|
10302
11200
|
stopOutcome = runObserveHooks(
|
|
@@ -10441,6 +11339,7 @@ export async function* agentLoop(messages, options) {
|
|
|
10441
11339
|
turnId,
|
|
10442
11340
|
toolUseId: call.id,
|
|
10443
11341
|
cwd: options.cwd || process.cwd(),
|
|
11342
|
+
emit: lifecycleHookEmitter,
|
|
10444
11343
|
});
|
|
10445
11344
|
toolBatchRecords.push({
|
|
10446
11345
|
tool: call.function.name,
|
|
@@ -10481,6 +11380,7 @@ export async function* agentLoop(messages, options) {
|
|
|
10481
11380
|
turnId,
|
|
10482
11381
|
cwd: options.cwd || process.cwd(),
|
|
10483
11382
|
parallel: true,
|
|
11383
|
+
emit: lifecycleHookEmitter,
|
|
10484
11384
|
});
|
|
10485
11385
|
continue; // all read results in place — back to the LLM call
|
|
10486
11386
|
}
|
|
@@ -10509,6 +11409,7 @@ export async function* agentLoop(messages, options) {
|
|
|
10509
11409
|
turnId,
|
|
10510
11410
|
toolUseId: call?.id || null,
|
|
10511
11411
|
cwd: options.cwd || process.cwd(),
|
|
11412
|
+
emit: lifecycleHookEmitter,
|
|
10512
11413
|
});
|
|
10513
11414
|
toolBatchRecords.push({
|
|
10514
11415
|
tool: "(unknown)",
|
|
@@ -10541,6 +11442,57 @@ export async function* agentLoop(messages, options) {
|
|
|
10541
11442
|
toolArgs = {};
|
|
10542
11443
|
}
|
|
10543
11444
|
|
|
11445
|
+
// Capability and exact-path authority must settle before any checkpoint,
|
|
11446
|
+
// hook, or observationally stronger `tool-executing` event. The same
|
|
11447
|
+
// preflight runs again inside executeTool to prevent call-site drift.
|
|
11448
|
+
const earlyAuthorityDenial = preflightToolExecutionAuthority(
|
|
11449
|
+
toolName,
|
|
11450
|
+
toolArgs,
|
|
11451
|
+
toolContext,
|
|
11452
|
+
);
|
|
11453
|
+
if (earlyAuthorityDenial) {
|
|
11454
|
+
const resultStr = capToolResultString(
|
|
11455
|
+
safeStringifyToolResult(earlyAuthorityDenial),
|
|
11456
|
+
);
|
|
11457
|
+
const decision = permissionDecision(
|
|
11458
|
+
call.id,
|
|
11459
|
+
toolName,
|
|
11460
|
+
earlyAuthorityDenial,
|
|
11461
|
+
);
|
|
11462
|
+
yield {
|
|
11463
|
+
type: "tool-result",
|
|
11464
|
+
tool: toolName,
|
|
11465
|
+
result: earlyAuthorityDenial,
|
|
11466
|
+
error: earlyAuthorityDenial.error,
|
|
11467
|
+
tool_use_id: call.id,
|
|
11468
|
+
turn_id: `${runId}:t${budget.consumed}`,
|
|
11469
|
+
permission_decision_id: decision?.id || null,
|
|
11470
|
+
permission_decision: decision,
|
|
11471
|
+
};
|
|
11472
|
+
messages.push({
|
|
11473
|
+
role: "tool",
|
|
11474
|
+
content: resultStr,
|
|
11475
|
+
tool_call_id: call.id,
|
|
11476
|
+
});
|
|
11477
|
+
const failed = emitToolHookLifecycle({
|
|
11478
|
+
tool: toolName,
|
|
11479
|
+
args: toolArgs,
|
|
11480
|
+
result: earlyAuthorityDenial,
|
|
11481
|
+
error: earlyAuthorityDenial.error,
|
|
11482
|
+
sessionId: options.sessionId || null,
|
|
11483
|
+
turnId,
|
|
11484
|
+
toolUseId: call.id,
|
|
11485
|
+
cwd: options.cwd || process.cwd(),
|
|
11486
|
+
emit: lifecycleHookEmitter,
|
|
11487
|
+
});
|
|
11488
|
+
toolBatchRecords.push({
|
|
11489
|
+
tool: toolName,
|
|
11490
|
+
toolUseId: call.id,
|
|
11491
|
+
failed,
|
|
11492
|
+
});
|
|
11493
|
+
continue;
|
|
11494
|
+
}
|
|
11495
|
+
|
|
10544
11496
|
// Auto-checkpoint the work tree before a mutating tool (opt-in), so the
|
|
10545
11497
|
// user can `cc checkpoint restore` back to just before this call.
|
|
10546
11498
|
const cpId = await _autoCheckpointBeforeTool(
|
|
@@ -10825,6 +11777,7 @@ export async function* agentLoop(messages, options) {
|
|
|
10825
11777
|
turnId,
|
|
10826
11778
|
toolUseId: call.id,
|
|
10827
11779
|
cwd: options.cwd || process.cwd(),
|
|
11780
|
+
emit: lifecycleHookEmitter,
|
|
10828
11781
|
});
|
|
10829
11782
|
toolBatchRecords.push({
|
|
10830
11783
|
tool: toolName,
|
|
@@ -10865,6 +11818,7 @@ export async function* agentLoop(messages, options) {
|
|
|
10865
11818
|
turnId,
|
|
10866
11819
|
cwd: options.cwd || process.cwd(),
|
|
10867
11820
|
parallel: false,
|
|
11821
|
+
emit: lifecycleHookEmitter,
|
|
10868
11822
|
});
|
|
10869
11823
|
}
|
|
10870
11824
|
|