chainlesschain 0.162.170 → 0.162.171
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-BTXN24I3.js → AIOps-Cf8yb18C.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BBB1VF4j.js → ActionButton-CsFT6FMt.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BVCXq0k6.js → Analytics-DQe_hXMA.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-C3EOo2s5.js → AppLayout-Cl9Pp7jk.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-CsJTZSMv.js → Artifacts-CBEzLVjF.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CP0AllJ_.js → Audit-BS2Un14C.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BCAjs08m.js → BackgroundAgents-DmcDH8OK.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BxQI5XH4.js → Backup-DvzjTkkn.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-Bu-PNzC5.js → BaseInput-DnbpuyME.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DjPQynup.js → Chat-BJRRCm2P.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CygkRopu.js → ChatBubbleRenderer-Bgpxyqd_.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BxL6xm7Z.js → Checkbox-DFOrWmvm.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-CcyB7h_6.js → Codegen-BIQ2kU6Q.js} +1 -1
- package/src/assets/web-panel/assets/{Col-8NQiKJJx.js → Col-BI3R-N4I.js} +1 -1
- package/src/assets/web-panel/assets/{Community-B31qy5OS.js → Community-CPiAwsHT.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-C-5fAsZ4.js → Compact-Bv4cslSK.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BTSccvF0.js → Compliance-CSlDG1NZ.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BXctU9IK.js → Cowork-CFcRumXf.js} +4 -4
- package/src/assets/web-panel/assets/{Cron-CxVFX9NG.js → Cron-DKYKMmOn.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DEF3dq8M.js → Crosschain-CxPMByp2.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BmKcE9he.js → DID-DJqiYw1C.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CECBq_wG.js → Dashboard-D2dLMxqy.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-U-iuPB0n.js → Dropdown-BnJc384A.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-D8BIlSPL.js → EmailListRenderer-DctFvm-J.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BzQGfUH-.js → FamilyGuardDashboard-J_xg-Qop.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-4_WlOqLZ.js → Federation-BckL07Uw.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-gtrW-DdF.js → FormItemContext-B8s5ccSO.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BLcvJJB6.js → GenericCardRenderer-hPMBK2No.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Bk0janGD.js → Git-CfMpYLo7.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CcfyMlV8.js → Governance-Bt3OcrC7.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CBLrAf4M.js → Inference-BlKc336N.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-ClguhteZ.js → KnowledgeGraph-B6p7og2c.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-Dp_oeKfJ.js → Logs-uv5Re6YR.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-RQ1HbTY5.js → MarkdownRenderer-CId5JWjX.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-BHk50smM.js → Marketplace-CfNbntHt.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BAY3CV6J.js → McpTools-YRXt74Yk.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-w0pG2AKx.js → Memory-BfQQs-Jd.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-C0VkDUE3.js → MobileBridge-BuGC-4S2.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BCvQ80I9.js → MobileProjects-Czieqw1m.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-DSIl41Tt.js → Mtc-DHWHOVCO.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-D1mzUlIr.js → MtcAudit-DWSEbMW-.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-CeJz4VSS.js → Multisig-DrhEv1vq.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-_O3aweu8.js → NLProgramming-D8wKCu7E.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Dt7_pO86.js → Notes-AFeqq0I_.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-Dhg3Nktv.js → NotificationSettings-D6qenYUo.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-QsAPcTb2.js → OrderTableRenderer-CF0X4fyB.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BuiaUPkv.js → Organization-DdfbNqf6.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-C3jb6knl.js → Overflow-BFEvnLfd.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BOMyrLkb.js → P2P-DtL6wbkz.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-UvLStCgS.js → PdhVaultBrowser-CQPriTud.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-B9cN0dJH.js → Permissions-Bj25ANmR.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DNjW5qro.js → PersonalDataHub-BNj2Oux0.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-CFLyZ6Sh.js → Pipeline-Dyj8UEaG.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-D3w6vOxf.js → Privacy-BBUrWRFz.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-cwcVsn8K.js → ProjectInit-BdX7uQn6.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-aUoAroc-.js → ProjectSettings-BghtW1zr.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-CrzQv7Zv.js → Projects-9VTquZCz.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-jZaPizKR.js → Providers-D-0K_Eza.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-DNzmwcfg.js → QrScannerModal-DDaN30_D.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-m-5Gwt70.js → QuickAsk-DfHlifOj.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CrrAjNxP.js → Recommend-CgLIvYE2.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-BDCMjRFe.js → RemoteSession-DpOPrwE4.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-CPCt1Jc8.js → Reputation-wMexlbCE.js} +1 -1
- package/src/assets/web-panel/assets/{Row-C-RorxHs.js → Row-D9aD_nrc.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CnQs4TuY.js → RssFeed-Drg7l6zu.js} +3 -3
- package/src/assets/web-panel/assets/{Search-B1jkeFZY.js → Search-BKO1nrwN.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CGKI3vvd.js → Security-G3Kyk-TG.js} +3 -3
- package/src/assets/web-panel/assets/{Services-DQjesYF8.js → Services-xJReTOna.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-C9Ipj0A6.js → Skeleton-ldVDVTPG.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-D2j54i9O.js → Skills-emwNYV0j.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-xhrVjTeb.js → Sla-CZYwGp1K.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-CyaPrzOF.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-CmIPU5wR.js → SyncSettings-Dcs5Hhj6.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-B-T1J42r.js → Tasks-CerFAo26.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Aq669sjw.js → Templates-BdZUbOg3.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BKWUWNXB.js → Tenant-B4c_7Bsw.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-VVFLS3Ol.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-C465ySdn.js → TimelineRenderer-tc6sp0TV.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BgVqF-bX.js → Tokens-Bw_vkMJq.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BMNHmxwH.js → Trigger-DkbJnpvz.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-B_adk89L.js → Trust-CIf9kqN1.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-Biw38mog.js → UkeySign-Cjwllnea.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-AC-eaw5k.js → VideoEditing-kgfkY7AN.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-8lQ5_t0X.js → Wallet-CYWXt4u9.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-JDmH_-DF.js → WebAuthn-QgPwyWCO.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-DHQAdITG.js → WorkflowEditor-Co0lha18.js} +1 -1
- package/src/assets/web-panel/assets/{chat-C3qauUVv.js → chat-CI__Ox-r.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DLP70JGe.js → colors-Csv0eYYd.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DjsoqA5t.js → compact-item-e53UCdHy.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CfEdl1in.js → createContext-BMPGRDhV.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-BIulobq7.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DWwHHlBG.js → hasIn-Dz68qIAq.js} +1 -1
- package/src/assets/web-panel/assets/{index-DbFnfyKh.js → index--7tHEadk.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKsWujXP.js → index-53ljQo_x.js} +1 -1
- package/src/assets/web-panel/assets/{index-ArZCi1rK.js → index-5iicv5af.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTEcPJGf.js → index-B1s1B8Iy.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcDeMCUK.js → index-B4lKPJy1.js} +1 -1
- package/src/assets/web-panel/assets/index-BGkiYnET.js +1 -0
- package/src/assets/web-panel/assets/{index-Ds1BfT_C.js → index-BHkDRNA5.js} +1 -1
- package/src/assets/web-panel/assets/{index-CNeLedtU.js → index-BOpz2qS1.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dfka-vzJ.js → index-BT3CV4Nh.js} +1 -1
- package/src/assets/web-panel/assets/{index-BqPuyN4p.js → index-BW2dImbg.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4k6JGm_.js → index-C-NbcAbP.js} +1 -1
- package/src/assets/web-panel/assets/{index-D-te-S9f.js → index-C53o-MdJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CsEk9s04.js → index-C5hLtxVz.js} +1 -1
- package/src/assets/web-panel/assets/{index-OfMwkFIo.js → index-CNtZMPKV.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bp_WNm2_.js → index-CXuON9OG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CgruUvB3.js → index-CYUxs6J1.js} +1 -1
- package/src/assets/web-panel/assets/{index-C8wrUFzY.js → index-CoGSbW5J.js} +1 -1
- package/src/assets/web-panel/assets/{index-BydBh7oT.js → index-CpTzqJ0Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-nuZaMsMy.js → index-CrsoSnQF.js} +1 -1
- package/src/assets/web-panel/assets/{index-B-rD2rhi.js → index-CvLBQk_M.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbF27GqZ.js → index-CyLUaP2i.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTDpF8MS.js → index-D0bg74e1.js} +1 -1
- package/src/assets/web-panel/assets/{index-DAcBi6F-.js → index-DCl4Xb4N.js} +1 -1
- package/src/assets/web-panel/assets/{index-D5VL0NYy.js → index-DJqQL6so.js} +1 -1
- package/src/assets/web-panel/assets/{index-UEbYl2pu.js → index-DVCd2aMX.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0xMzvk8.js → index-DctYrVMq.js} +1 -1
- package/src/assets/web-panel/assets/{index-C47EiQ7d.js → index-Dk9Gi8xd.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnhJbWsl.js → index-DnXm3Qsh.js} +1 -1
- package/src/assets/web-panel/assets/{index-T5emAyFT.js → index-Dtjm1K1a.js} +1 -1
- package/src/assets/web-panel/assets/{index-DgQ92xdO.js → index-DyqcHiL1.js} +1 -1
- package/src/assets/web-panel/assets/index-Eyhrr_3u.js +1 -0
- package/src/assets/web-panel/assets/{index-VtCz3A64.js → index-HbhQRrM2.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4gCFHtB.js → index-K5E_wjut.js} +1 -1
- package/src/assets/web-panel/assets/{index-DbjUePgq.js → index-N9DkR1i7.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dn2rNMmv.js → index-_LmPlyeV.js} +1 -1
- package/src/assets/web-panel/assets/{index-Lh0e2afo.js → index-ajz0E0pK.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dmt1HDWZ.js → index-t6f80uqL.js} +17 -17
- package/src/assets/web-panel/assets/{index-_g5q2hF3.js → index-wcCouSNq.js} +1 -1
- package/src/assets/web-panel/assets/{index-gu8zBhtp.js → index-ygfUVVFv.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-C_pMmi75.js → initDefaultProps-DVCeQJrT.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BvKgS96o.js → motion-chBCR_8v.js} +1 -1
- package/src/assets/web-panel/assets/{move-C-1U4R1I.js → move-BziPlrh6.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-D6BC968-.js → mtc-parser-Cenk0OD7.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DxOZvcGQ.js → omit-D-AiRsei.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-LpzmHdkw.js → pickAttrs-CHw3Fvqt.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-Bm--qom1.js → placementArrow-g8I-rCad.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-CVZZlhJf.js → responsiveObserve-BWffAr9k.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DD1xINGx.js → slide-BmUQqhkb.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-LrjHAy63.js → statusUtils-B89gCO-y.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BqknYxTe.js → styleChecker-x1bnCF73.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-Dljy3diT.js → useFlexGapSupport-B-k1_Ggw.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-OedUtPLc.js → useFs-Bc3PUqQk.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-Cz-kxbP_.js → usePersonalDataHub-D8qFDX1I.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-C8ZuTk8W.js → vnode-UaL5-WGg.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CjbgJYXQ.js → zoom-Dv7bsbLs.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +39 -14
- package/src/commands/background-session.js +12 -1
- package/src/gateways/ws/ws-agent-handler.js +26 -1
- package/src/gateways/ws/ws-approval-gate.js +131 -0
- package/src/lib/agent-worktree.js +173 -5
- package/src/lib/background-agent-supervisor.js +193 -4
- package/src/lib/interaction-adapter.js +26 -5
- package/src/lib/repl-rewind.js +62 -14
- package/src/lib/turn-binding.js +134 -0
- package/src/repl/agent-repl.js +75 -3
- package/src/repl/repl-turn-binding.js +91 -0
- package/src/runtime/headless-runner.js +59 -0
- package/src/assets/web-panel/assets/SpeechSettings-C6KMOUCa.js +0 -1
- package/src/assets/web-panel/assets/Terminal-DEeAvA8L.js +0 -3
- package/src/assets/web-panel/assets/devWarning-Cwuuqc_f.js +0 -1
- package/src/assets/web-panel/assets/index-BkTk-zJK.js +0 -1
- package/src/assets/web-panel/assets/index-C-BSTEhe.js +0 -1
|
@@ -26,6 +26,7 @@ import { runnableTaskModel } from "../../lib/runnable-provider.js";
|
|
|
26
26
|
import { PlanState } from "../../lib/plan-mode.js";
|
|
27
27
|
import { CLISlotFiller } from "../../lib/slot-filler.js";
|
|
28
28
|
import { createAbortError, isAbortError } from "../../lib/abort-utils.js";
|
|
29
|
+
import { createWsApprovalGate } from "./ws-approval-gate.js";
|
|
29
30
|
|
|
30
31
|
export class WSAgentHandler {
|
|
31
32
|
/**
|
|
@@ -34,7 +35,7 @@ export class WSAgentHandler {
|
|
|
34
35
|
* @param {import("../../lib/interaction-adapter.js").WebSocketInteractionAdapter} options.interaction
|
|
35
36
|
* @param {object} [options.db]
|
|
36
37
|
*/
|
|
37
|
-
constructor({ session, interaction, db }) {
|
|
38
|
+
constructor({ session, interaction, db, approvalGate }) {
|
|
38
39
|
this.session = session;
|
|
39
40
|
this.interaction = interaction;
|
|
40
41
|
this.db = db || null;
|
|
@@ -44,6 +45,27 @@ export class WSAgentHandler {
|
|
|
44
45
|
// P0-2: monotonic sequence for crash-safe side-effect ledger op ids. The
|
|
45
46
|
// session id + a per-op nonce keep ids unique across turns and processes.
|
|
46
47
|
this._sideEffectSeq = 0;
|
|
48
|
+
// P0 authority: permission-gate-over-WS. An explicitly injected gate wins
|
|
49
|
+
// (tests / embedders); otherwise one is built lazily on the first turn
|
|
50
|
+
// when CC_WS_APPROVAL_GATE=1 — its confirmer raises confirm questions
|
|
51
|
+
// over the interaction adapter WITH an approval binding the client must
|
|
52
|
+
// echo back on approve (mismatch → deny). Default (env unset, nothing
|
|
53
|
+
// injected): approvalGate stays null and loopOptions are byte-identical.
|
|
54
|
+
this._approvalGate = approvalGate || null;
|
|
55
|
+
this._approvalGateInit = Boolean(approvalGate);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Resolve the session's approval gate (lazy, opt-in; null when disabled). */
|
|
59
|
+
async _ensureApprovalGate() {
|
|
60
|
+
if (this._approvalGateInit) return this._approvalGate;
|
|
61
|
+
this._approvalGateInit = true;
|
|
62
|
+
if (process.env.CC_WS_APPROVAL_GATE === "1") {
|
|
63
|
+
this._approvalGate = await createWsApprovalGate({
|
|
64
|
+
sessionId: this.session.id,
|
|
65
|
+
interaction: this.interaction,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return this._approvalGate;
|
|
47
69
|
}
|
|
48
70
|
|
|
49
71
|
/**
|
|
@@ -127,6 +149,9 @@ export class WSAgentHandler {
|
|
|
127
149
|
slotFiller,
|
|
128
150
|
interaction: this.interaction,
|
|
129
151
|
signal: abortController.signal,
|
|
152
|
+
// P0 authority: null unless opted in (byte-identical default — agent
|
|
153
|
+
// core already defaults `options.approvalGate || null`).
|
|
154
|
+
approvalGate: await this._ensureApprovalGate(),
|
|
130
155
|
};
|
|
131
156
|
|
|
132
157
|
// P0-2: crash-safe side-effect ledger for the IDE/bridge path — mirrors
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission-gate-over-WS — the approval-BINDING producer for the WS path.
|
|
3
|
+
*
|
|
4
|
+
* The consumer side has been wired for a while: WebSocketInteractionAdapter
|
|
5
|
+
* stores `options.binding` on a pending request, rides it out on the message,
|
|
6
|
+
* and `_resolvePending` rejects an approve whose echoed binding mismatches
|
|
7
|
+
* (deny, fail closed). What was missing is the PRODUCER: nothing on the WS
|
|
8
|
+
* path ever raised an approval request WITH a binding, because the WS agent
|
|
9
|
+
* handler wired no ApprovalGate at all (CONFIRM-tier decisions fell closed).
|
|
10
|
+
*
|
|
11
|
+
* This module supplies both halves:
|
|
12
|
+
* - `buildWsApprovalConfirmer` — the gate confirmer that computes
|
|
13
|
+
* `approvalBindingDigest({toolCallId, args, policyDigest})` (mirroring the
|
|
14
|
+
* headless-stream `interactiveConfirm` producer) and asks the WS client
|
|
15
|
+
* over the interaction adapter with the binding attached. Timeout /
|
|
16
|
+
* disconnect / any transport error → false (fail closed).
|
|
17
|
+
* - `createWsApprovalGate` — a session-scoped gate whose CONFIRM decisions
|
|
18
|
+
* route to that confirmer. Policy tiers stay authoritative on the shared
|
|
19
|
+
* singleton gate (so `sessions.policy.set` over WS keeps working and no
|
|
20
|
+
* global confirmer is ever mutated — a per-session confirmer on the
|
|
21
|
+
* process-wide singleton would race across concurrent WS sessions).
|
|
22
|
+
*
|
|
23
|
+
* Default byte-identical: the WS handler only builds this when
|
|
24
|
+
* `CC_WS_APPROVAL_GATE=1` (or a gate is injected); otherwise loopOptions
|
|
25
|
+
* carry `approvalGate: null` exactly as before.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { approvalBindingDigest } from "../../lib/agent-authority.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build the binding-attaching confirmer for one WS session.
|
|
32
|
+
*
|
|
33
|
+
* @param {object} opts
|
|
34
|
+
* @param {{askConfirm: Function}} opts.interaction - the session's WS adapter
|
|
35
|
+
* @param {string} opts.sessionId
|
|
36
|
+
* @returns {(ctx?: object) => Promise<boolean>}
|
|
37
|
+
*/
|
|
38
|
+
export function buildWsApprovalConfirmer({ interaction, sessionId }) {
|
|
39
|
+
let approvalSeq = 0;
|
|
40
|
+
return async (ctx = {}) => {
|
|
41
|
+
const command = ctx.command ?? ctx.args?.command ?? null;
|
|
42
|
+
const risk = ctx.riskLevel || ctx.risk || "unknown";
|
|
43
|
+
// Bind this approval to the exact call it authorizes (same recipe as the
|
|
44
|
+
// headless-stream producer): request identity + normalized args + the
|
|
45
|
+
// policy/rule in force. The client must echo it back on approve.
|
|
46
|
+
const binding = approvalBindingDigest({
|
|
47
|
+
toolCallId: `${sessionId}:appr-${++approvalSeq}`,
|
|
48
|
+
args: ctx.args ?? (command != null ? { command } : null),
|
|
49
|
+
policyDigest: ctx.rule || ctx.riskLevel || ctx.risk || null,
|
|
50
|
+
});
|
|
51
|
+
const what = ctx.tool || ctx.toolName || "tool";
|
|
52
|
+
const detail = command != null ? `: ${String(command).slice(0, 200)}` : "";
|
|
53
|
+
try {
|
|
54
|
+
const ok = await interaction.askConfirm(
|
|
55
|
+
`Approve ${what}${detail}? (risk: ${risk})`,
|
|
56
|
+
{
|
|
57
|
+
default: false,
|
|
58
|
+
binding,
|
|
59
|
+
// Structured context so UIs can render a proper approval card
|
|
60
|
+
// instead of parsing the question text.
|
|
61
|
+
approval: {
|
|
62
|
+
tool: what,
|
|
63
|
+
command,
|
|
64
|
+
risk,
|
|
65
|
+
rule: ctx.rule || null,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
return ok === true;
|
|
70
|
+
} catch {
|
|
71
|
+
// timeout / disconnect / interrupt → fail closed, never approve
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a session-scoped approval gate for the WS agent handler.
|
|
79
|
+
*
|
|
80
|
+
* Policy lookups delegate to the persistent singleton gate (per-session tiers
|
|
81
|
+
* set via `sessions.policy.set` stay live); CONFIRM decisions route to the
|
|
82
|
+
* binding-attaching WS confirmer. Returns null on any load failure so the
|
|
83
|
+
* caller degrades to the legacy no-gate path (fail closed at CONFIRM).
|
|
84
|
+
*
|
|
85
|
+
* @param {object} opts
|
|
86
|
+
* @param {string} opts.sessionId
|
|
87
|
+
* @param {{askConfirm: Function}} opts.interaction
|
|
88
|
+
* @param {object} [opts.deps] - test seams: `loadSingletons`, `loadSessionCore`
|
|
89
|
+
* @returns {Promise<object|null>}
|
|
90
|
+
*/
|
|
91
|
+
export async function createWsApprovalGate({
|
|
92
|
+
sessionId,
|
|
93
|
+
interaction,
|
|
94
|
+
deps = {},
|
|
95
|
+
}) {
|
|
96
|
+
try {
|
|
97
|
+
const loadSingletons =
|
|
98
|
+
deps.loadSingletons ||
|
|
99
|
+
(() => import("../../lib/session-core-singletons.js"));
|
|
100
|
+
const loadSessionCore =
|
|
101
|
+
deps.loadSessionCore || (() => import("@chainlesschain/session-core"));
|
|
102
|
+
const [singletons, core] = await Promise.all([
|
|
103
|
+
loadSingletons(),
|
|
104
|
+
loadSessionCore(),
|
|
105
|
+
]);
|
|
106
|
+
const inner = await singletons.getApprovalGate();
|
|
107
|
+
const confirmer =
|
|
108
|
+
deps.confirmer || buildWsApprovalConfirmer({ interaction, sessionId });
|
|
109
|
+
const gate = new core.ApprovalGate({
|
|
110
|
+
defaultPolicy: core.APPROVAL_POLICY?.STRICT || "strict",
|
|
111
|
+
confirm: confirmer,
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
// Explicit ctx.policy always wins inside decide(); resolve it from the
|
|
115
|
+
// singleton on every call so mid-session `sessions.policy.set` applies.
|
|
116
|
+
decide: (ctx = {}) =>
|
|
117
|
+
gate.decide({
|
|
118
|
+
...ctx,
|
|
119
|
+
policy:
|
|
120
|
+
ctx.policy || inner.getSessionPolicy(ctx.sessionId || sessionId),
|
|
121
|
+
}),
|
|
122
|
+
hasConfirmer: () => gate.hasConfirmer(),
|
|
123
|
+
setConfirmer: (fn) => gate.setConfirmer(fn),
|
|
124
|
+
getSessionPolicy: (sid) => inner.getSessionPolicy(sid || sessionId),
|
|
125
|
+
setSessionPolicy: (sid, policy) =>
|
|
126
|
+
inner.setSessionPolicy(sid || sessionId, policy),
|
|
127
|
+
};
|
|
128
|
+
} catch {
|
|
129
|
+
return null; // degrade to the legacy no-gate path
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -11,11 +11,18 @@
|
|
|
11
11
|
* a merge hint. Removal failures fail-open to "kept".
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { execSync } from "child_process";
|
|
14
|
+
import { execFileSync, execSync } from "child_process";
|
|
15
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
16
|
+
import { resolve } from "node:path";
|
|
15
17
|
import { createWorktree, removeWorktree } from "./worktree-isolator.js";
|
|
16
18
|
import { findProjectRoot } from "./project-instructions.js";
|
|
17
19
|
|
|
18
|
-
export const _deps = {
|
|
20
|
+
export const _deps = {
|
|
21
|
+
execFileSync,
|
|
22
|
+
execSync,
|
|
23
|
+
createWorktree,
|
|
24
|
+
removeWorktree,
|
|
25
|
+
};
|
|
19
26
|
|
|
20
27
|
function git(cmd, cwd, deps) {
|
|
21
28
|
return (deps.execSync || execSync)(`git ${cmd}`, {
|
|
@@ -25,6 +32,132 @@ function git(cmd, cwd, deps) {
|
|
|
25
32
|
});
|
|
26
33
|
}
|
|
27
34
|
|
|
35
|
+
function canonicalPath(value) {
|
|
36
|
+
const absolute = resolve(String(value || ""));
|
|
37
|
+
const real = realpathSync.native
|
|
38
|
+
? realpathSync.native(absolute)
|
|
39
|
+
: realpathSync(absolute);
|
|
40
|
+
return process.platform === "win32" ? real.toLowerCase() : real;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseRegisteredWorktrees(text) {
|
|
44
|
+
const entries = [];
|
|
45
|
+
let current = null;
|
|
46
|
+
for (const line of String(text || "").split(/\r?\n/)) {
|
|
47
|
+
if (line.startsWith("worktree ")) {
|
|
48
|
+
if (current) entries.push(current);
|
|
49
|
+
current = { path: line.slice("worktree ".length) };
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!current) continue;
|
|
53
|
+
if (line.startsWith("HEAD ")) current.head = line.slice("HEAD ".length);
|
|
54
|
+
if (line.startsWith("branch ")) {
|
|
55
|
+
current.branch = line.slice("branch ".length);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (current) entries.push(current);
|
|
59
|
+
return entries;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Verify that persisted worktree metadata still identifies the same linked
|
|
64
|
+
* worktree. Cleanup callers use this as a fail-closed identity check before
|
|
65
|
+
* invoking any removal primitive: a missing/corrupt path must never turn into
|
|
66
|
+
* a recursive delete of the repository checkout.
|
|
67
|
+
*
|
|
68
|
+
* @returns {{valid:boolean, reason:string}}
|
|
69
|
+
*/
|
|
70
|
+
export function validateAgentWorktree(info, { deps = _deps } = {}) {
|
|
71
|
+
const worktreePath = info?.path || info?.worktreePath;
|
|
72
|
+
if (!worktreePath || !info?.repoRoot || !info?.branch || !info?.baseSha) {
|
|
73
|
+
return { valid: false, reason: "incomplete worktree metadata" };
|
|
74
|
+
}
|
|
75
|
+
if (!/^[0-9a-f]{40,64}$/i.test(String(info.baseSha))) {
|
|
76
|
+
return { valid: false, reason: "invalid worktree base SHA" };
|
|
77
|
+
}
|
|
78
|
+
if (!existsSync(worktreePath) || !existsSync(info.repoRoot)) {
|
|
79
|
+
return { valid: false, reason: "worktree or repository path is missing" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const repo = canonicalPath(info.repoRoot);
|
|
84
|
+
const worktree = canonicalPath(worktreePath);
|
|
85
|
+
if (repo === worktree) {
|
|
86
|
+
return {
|
|
87
|
+
valid: false,
|
|
88
|
+
reason: "refusing to treat the main checkout as an agent worktree",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const repoTop = canonicalPath(
|
|
93
|
+
git("rev-parse --show-toplevel", info.repoRoot, deps).trim(),
|
|
94
|
+
);
|
|
95
|
+
const worktreeTop = canonicalPath(
|
|
96
|
+
git("rev-parse --show-toplevel", worktreePath, deps).trim(),
|
|
97
|
+
);
|
|
98
|
+
if (repoTop !== repo || worktreeTop !== worktree) {
|
|
99
|
+
return { valid: false, reason: "worktree path identity mismatch" };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const registered = parseRegisteredWorktrees(
|
|
103
|
+
git("worktree list --porcelain", info.repoRoot, deps),
|
|
104
|
+
).find((entry) => {
|
|
105
|
+
try {
|
|
106
|
+
return canonicalPath(entry.path) === worktree;
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
if (!registered) {
|
|
112
|
+
return {
|
|
113
|
+
valid: false,
|
|
114
|
+
reason: "path is not a registered worktree of the repository",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (registered.branch !== `refs/heads/${info.branch}`) {
|
|
118
|
+
return {
|
|
119
|
+
valid: false,
|
|
120
|
+
reason: "registered worktree branch does not match persisted metadata",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return { valid: true, reason: "verified" };
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return {
|
|
126
|
+
valid: false,
|
|
127
|
+
reason: `worktree identity could not be verified: ${error.message}`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function removeVerifiedAgentWorktree(info, deps) {
|
|
133
|
+
// Unit seams historically inject removeWorktree directly. Keep that seam,
|
|
134
|
+
// while production uses a non-forced git removal: if the tree changed
|
|
135
|
+
// between inspection and deletion, git refuses instead of discarding it.
|
|
136
|
+
if (deps.removeWorktree && deps.removeWorktree !== removeWorktree) {
|
|
137
|
+
deps.removeWorktree(info.repoRoot, info.path);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const run = deps.execFileSync || execFileSync;
|
|
142
|
+
run("git", ["worktree", "remove", info.path], {
|
|
143
|
+
cwd: info.repoRoot,
|
|
144
|
+
encoding: "utf8",
|
|
145
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
146
|
+
});
|
|
147
|
+
try {
|
|
148
|
+
run("git", ["branch", "-D", "--", info.branch], {
|
|
149
|
+
cwd: info.repoRoot,
|
|
150
|
+
encoding: "utf8",
|
|
151
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
152
|
+
});
|
|
153
|
+
return null;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
// The worktree itself is already gone. Preserve an honest warning rather
|
|
156
|
+
// than claiming the whole cleanup failed and that the path was kept.
|
|
157
|
+
return `branch cleanup failed: ${error.message}`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
28
161
|
/**
|
|
29
162
|
* @returns {{ path, branch, repoRoot, baseSha }}
|
|
30
163
|
*/
|
|
@@ -51,22 +184,57 @@ export function setupAgentWorktree({
|
|
|
51
184
|
export function finishAgentWorktree(info, { deps = _deps } = {}) {
|
|
52
185
|
if (!info) return { removed: false, kept: false, reason: "no worktree" };
|
|
53
186
|
|
|
187
|
+
// Production cleanup validates the persisted path against git's own
|
|
188
|
+
// worktree registry. Custom dependency bundles are test seams and retain the
|
|
189
|
+
// historical pure mocked behavior unless they provide validateWorktree.
|
|
190
|
+
const validation =
|
|
191
|
+
typeof deps.validateWorktree === "function"
|
|
192
|
+
? deps.validateWorktree(info)
|
|
193
|
+
: deps === _deps
|
|
194
|
+
? validateAgentWorktree(info, { deps })
|
|
195
|
+
: { valid: true, reason: "injected validation seam" };
|
|
196
|
+
if (!validation?.valid) {
|
|
197
|
+
return {
|
|
198
|
+
removed: false,
|
|
199
|
+
kept: true,
|
|
200
|
+
reason: `unverifiable worktree: ${validation?.reason || "unknown reason"}`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
54
204
|
let dirty = true;
|
|
55
205
|
let headSha = null;
|
|
206
|
+
let readable = false;
|
|
56
207
|
try {
|
|
57
208
|
dirty = git("status --porcelain", info.path, deps).trim().length > 0;
|
|
58
209
|
headSha = git("rev-parse HEAD", info.path, deps).trim();
|
|
210
|
+
readable = true;
|
|
59
211
|
} catch {
|
|
60
212
|
/* unreadable → keep, never destroy work we cannot verify */
|
|
61
213
|
}
|
|
62
214
|
|
|
215
|
+
if (!readable) {
|
|
216
|
+
return {
|
|
217
|
+
removed: false,
|
|
218
|
+
kept: true,
|
|
219
|
+
reason: "unverifiable worktree: git state could not be read",
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
63
223
|
const hasCommits = headSha !== null && headSha !== info.baseSha;
|
|
64
224
|
if (!dirty && !hasCommits) {
|
|
65
225
|
try {
|
|
66
|
-
|
|
67
|
-
return {
|
|
226
|
+
const warning = removeVerifiedAgentWorktree(info, deps);
|
|
227
|
+
return {
|
|
228
|
+
removed: true,
|
|
229
|
+
kept: false,
|
|
230
|
+
reason: warning ? `no changes; ${warning}` : "no changes",
|
|
231
|
+
};
|
|
68
232
|
} catch (err) {
|
|
69
|
-
return {
|
|
233
|
+
return {
|
|
234
|
+
removed: false,
|
|
235
|
+
kept: true,
|
|
236
|
+
reason: `cleanup failed: ${err.message}`,
|
|
237
|
+
};
|
|
70
238
|
}
|
|
71
239
|
}
|
|
72
240
|
return {
|
|
@@ -6,16 +6,21 @@ import {
|
|
|
6
6
|
openSync,
|
|
7
7
|
readFileSync,
|
|
8
8
|
readdirSync,
|
|
9
|
+
realpathSync,
|
|
9
10
|
renameSync,
|
|
10
11
|
rmSync,
|
|
11
12
|
statSync,
|
|
12
13
|
writeFileSync,
|
|
13
14
|
} from "node:fs";
|
|
14
15
|
import { randomBytes } from "node:crypto";
|
|
15
|
-
import { join } from "node:path";
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
16
17
|
import { fileURLToPath } from "node:url";
|
|
17
18
|
import { getHomeDir } from "./paths.js";
|
|
18
19
|
import { deriveSessionState } from "./session-lifecycle.js";
|
|
20
|
+
import {
|
|
21
|
+
finishAgentWorktree,
|
|
22
|
+
validateAgentWorktree,
|
|
23
|
+
} from "./agent-worktree.js";
|
|
19
24
|
|
|
20
25
|
export const DEFAULT_HEARTBEAT_INTERVAL_MS = 5000;
|
|
21
26
|
export const DEFAULT_HEARTBEAT_STALE_MS = 120000;
|
|
@@ -280,6 +285,20 @@ export function writeBackgroundAgentState(state) {
|
|
|
280
285
|
if (Number(current.pinnedAt || 0) > Number(next.pinnedAt || 0)) {
|
|
281
286
|
next = { ...next, pinned: current.pinned, pinnedAt: current.pinnedAt };
|
|
282
287
|
}
|
|
288
|
+
// A background worktree is immutable session identity, just like the
|
|
289
|
+
// conversation id. Heartbeat/finalize/rename writers may race with a stale
|
|
290
|
+
// snapshot, but no same-id transition is allowed to move the run back to
|
|
291
|
+
// the main checkout or drop the cleanup metadata.
|
|
292
|
+
if (current.worktreePath) {
|
|
293
|
+
next = {
|
|
294
|
+
...next,
|
|
295
|
+
cwd: current.worktreePath,
|
|
296
|
+
repoRoot: current.repoRoot,
|
|
297
|
+
worktreePath: current.worktreePath,
|
|
298
|
+
baseSha: current.baseSha,
|
|
299
|
+
branch: current.branch,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
283
302
|
}
|
|
284
303
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
285
304
|
writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
@@ -500,7 +519,7 @@ export function renameBackgroundAgent(id, title, options = {}) {
|
|
|
500
519
|
* conversation session is NOT touched — that's `cc session delete`.
|
|
501
520
|
*/
|
|
502
521
|
export function removeBackgroundAgent(id, options = {}) {
|
|
503
|
-
|
|
522
|
+
let state = effectiveBackgroundAgentState(readBackgroundAgentState(id), {
|
|
504
523
|
now: options.now,
|
|
505
524
|
heartbeatStaleMs: options.heartbeatStaleMs,
|
|
506
525
|
});
|
|
@@ -513,15 +532,28 @@ export function removeBackgroundAgent(id, options = {}) {
|
|
|
513
532
|
}
|
|
514
533
|
try {
|
|
515
534
|
stopBackgroundAgent(id);
|
|
535
|
+
state =
|
|
536
|
+
effectiveBackgroundAgentState(readBackgroundAgentState(id), {
|
|
537
|
+
now: options.now,
|
|
538
|
+
heartbeatStaleMs: options.heartbeatStaleMs,
|
|
539
|
+
}) || state;
|
|
516
540
|
} catch {
|
|
517
541
|
/* best-effort — the record removal below is the point of rm --force */
|
|
518
542
|
}
|
|
519
543
|
}
|
|
544
|
+
const worktree = cleanupBackgroundAgentWorktree(state, {
|
|
545
|
+
keepWorktree: options.keepWorktree === true,
|
|
546
|
+
});
|
|
520
547
|
rmSync(statePath(id), { force: true });
|
|
521
548
|
if (options.keepLog !== true) {
|
|
522
549
|
rmSync(logPath(id), { force: true });
|
|
523
550
|
}
|
|
524
|
-
return {
|
|
551
|
+
return {
|
|
552
|
+
id,
|
|
553
|
+
removed: true,
|
|
554
|
+
status: state.status,
|
|
555
|
+
...(worktree ? { worktree } : {}),
|
|
556
|
+
};
|
|
525
557
|
}
|
|
526
558
|
|
|
527
559
|
/**
|
|
@@ -598,6 +630,68 @@ export function buildFollowUpArgv(argv, opts = {}) {
|
|
|
598
630
|
return out;
|
|
599
631
|
}
|
|
600
632
|
|
|
633
|
+
function canonicalPath(value) {
|
|
634
|
+
const absolute = resolve(String(value || ""));
|
|
635
|
+
try {
|
|
636
|
+
const real = realpathSync.native
|
|
637
|
+
? realpathSync.native(absolute)
|
|
638
|
+
: realpathSync(absolute);
|
|
639
|
+
return process.platform === "win32" ? real.toLowerCase() : real;
|
|
640
|
+
} catch {
|
|
641
|
+
return process.platform === "win32" ? absolute.toLowerCase() : absolute;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Normalize and verify worktree metadata before it enters a background job.
|
|
647
|
+
* The metadata is persisted as top-level state fields for compatibility with
|
|
648
|
+
* status/IDE consumers: repoRoot/worktreePath/baseSha/branch.
|
|
649
|
+
*/
|
|
650
|
+
export function normalizeBackgroundWorktree(worktree, cwd) {
|
|
651
|
+
if (!worktree) return null;
|
|
652
|
+
const normalized = {
|
|
653
|
+
repoRoot: resolve(String(worktree.repoRoot || "")),
|
|
654
|
+
worktreePath: resolve(String(worktree.worktreePath || worktree.path || "")),
|
|
655
|
+
baseSha: String(worktree.baseSha || ""),
|
|
656
|
+
branch: String(worktree.branch || ""),
|
|
657
|
+
};
|
|
658
|
+
if (
|
|
659
|
+
!normalized.repoRoot ||
|
|
660
|
+
!normalized.worktreePath ||
|
|
661
|
+
!normalized.baseSha ||
|
|
662
|
+
!normalized.branch
|
|
663
|
+
) {
|
|
664
|
+
throw new Error(
|
|
665
|
+
"Cannot launch background agent: incomplete worktree metadata",
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
if (canonicalPath(cwd) !== canonicalPath(normalized.worktreePath)) {
|
|
669
|
+
throw new Error(
|
|
670
|
+
"Cannot launch background agent: cwd does not match the persisted worktree path",
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
const validation = validateAgentWorktree({
|
|
674
|
+
...normalized,
|
|
675
|
+
path: normalized.worktreePath,
|
|
676
|
+
});
|
|
677
|
+
if (!validation.valid) {
|
|
678
|
+
throw new Error(
|
|
679
|
+
`Cannot launch background agent: worktree metadata is not usable (${validation.reason})`,
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
return normalized;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function persistedWorktree(state) {
|
|
686
|
+
if (!state?.worktreePath) return null;
|
|
687
|
+
return {
|
|
688
|
+
repoRoot: state.repoRoot,
|
|
689
|
+
worktreePath: state.worktreePath,
|
|
690
|
+
baseSha: state.baseSha,
|
|
691
|
+
branch: state.branch,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
601
695
|
/**
|
|
602
696
|
* Continue a finished/crashed background session as a NEW background session
|
|
603
697
|
* on the same conversation (`cc daemon resume <id> <prompt>`). The original
|
|
@@ -622,14 +716,27 @@ export function resumeBackgroundAgent(id, prompt, options = {}) {
|
|
|
622
716
|
}
|
|
623
717
|
const text = String(prompt || "").trim();
|
|
624
718
|
if (!text) throw new Error("resume requires a prompt");
|
|
719
|
+
const worktree = persistedWorktree(state);
|
|
720
|
+
if (
|
|
721
|
+
worktree &&
|
|
722
|
+
options.cwd &&
|
|
723
|
+
canonicalPath(options.cwd) !== canonicalPath(worktree.worktreePath)
|
|
724
|
+
) {
|
|
725
|
+
throw new Error(
|
|
726
|
+
`Background agent ${id} is bound to worktree "${worktree.worktreePath}" and cannot resume in a different cwd`,
|
|
727
|
+
);
|
|
728
|
+
}
|
|
625
729
|
const argv = ["agent", "--session", state.sessionId, "-p", text];
|
|
626
730
|
return launchBackgroundAgent({
|
|
627
731
|
argv,
|
|
628
|
-
|
|
732
|
+
// Worktree sessions fail closed: never fall back to process.cwd(), which
|
|
733
|
+
// could silently resume a write task in the main checkout.
|
|
734
|
+
cwd: worktree?.worktreePath || options.cwd || state.cwd || process.cwd(),
|
|
629
735
|
sessionId: state.sessionId,
|
|
630
736
|
title: options.title || state.title || text.slice(0, 100),
|
|
631
737
|
cliEntry: options.cliEntry,
|
|
632
738
|
followUpArgv: ["agent", "--session", state.sessionId],
|
|
739
|
+
worktree,
|
|
633
740
|
});
|
|
634
741
|
}
|
|
635
742
|
|
|
@@ -667,8 +774,10 @@ export function launchBackgroundAgent({
|
|
|
667
774
|
title,
|
|
668
775
|
cliEntry,
|
|
669
776
|
followUpArgv,
|
|
777
|
+
worktree,
|
|
670
778
|
}) {
|
|
671
779
|
assertUsableCwd(cwd);
|
|
780
|
+
const worktreeState = normalizeBackgroundWorktree(worktree, cwd);
|
|
672
781
|
const id = createBackgroundAgentId();
|
|
673
782
|
const dir = backgroundAgentsDir();
|
|
674
783
|
const jobFile = join(dir, `${id}.job.${process.pid}.json`);
|
|
@@ -683,6 +792,7 @@ export function launchBackgroundAgent({
|
|
|
683
792
|
title: title || "Background agent",
|
|
684
793
|
cliEntry: cliEntry || process.argv[1],
|
|
685
794
|
logFile: logPath(id),
|
|
795
|
+
...(worktreeState || {}),
|
|
686
796
|
// Present = interactive attach can start follow-up turns; absent = the
|
|
687
797
|
// transport rejects prompts (log-only session).
|
|
688
798
|
...(Array.isArray(followUpArgv) ? { followUpArgv } : {}),
|
|
@@ -706,6 +816,7 @@ export function launchBackgroundAgent({
|
|
|
706
816
|
endedAt: null,
|
|
707
817
|
exitCode: null,
|
|
708
818
|
logFile: job.logFile,
|
|
819
|
+
...(worktreeState || {}),
|
|
709
820
|
};
|
|
710
821
|
writeBackgroundAgentState(state);
|
|
711
822
|
let child;
|
|
@@ -866,6 +977,84 @@ export function stopBackgroundAgent(id) {
|
|
|
866
977
|
return { ...next, stopped: true };
|
|
867
978
|
}
|
|
868
979
|
|
|
980
|
+
function activeWorktreeProcesses(state) {
|
|
981
|
+
const active = [];
|
|
982
|
+
if (
|
|
983
|
+
Number.isInteger(Number(state?.pid)) &&
|
|
984
|
+
Number(state.pid) > 0 &&
|
|
985
|
+
isSameProcess(state.pid, state.startedAt)
|
|
986
|
+
) {
|
|
987
|
+
active.push("worker");
|
|
988
|
+
}
|
|
989
|
+
if (
|
|
990
|
+
Number.isInteger(Number(state?.agentPid)) &&
|
|
991
|
+
Number(state.agentPid) > 0 &&
|
|
992
|
+
isSameProcess(state.agentPid, state.agentStartedAt)
|
|
993
|
+
) {
|
|
994
|
+
active.push("agent");
|
|
995
|
+
}
|
|
996
|
+
return active;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Explicit background-record cleanup owns the worktree teardown. Normal
|
|
1001
|
+
* completion, kill, lost-worker reconciliation, attach and resume all retain
|
|
1002
|
+
* it so a later resume continues in the exact same isolated checkout.
|
|
1003
|
+
*/
|
|
1004
|
+
export function cleanupBackgroundAgentWorktree(state, options = {}) {
|
|
1005
|
+
const worktree = persistedWorktree(state);
|
|
1006
|
+
if (!worktree) return null;
|
|
1007
|
+
if (options.keepWorktree === true) {
|
|
1008
|
+
return {
|
|
1009
|
+
removed: false,
|
|
1010
|
+
kept: true,
|
|
1011
|
+
reason: "kept by explicit request",
|
|
1012
|
+
path: worktree.worktreePath,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const blockers = [];
|
|
1017
|
+
const active = activeWorktreeProcesses(state);
|
|
1018
|
+
if (active.length) blockers.push(`active ${active.join("+")} process`);
|
|
1019
|
+
if (Number(state?.pendingApprovals || 0) > 0) {
|
|
1020
|
+
blockers.push("pending approval");
|
|
1021
|
+
}
|
|
1022
|
+
if (state?.pendingQuestion) blockers.push("pending user input");
|
|
1023
|
+
if (
|
|
1024
|
+
Number(state?.uncertainSideEffects || 0) > 0 ||
|
|
1025
|
+
state?.phase === "uncertain_side_effect" ||
|
|
1026
|
+
state?.phase === "uncertain-side-effect"
|
|
1027
|
+
) {
|
|
1028
|
+
blockers.push("unfinished or uncertain side effect");
|
|
1029
|
+
}
|
|
1030
|
+
if (blockers.length) {
|
|
1031
|
+
throw new Error(
|
|
1032
|
+
`Refusing to clean background worktree "${worktree.worktreePath}": ${blockers.join(", ")}`,
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (!existsSync(worktree.worktreePath)) {
|
|
1037
|
+
return {
|
|
1038
|
+
removed: false,
|
|
1039
|
+
kept: false,
|
|
1040
|
+
reason: "worktree path already missing",
|
|
1041
|
+
path: worktree.worktreePath,
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const result = finishAgentWorktree({
|
|
1046
|
+
...worktree,
|
|
1047
|
+
path: worktree.worktreePath,
|
|
1048
|
+
});
|
|
1049
|
+
if (!result.removed) {
|
|
1050
|
+
throw new Error(
|
|
1051
|
+
`Refusing to remove background agent record while its worktree is kept (${result.reason}). ` +
|
|
1052
|
+
"Resolve or preserve the work first, or pass --keep-worktree to remove only the record.",
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
return { ...result, path: worktree.worktreePath };
|
|
1056
|
+
}
|
|
1057
|
+
|
|
869
1058
|
export function openBackgroundLogFile(id) {
|
|
870
1059
|
const fd = openSync(logPath(id), "a", 0o600);
|
|
871
1060
|
return { fd, close: () => closeSync(fd) };
|