chainlesschain 0.162.162 → 0.162.163
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-ClI2ALgv.js → AIOps-BQ0kgfA1.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BDjRcJaK.js → ActionButton-Dr1MDANe.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DikSRsaT.js → Analytics-CZ5vsaXW.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-9mLhMFmz.js → AppLayout-BIBga881.js} +3 -3
- package/src/assets/web-panel/assets/{Artifacts-BSr88BrH.js → Artifacts-vpXHRU3u.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CdvyimOF.js → Audit-B8exuoub.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-D3_hCedh.js → BackgroundAgents-923evRRN.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-Cyvv9RHf.js → Backup-sr_sswlQ.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BZM14kt8.js → BaseInput-CnW-4XKT.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DrQSokVz.js → Chat-FZCsTZIL.js} +6 -6
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DDPd9qT0.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-bFLYeWMV.js → Checkbox-DFrxB59H.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-7mN7xJXT.js → Codegen-_b6WEel8.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DAB5ZWm7.js → Col-CkbnnZ-K.js} +1 -1
- package/src/assets/web-panel/assets/{Community-B_1TymaK.js → Community-BxM_82zW.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Bq1uSWLJ.js → Compact-DYinP45R.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CyiE1jEe.js → Compliance-DyXp3sSP.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Cpk0D7vm.js → Cowork-BEr2DhW7.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-d_Lv3rdB.js → Cron-B-Vwo1hN.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DMqhHjMR.js → Crosschain-DbLxKddd.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Br-oPuBw.js → DID-BsQ_LFw9.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CfLW_RaP.js → Dashboard-lVlXQHu5.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DZnTOExV.js → Dropdown-Beo_foO7.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DHIUHDv1.js → EmailListRenderer-DszHIXED.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Bw3t3myu.js → FamilyGuardDashboard-DcAwoh81.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-D9cpQPAY.js → Federation-CuJLa2eb.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Jdczr9Mr.js → FormItemContext-Cxd1lxhc.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BT8ZMpC5.js → GenericCardRenderer-BBvq5Jsw.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DjARip2b.js → Git-WqPKetx6.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BgWbSRcj.js → Governance-CWYtY3ke.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CfouJciM.js → Inference-BnX1iiNV.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-50l7mrAr.js → KnowledgeGraph-BJuM8GN5.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-rq50H84B.js → Logs-DqLMNntm.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-DUPIEdQH.js → MarkdownRenderer-BqYwEYou.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-BYgPDmHU.js → Marketplace-_vGfmhNl.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-5DVgcFdx.js → McpTools-BeJDOkDj.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-B70LPk7f.js → Memory-Da0EEZDO.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-De8ssA5Y.js → MobileBridge-DSuzcCCy.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-D0RAk4e8.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-RJsOTsIA.js → Mtc-BE9YgdA0.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-Crh-6BEK.js → MtcAudit-fC2BpO5S.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-wuoR5l3x.js → Multisig-D24Xxqw3.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-U2Mji4fZ.js → NLProgramming-Bs3oGvS8.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CdTZAyDH.js → Notes-BFjmAA-3.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-C-FucWE7.js → NotificationSettings-BX_9Bhv0.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DCYFTeaI.js → OrderTableRenderer-wjkZwJXI.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-an3OF85L.js → Organization-DBcdsVEP.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-C-1vmYFe.js → Overflow-DDyaF9vC.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Cwh47xKg.js → P2P-BB78L4he.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Dm38GT0k.js → PdhVaultBrowser-DqTiZFum.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DS2Y8awD.js → Permissions-INbnWv4j.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BQNziA0I.js → PersonalDataHub-BST-VmTq.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-DF85L1ue.js → Pipeline-CE7_tS2k.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BuLornF-.js → Privacy-hK5L8dOB.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-C3Y31IwX.js → ProjectInit-BvkkUoDn.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BCPiI8aD.js → ProjectSettings-u7_EUtkm.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BZ6s4k6g.js → Projects-5Fmt_8Nw.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-D3Wp022X.js → Providers-B9eanarR.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CUJfQkH4.js → QrScannerModal-C0dX5N2f.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-UAc0EO8l.js → QuickAsk-CKTpG-PZ.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BaxKupcs.js → Recommend-Px_w_XQM.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-C75NNnXZ.js → RemoteSession-Di0fnYkM.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-BVXynZAk.js → Reputation-vesnWjXt.js} +1 -1
- package/src/assets/web-panel/assets/{Row-C5MRZ0bi.js → Row-exbY6LkT.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-DAKkEYka.js → RssFeed-B3O3x70t.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DvWv9YOA.js → Search-uRA0DhNz.js} +1 -1
- package/src/assets/web-panel/assets/{Security-B5peafW1.js → Security-CViqdH6K.js} +4 -4
- package/src/assets/web-panel/assets/{Services-UQmvPEaG.js → Services-CkXS97WN.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-C3bhWpuF.js → Skeleton-DxW5WYnE.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DfkKf9_H.js → Skills-CLxmGFse.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DqMSjlBM.js → Sla-Cr5fziXq.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-C-WbxWUX.js → SpeechSettings-H0JSb3cp.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CYuHZ4N2.js → SyncSettings-DMYWMae7.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-CyeAyB9d.js → Tasks-CbYyDdF4.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BTCeQnpq.js → Templates-Cpc8JjiK.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BifQuA9Y.js → Tenant-CRh77Jda.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-Bx_9Bocf.js → Terminal-4D8tNBO5.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BX0RFCOm.js → TimelineRenderer-D41jzzbG.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CGHrCmUS.js → Tokens-C_jWEy6b.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DlqaY4SO.js → Trigger-Dk1ZUhsy.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DfOmj-ay.js → Trust-CLVy4l7m.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-B72jH5fE.js → UkeySign-C-FeqFDj.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BT-63fqv.js → VideoEditing-CqVZzyMm.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Dsul3sHr.js → Wallet-DX9BW9pu.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Izkwwdgy.js → WebAuthn-7hQDpWw_.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-D3FHSiH1.js → WorkflowEditor-6XqUAO0j.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CmVz5z3U.js → chat-5R_vkaJG.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CuLgyJBp.js → colors-huUPZdC-.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Ml1YkXpw.js → compact-item-CfJlgisE.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BvjJ48bs.js → createContext-jO8heofr.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DeIrCfH-.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DVC0157C.js → hasIn-DY8Vxqx7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJyN28Et.js → index-A48PtXw9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CudmwNBV.js → index-B300RPnT.js} +3 -3
- package/src/assets/web-panel/assets/{index-BbepULSc.js → index-B74OXFxC.js} +1 -1
- package/src/assets/web-panel/assets/{index-DYXHFr4r.js → index-BAON7TzU.js} +1 -1
- package/src/assets/web-panel/assets/{index-BzG-x9p4.js → index-BCqxyvqp.js} +1 -1
- package/src/assets/web-panel/assets/{index-wvb_FtaV.js → index-BHE8OIls.js} +1 -1
- package/src/assets/web-panel/assets/{index-QXzlT9KD.js → index-BQS-uPOK.js} +1 -1
- package/src/assets/web-panel/assets/{index-DukDVXCv.js → index-BRXOP09X.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFYVQMvu.js → index-BYLBPpt8.js} +1 -1
- package/src/assets/web-panel/assets/{index-BydjdZnl.js → index-BfCx4pFT.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmFNvNLZ.js → index-BliaT02i.js} +1 -1
- package/src/assets/web-panel/assets/{index-KMqhcF7l.js → index-BmqGerpC.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ck_52dz_.js → index-BsBtRWbA.js} +1 -1
- package/src/assets/web-panel/assets/{index-C229ABfy.js → index-BtEXSoPL.js} +1 -1
- package/src/assets/web-panel/assets/index-BxUKMRpP.js +1 -0
- package/src/assets/web-panel/assets/{index-C_H_S-DY.js → index-Bxax9vQ6.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0S8NzO6.js → index-C2iVSFpy.js} +1 -1
- package/src/assets/web-panel/assets/{index-BjHBpRYZ.js → index-CA6sO5VD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bzr0rB5T.js → index-CA90TZxG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DDGaOWBV.js → index-CLhBh-qE.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dt5KMjwJ.js → index-CRElRhyS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BbPtzB8M.js → index-Cbe9G0V4.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_vRgTl9.js → index-ChvHubX3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BpFJHiTv.js → index-Cis_Ip7H.js} +1 -1
- package/src/assets/web-panel/assets/{index-KjgKdZe-.js → index-Cjxbu68P.js} +1 -1
- package/src/assets/web-panel/assets/{index-vG4XSJ1d.js → index-CzKmteNS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BUt8kTIB.js → index-D0pet5Uy.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfuKP4qe.js → index-D1VLuww0.js} +1 -1
- package/src/assets/web-panel/assets/{index-4SBta3d7.js → index-DXBcvUnf.js} +1 -1
- package/src/assets/web-panel/assets/{index-DC_OI9H2.js → index-Db1cLWir.js} +1 -1
- package/src/assets/web-panel/assets/{index-DuGszaWY.js → index-DoI2sZm7.js} +1 -1
- package/src/assets/web-panel/assets/index-DscU9iPI.js +1 -0
- package/src/assets/web-panel/assets/{index-4wcgvPoF.js → index-KRHroryI.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3lD5qap.js → index-d1xkbtNz.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_EvG_GW.js → index-dKx57io7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CXkJH-lK.js → index-j3O2EEJe.js} +1 -1
- package/src/assets/web-panel/assets/{index-BiC6dQZG.js → index-p85YaaKw.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cw0Rjy87.js → index-v3pTaEQm.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_XuRgGn.js → index-vXs3B8GJ.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BDgd2YZ8.js → initDefaultProps-MCB7-raZ.js} +1 -1
- package/src/assets/web-panel/assets/{motion-ErtrXdYe.js → motion-UWDF9swV.js} +1 -1
- package/src/assets/web-panel/assets/{move-_GPPNCgs.js → move-CsTmgXZU.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DRLL-8V2.js → mtc-parser-fQy6KGLg.js} +1 -1
- package/src/assets/web-panel/assets/{omit-p86sl023.js → omit-woPnEjd1.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BTu7cMLH.js → pickAttrs-Bs2dZ1gf.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BEKz6Isr.js → placementArrow-C7EVgPkg.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Ck42c9MP.js → responsiveObserve-BH2kYCTN.js} +1 -1
- package/src/assets/web-panel/assets/{slide-1CEvuPsB.js → slide-BFBP2V_e.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BDfdx1GJ.js → statusUtils-xdDxDYse.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-B4K46m5w.js → styleChecker-aAWgPPQf.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DoIVpJNq.js → useFlexGapSupport-DkGSAWoO.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DdAWi-ln.js → useFs-BrOih40N.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-iUu6k8dx.js → usePersonalDataHub-eIYrgMqO.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DZz4R6Tw.js → vnode-B8lutNSE.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DXlf9sA3.js → zoom-CffNre7V.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +254 -23
- package/src/commands/agent.js +105 -11
- package/src/commands/context.js +125 -14
- package/src/commands/hook.js +181 -7
- package/src/commands/plugin.js +239 -1
- package/src/commands/review.js +347 -2
- package/src/data/changelog.json +9 -4
- package/src/gateways/ws/remote-session-protocol.js +39 -0
- package/src/gateways/ws/session-protocol.js +5 -2
- package/src/lib/agent-schedule-store.js +289 -10
- package/src/lib/channels/channel-manager.js +54 -2
- package/src/lib/cli-context-engineering.js +9 -2
- package/src/lib/context-breakdown.js +116 -0
- package/src/lib/doctor-checkup.js +52 -0
- package/src/lib/hook-event-log.cjs +193 -0
- package/src/lib/interaction-adapter.js +34 -4
- package/src/lib/json-schema-output.js +46 -12
- package/src/lib/json-schema-validate.js +143 -3
- package/src/lib/plugin-runtime/capability-consent.js +175 -0
- package/src/lib/plugin-runtime/policy.js +62 -0
- package/src/lib/plugin-runtime/scopes.js +20 -4
- package/src/lib/project-instructions.js +128 -0
- package/src/lib/repl-goal.js +132 -0
- package/src/lib/repl-rewind.js +52 -0
- package/src/lib/schedule-planner.js +17 -1
- package/src/lib/settings-hook-events.cjs +13 -0
- package/src/lib/settings-loader.cjs +42 -0
- package/src/lib/side-effect-ledger-store.js +88 -0
- package/src/lib/side-effect-ledger.js +296 -0
- package/src/lib/sub-agent-context.js +74 -5
- package/src/lib/subagent-inheritance.js +112 -0
- package/src/lib/turn-binding-store.js +119 -0
- package/src/repl/agent-repl.js +136 -0
- package/src/repl/permission-tier.js +28 -0
- package/src/runtime/agent-core.js +376 -10
- package/src/runtime/coding-agent-contract-shared.cjs +60 -1
- package/src/runtime/headless-runner.js +267 -0
- package/src/runtime/headless-stream.js +60 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-HwJR-C6B.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-D10VISps.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DhiGrbsh.js +0 -1
- package/src/assets/web-panel/assets/index-B7a1TrU6.js +0 -1
- package/src/assets/web-panel/assets/index-C2B1KRja.js +0 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin capability CONSENT (Phase 3 gap: capability declaration + re-consent).
|
|
3
|
+
*
|
|
4
|
+
* Complements version trust ([[trust.js]]): trust pins the exact CODE a plugin
|
|
5
|
+
* ships (re-consent on version bump); this pins the CAPABILITY SET the plugin is
|
|
6
|
+
* allowed to request (process / network / filesystem / mcp / monitor /
|
|
7
|
+
* credential). The two are orthogonal — a plugin can be trusted-to-run yet still
|
|
8
|
+
* WIDEN what it wants (add `network:*`, a new credential), which must re-prompt.
|
|
9
|
+
*
|
|
10
|
+
* Consent is keyed by the SET, not the version: once the user consents, later
|
|
11
|
+
* versions that keep the same (or a narrower) capability set stay consented; any
|
|
12
|
+
* WIDENING (a new grant) revokes consent until re-granted. This mirrors
|
|
13
|
+
* `consentRequiredForUpgrade` in [[capabilities.js]] exactly.
|
|
14
|
+
*
|
|
15
|
+
* Recorded in the USER data dir (never in the repo — a checked-in project file
|
|
16
|
+
* must not be able to pre-grant itself capabilities), keyed `<scope>:<name>` →
|
|
17
|
+
* { version, capabilities, consentedAt }. Pure over injected IO so unit tests
|
|
18
|
+
* never touch the real user-data dir.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from "fs";
|
|
22
|
+
import path from "path";
|
|
23
|
+
import { getElectronUserDataDir } from "../paths.js";
|
|
24
|
+
import {
|
|
25
|
+
normalizeCapabilities,
|
|
26
|
+
diffCapabilities,
|
|
27
|
+
describeCapabilities,
|
|
28
|
+
} from "./capabilities.js";
|
|
29
|
+
|
|
30
|
+
export const _deps = {
|
|
31
|
+
existsSync: fs.existsSync,
|
|
32
|
+
readFileSync: fs.readFileSync,
|
|
33
|
+
writeFileSync: fs.writeFileSync,
|
|
34
|
+
mkdirSync: fs.mkdirSync,
|
|
35
|
+
now: () => new Date().toISOString(),
|
|
36
|
+
// Injectable so unit tests never touch the real user-data dir.
|
|
37
|
+
storePath: () =>
|
|
38
|
+
path.join(getElectronUserDataDir(), "plugin-capability-consent.json"),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function consentKey(scope, name) {
|
|
42
|
+
return `${scope}:${name}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function loadConsentStore() {
|
|
46
|
+
try {
|
|
47
|
+
const p = _deps.storePath();
|
|
48
|
+
if (!_deps.existsSync(p)) return {};
|
|
49
|
+
const data = JSON.parse(_deps.readFileSync(p, "utf8"));
|
|
50
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
51
|
+
} catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function saveConsentStore(store) {
|
|
57
|
+
const p = _deps.storePath();
|
|
58
|
+
_deps.mkdirSync(path.dirname(p), { recursive: true });
|
|
59
|
+
_deps.writeFileSync(p, JSON.stringify(store, null, 2), "utf8");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Coerce a capability set to the canonical shape diffCapabilities needs.
|
|
64
|
+
* `normalizeCapabilities` is NOT idempotent — it re-parses `filesystem`/
|
|
65
|
+
* `credential` through `toList`, so feeding it an already-normalized set (e.g.
|
|
66
|
+
* one read back from the store) corrupts those lists. Detect an already-
|
|
67
|
+
* canonical set and pass it through unchanged; only normalize raw input.
|
|
68
|
+
*/
|
|
69
|
+
function canonicalCaps(caps) {
|
|
70
|
+
if (
|
|
71
|
+
caps &&
|
|
72
|
+
typeof caps === "object" &&
|
|
73
|
+
caps.network &&
|
|
74
|
+
typeof caps.network === "object" &&
|
|
75
|
+
"domains" in caps.network &&
|
|
76
|
+
caps.filesystem &&
|
|
77
|
+
"roots" in caps.filesystem &&
|
|
78
|
+
caps.credential &&
|
|
79
|
+
"names" in caps.credential
|
|
80
|
+
) {
|
|
81
|
+
return caps;
|
|
82
|
+
}
|
|
83
|
+
return normalizeCapabilities(caps);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** True when a capability set grants nothing (all-deny) — nothing to consent. */
|
|
87
|
+
export function capabilitiesAreEmpty(caps) {
|
|
88
|
+
return !diffCapabilities(null, canonicalCaps(caps)).widened;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* PURE consent status: given a plugin's DECLARED capabilities and its stored
|
|
93
|
+
* consent entry, decide whether consent covers them. Re-consent is required on
|
|
94
|
+
* a first request or any WIDENING (new grant) — not on a version bump alone
|
|
95
|
+
* (trust handles versions).
|
|
96
|
+
*
|
|
97
|
+
* @param {object} declaredCaps raw or normalized capabilities
|
|
98
|
+
* @param {object|null} entry stored `{ version, capabilities }` (or null)
|
|
99
|
+
* @returns {{consented:boolean, reason:string, added:string[]}}
|
|
100
|
+
*/
|
|
101
|
+
export function capabilityConsentStatus(declaredCaps, entry) {
|
|
102
|
+
const declared = canonicalCaps(declaredCaps);
|
|
103
|
+
if (!diffCapabilities(null, declared).widened) {
|
|
104
|
+
return { consented: true, reason: "no capabilities declared", added: [] };
|
|
105
|
+
}
|
|
106
|
+
if (!entry) {
|
|
107
|
+
return {
|
|
108
|
+
consented: false,
|
|
109
|
+
reason: "capabilities never consented",
|
|
110
|
+
added: diffCapabilities(null, declared).added,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Coerce the stored set to canonical too (raw hand-written entry, or an
|
|
114
|
+
// already-normalized one read from the store) before diffing.
|
|
115
|
+
const diff = diffCapabilities(canonicalCaps(entry.capabilities), declared);
|
|
116
|
+
if (diff.widened) {
|
|
117
|
+
return {
|
|
118
|
+
consented: false,
|
|
119
|
+
reason: "capabilities widened since last consent",
|
|
120
|
+
added: diff.added,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
consented: true,
|
|
125
|
+
reason: "consent covers declared capabilities",
|
|
126
|
+
added: [],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Load the store and report whether one plugin's declared caps are consented. */
|
|
131
|
+
export function isPluginCapabilityConsented(plugin, declaredCaps) {
|
|
132
|
+
if (!plugin || !plugin.name) return false;
|
|
133
|
+
const entry = loadConsentStore()[consentKey(plugin.scope, plugin.name)];
|
|
134
|
+
return capabilityConsentStatus(declaredCaps, entry).consented;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Record consent for a plugin's currently-declared capability set. */
|
|
138
|
+
export function consentPluginCapabilities(
|
|
139
|
+
name,
|
|
140
|
+
{ scope = "project", version, capabilities } = {},
|
|
141
|
+
) {
|
|
142
|
+
if (!version) throw new Error("consentPluginCapabilities requires a version");
|
|
143
|
+
const store = loadConsentStore();
|
|
144
|
+
store[consentKey(scope, name)] = {
|
|
145
|
+
version,
|
|
146
|
+
capabilities: canonicalCaps(capabilities),
|
|
147
|
+
consentedAt: _deps.now(),
|
|
148
|
+
};
|
|
149
|
+
saveConsentStore(store);
|
|
150
|
+
return { name, scope, version };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Revoke capability consent for a plugin at a scope. */
|
|
154
|
+
export function revokeCapabilityConsent(name, { scope = "project" } = {}) {
|
|
155
|
+
const store = loadConsentStore();
|
|
156
|
+
const key = consentKey(scope, name);
|
|
157
|
+
const existed = Object.prototype.hasOwnProperty.call(store, key);
|
|
158
|
+
delete store[key];
|
|
159
|
+
saveConsentStore(store);
|
|
160
|
+
return { name, scope, removed: existed };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** All consent entries (for `cc plugin consent --list`). */
|
|
164
|
+
export function listCapabilityConsent() {
|
|
165
|
+
return Object.entries(loadConsentStore()).map(([key, v]) => {
|
|
166
|
+
const idx = key.indexOf(":");
|
|
167
|
+
return {
|
|
168
|
+
scope: key.slice(0, idx),
|
|
169
|
+
name: key.slice(idx + 1),
|
|
170
|
+
version: v?.version || null,
|
|
171
|
+
consentedAt: v?.consentedAt || null,
|
|
172
|
+
capabilities: describeCapabilities(v?.capabilities),
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { enforcePluginPolicy } from "../plugin-security.js";
|
|
26
26
|
import { loadManagedSettings } from "../settings-loader.cjs";
|
|
27
27
|
import { verifyInstalledSignature } from "./signature.js";
|
|
28
|
+
import { isPluginCapabilityConsented } from "./capability-consent.js";
|
|
28
29
|
|
|
29
30
|
// Managed settings are org-admin controlled and effectively static for a
|
|
30
31
|
// session, and discoverPlugins is called by every collector — memoize the load
|
|
@@ -142,6 +143,67 @@ export function filterByManagedPolicy(plugins, managed) {
|
|
|
142
143
|
return { kept, dropped };
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Whether capability-consent must be ENFORCED at load time. The consent gate
|
|
148
|
+
* ([[capability-consent.js]]) already exists but was consulted only by `cc
|
|
149
|
+
* plugin consent`/`validate`/`doctor` — a plugin that declared capabilities and
|
|
150
|
+
* was never consented still loaded all six component types. Enforcement is
|
|
151
|
+
* OPT-IN (byte-identical default): an org sets `requirePluginCapabilityConsent`
|
|
152
|
+
* in managed settings, or a single user exports `CC_REQUIRE_PLUGIN_CONSENT=1`.
|
|
153
|
+
*
|
|
154
|
+
* @param {object|null} managed managed settings
|
|
155
|
+
* @param {object} [env] process env (injectable)
|
|
156
|
+
* @returns {boolean}
|
|
157
|
+
*/
|
|
158
|
+
export function capabilityConsentRequired(managed, env) {
|
|
159
|
+
const raw = (env || process.env).CC_REQUIRE_PLUGIN_CONSENT;
|
|
160
|
+
if (raw != null && /^(1|true|yes|on)$/i.test(String(raw).trim())) return true;
|
|
161
|
+
return (
|
|
162
|
+
managed?.requirePluginCapabilityConsent === true ||
|
|
163
|
+
managed?.requirePluginCapabilityConsent === "require"
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Drop plugins that DECLARE capabilities but whose current capability SET has
|
|
169
|
+
* not been consented (first request or any widening). Fail-closed: a consent
|
|
170
|
+
* check that throws drops the plugin. Legacy plugins that declare no
|
|
171
|
+
* `permissions` block are UNAFFECTED — the same "未声明 legacy 不受限" rule the
|
|
172
|
+
* declaration audit uses. A plugin whose declared set is empty (all-deny) is
|
|
173
|
+
* auto-consented by `isPluginCapabilityConsented`, so it stays.
|
|
174
|
+
*
|
|
175
|
+
* @param {Array<{name, scope, manifest}>} plugins
|
|
176
|
+
* @returns {{ kept: Array, dropped: Array<{name, reason}> }}
|
|
177
|
+
*/
|
|
178
|
+
export function filterByCapabilityConsent(plugins) {
|
|
179
|
+
const kept = [];
|
|
180
|
+
const dropped = [];
|
|
181
|
+
for (const p of plugins) {
|
|
182
|
+
if (!p.manifest?.capabilitiesDeclared) {
|
|
183
|
+
kept.push(p); // legacy plugin, no declaration → not gated
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
let consented = false;
|
|
187
|
+
try {
|
|
188
|
+
consented = isPluginCapabilityConsented(
|
|
189
|
+
{ name: p.name, scope: p.scope },
|
|
190
|
+
p.manifest.capabilities,
|
|
191
|
+
);
|
|
192
|
+
} catch {
|
|
193
|
+
consented = false; // fail closed
|
|
194
|
+
}
|
|
195
|
+
if (consented) {
|
|
196
|
+
kept.push(p);
|
|
197
|
+
} else {
|
|
198
|
+
dropped.push({
|
|
199
|
+
name: p.name,
|
|
200
|
+
reason: `capabilities not consented — run \`cc plugin consent ${p.name} --grant\``,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { kept, dropped };
|
|
205
|
+
}
|
|
206
|
+
|
|
145
207
|
let _warnedDropped = new Set();
|
|
146
208
|
|
|
147
209
|
/** One-time stderr warning naming plugins an org policy blocked from loading. */
|
|
@@ -27,6 +27,8 @@ import { parsePluginManifest } from "./manifest.js";
|
|
|
27
27
|
import {
|
|
28
28
|
loadManagedPluginPolicy,
|
|
29
29
|
filterByManagedPolicy,
|
|
30
|
+
filterByCapabilityConsent,
|
|
31
|
+
capabilityConsentRequired,
|
|
30
32
|
warnDroppedOnce,
|
|
31
33
|
} from "./policy.js";
|
|
32
34
|
|
|
@@ -160,10 +162,24 @@ export function discoverPlugins(opts = {}) {
|
|
|
160
162
|
env: opts.env,
|
|
161
163
|
managedSettingsFile: opts.managedSettingsFile,
|
|
162
164
|
});
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return
|
|
165
|
+
const consentGate = capabilityConsentRequired(managed, opts.env);
|
|
166
|
+
// Default (no managed policy AND consent enforcement off): byte-identical —
|
|
167
|
+
// return every discovered plugin, exactly as before.
|
|
168
|
+
if (!managed && !consentGate) return all;
|
|
169
|
+
let list = all;
|
|
170
|
+
if (managed) {
|
|
171
|
+
const { kept, dropped } = filterByManagedPolicy(list, managed);
|
|
172
|
+
if (dropped.length > 0) warnDroppedOnce(dropped);
|
|
173
|
+
list = kept;
|
|
174
|
+
}
|
|
175
|
+
// Capability-consent enforcement funnels through the SAME chokepoint, so an
|
|
176
|
+
// un-consented plugin loads NONE of its six component types (opt-in).
|
|
177
|
+
if (consentGate) {
|
|
178
|
+
const { kept, dropped } = filterByCapabilityConsent(list);
|
|
179
|
+
if (dropped.length > 0) warnDroppedOnce(dropped);
|
|
180
|
+
list = kept;
|
|
181
|
+
}
|
|
182
|
+
return list;
|
|
167
183
|
}
|
|
168
184
|
|
|
169
185
|
// ── internals ────────────────────────────────────────────────────────────
|
|
@@ -477,4 +477,132 @@ export function loadProjectInstructionsBlock(opts = {}) {
|
|
|
477
477
|
}
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
+
function isDir(fs, p) {
|
|
481
|
+
try {
|
|
482
|
+
return fs.statSync(p).isDirectory();
|
|
483
|
+
} catch {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Lazy subtree-instruction discovery (module 99 §5.3 "子目录指令按首次访问子树
|
|
490
|
+
* 懒加载"). Startup only loads the instruction chain from the project root DOWN
|
|
491
|
+
* to the cwd (see `findInstructionFiles`); a large monorepo's per-package
|
|
492
|
+
* `cc.md`/`CLAUDE.md`/`AGENTS.md` that sit BELOW the cwd are intentionally NOT
|
|
493
|
+
* injected up front — they cost tokens for subtrees a turn may never touch.
|
|
494
|
+
*
|
|
495
|
+
* This resolver answers "given a path a tool just accessed, which subtree
|
|
496
|
+
* instruction files should now be injected?" — the files in every directory
|
|
497
|
+
* strictly between `baseDir` (the already-loaded cwd) and the accessed path's
|
|
498
|
+
* directory, minus anything already loaded or excluded. Pure + deterministic;
|
|
499
|
+
* the tool-time injection itself is the caller's job.
|
|
500
|
+
*
|
|
501
|
+
* @param {object} opts
|
|
502
|
+
* @param {string} opts.repoRoot excludes are matched relative to this
|
|
503
|
+
* @param {string} opts.baseDir the startup cwd (its own file already loaded)
|
|
504
|
+
* @param {string} opts.accessedPath a file/dir a tool touched (abs or rel to baseDir)
|
|
505
|
+
* @param {Set<string>|string[]} [opts.alreadyLoaded] abs paths already injected
|
|
506
|
+
* @param {string[]} [opts.instructionExcludes]
|
|
507
|
+
* @returns {Array<{path:string, scope:"project", dir:string}>} shallowest-first
|
|
508
|
+
*/
|
|
509
|
+
export function resolveSubtreeInstructions(opts = {}) {
|
|
510
|
+
const { fs, path } = resolveDeps(opts);
|
|
511
|
+
const repoRoot = path.resolve(opts.repoRoot || opts.baseDir || process.cwd());
|
|
512
|
+
const baseDir = path.resolve(opts.baseDir || repoRoot);
|
|
513
|
+
const excludes = normalizeInstructionExcludes(opts.instructionExcludes);
|
|
514
|
+
const already =
|
|
515
|
+
opts.alreadyLoaded instanceof Set
|
|
516
|
+
? opts.alreadyLoaded
|
|
517
|
+
: new Set((opts.alreadyLoaded || []).map((p) => path.resolve(p)));
|
|
518
|
+
|
|
519
|
+
if (!opts.accessedPath) return [];
|
|
520
|
+
// Resolve the accessed path to a directory inside the subtree. A not-yet-
|
|
521
|
+
// created file (about to be written) stats as neither file nor dir → treat it
|
|
522
|
+
// as a file and use its parent.
|
|
523
|
+
let target = path.resolve(baseDir, opts.accessedPath);
|
|
524
|
+
if (!isDir(fs, target)) target = path.dirname(target);
|
|
525
|
+
|
|
526
|
+
// Subtree instructions apply only to DESCENDANTS of baseDir (ancestors were
|
|
527
|
+
// loaded at startup). Reject a target that escapes baseDir or the repo.
|
|
528
|
+
const relFromBase = path.relative(baseDir, target);
|
|
529
|
+
if (
|
|
530
|
+
!relFromBase ||
|
|
531
|
+
relFromBase.startsWith("..") ||
|
|
532
|
+
path.isAbsolute(relFromBase)
|
|
533
|
+
) {
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
const relFromRoot = path.relative(repoRoot, target);
|
|
537
|
+
if (relFromRoot.startsWith("..") || path.isAbsolute(relFromRoot)) return [];
|
|
538
|
+
|
|
539
|
+
// Directories strictly between baseDir (exclusive) and target (inclusive).
|
|
540
|
+
const dirs = [];
|
|
541
|
+
let dir = target;
|
|
542
|
+
while (dir !== baseDir) {
|
|
543
|
+
dirs.push(dir);
|
|
544
|
+
const parent = path.dirname(dir);
|
|
545
|
+
if (parent === dir) break; // hit fs root without meeting baseDir
|
|
546
|
+
dir = parent;
|
|
547
|
+
}
|
|
548
|
+
dirs.reverse(); // shallowest first
|
|
549
|
+
|
|
550
|
+
const out = [];
|
|
551
|
+
for (const d of dirs) {
|
|
552
|
+
const file = firstExisting(fs, path, d, PROJECT_FILE_NAMES);
|
|
553
|
+
if (!file) continue;
|
|
554
|
+
const abs = path.resolve(file);
|
|
555
|
+
if (already.has(abs)) continue;
|
|
556
|
+
if (
|
|
557
|
+
excludes.length > 0 &&
|
|
558
|
+
pathIsExcluded(path.relative(repoRoot, abs), excludes)
|
|
559
|
+
) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
out.push({ path: abs, scope: "project", dir: d });
|
|
563
|
+
}
|
|
564
|
+
return out;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Stateful companion to `resolveSubtreeInstructions`: tracks which subtree
|
|
569
|
+
* directories have already been injected across a session so a second access to
|
|
570
|
+
* the same subtree is a no-op. `onAccess(path)` returns ONLY the freshly
|
|
571
|
+
* discovered files. Fail-open (any error → []).
|
|
572
|
+
*/
|
|
573
|
+
export class SubtreeInstructionLoader {
|
|
574
|
+
constructor(opts = {}) {
|
|
575
|
+
const path = opts.deps?.path || pathDefault;
|
|
576
|
+
this._opts = opts;
|
|
577
|
+
this._loadedDirs = new Set();
|
|
578
|
+
this._loadedFiles = new Set(
|
|
579
|
+
(opts.alreadyLoaded || []).map((p) => path.resolve(p)),
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** @returns {Array<{path:string, scope:"project", dir:string}>} newly injected */
|
|
584
|
+
onAccess(accessedPath) {
|
|
585
|
+
try {
|
|
586
|
+
const found = resolveSubtreeInstructions({
|
|
587
|
+
...this._opts,
|
|
588
|
+
accessedPath,
|
|
589
|
+
alreadyLoaded: this._loadedFiles,
|
|
590
|
+
});
|
|
591
|
+
const fresh = found.filter((f) => !this._loadedDirs.has(f.dir));
|
|
592
|
+
for (const f of fresh) {
|
|
593
|
+
this._loadedDirs.add(f.dir);
|
|
594
|
+
this._loadedFiles.add(f.path);
|
|
595
|
+
}
|
|
596
|
+
return fresh;
|
|
597
|
+
} catch {
|
|
598
|
+
return [];
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Absolute paths injected so far (startup set + every onAccess discovery). */
|
|
603
|
+
loadedFiles() {
|
|
604
|
+
return [...this._loadedFiles];
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
480
608
|
export const _deps = { fs: fsDefault, path: pathDefault, os: osDefault };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL `/goal <condition>` — a SESSION completion condition for interactive use.
|
|
3
|
+
*
|
|
4
|
+
* Three distinct "goal" concepts, kept separate on purpose:
|
|
5
|
+
* - `cc goal` → long-term, cross-session OKR (goal-store.js).
|
|
6
|
+
* - headless --goal-condition → AUTO re-drives outer turns until met/exhausted.
|
|
7
|
+
* - REPL `/goal <cond>` → THIS: a human is present each turn, so it
|
|
8
|
+
* EVALUATES + REPORTS after every turn whether the condition is met. It does
|
|
9
|
+
* NOT auto-submit follow-ups (that autonomous loop belongs to headless, where
|
|
10
|
+
* no human is watching) — the user decides whether to keep going.
|
|
11
|
+
*
|
|
12
|
+
* This is the pure, clock/dep-injected core over the already-tested
|
|
13
|
+
* goal-condition-engine.js; the agent-repl wiring is thin display + eval glue.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
isDeterministicCondition,
|
|
18
|
+
runDeterministicCheck,
|
|
19
|
+
GoalConditionEngine,
|
|
20
|
+
GOAL_DECISION,
|
|
21
|
+
} from "./goal-condition-engine.js";
|
|
22
|
+
|
|
23
|
+
// Interactive sessions are human-paced, so the turn-count budget is generous —
|
|
24
|
+
// it exists only as a backstop (the engine hard-caps at 100 regardless). The
|
|
25
|
+
// meaningful signal in the REPL is met / not-yet-met, not budget exhaustion.
|
|
26
|
+
export const REPL_GOAL_MAX_OUTER_TURNS = 100;
|
|
27
|
+
|
|
28
|
+
/** Parse the `/goal` argument into an intent. Empty → status; a keyword → clear. */
|
|
29
|
+
export function parseGoalCommand(arg) {
|
|
30
|
+
const s = String(arg ?? "").trim();
|
|
31
|
+
if (!s) return { action: "status" };
|
|
32
|
+
if (s === "clear" || s === "off" || s === "stop" || s === "drop")
|
|
33
|
+
return { action: "clear" };
|
|
34
|
+
return { action: "set", spec: s };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create the REPL's session goal engine for a spec. Throws (via
|
|
39
|
+
* parseGoalCondition) on a malformed spec so the caller can report it.
|
|
40
|
+
*/
|
|
41
|
+
export function createReplGoal(spec, { now = () => 0 } = {}) {
|
|
42
|
+
return new GoalConditionEngine({
|
|
43
|
+
condition: spec,
|
|
44
|
+
budget: { maxOuterTurns: REPL_GOAL_MAX_OUTER_TURNS },
|
|
45
|
+
now,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Display lines shown when a goal is set. */
|
|
50
|
+
export function renderGoalStart(engine) {
|
|
51
|
+
return [
|
|
52
|
+
`◎ goal set: ${engine.condition.source}`,
|
|
53
|
+
` evaluated after each turn (≤ ${engine.budget.maxOuterTurns} turns); /goal clear to drop, /goal to check`,
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Display lines for `/goal` with no argument (status). */
|
|
58
|
+
export function renderGoalStatus(engine) {
|
|
59
|
+
if (!engine)
|
|
60
|
+
return [
|
|
61
|
+
"No session goal. Set one with /goal <condition>",
|
|
62
|
+
" e.g. /goal exit-zero:npm test · /goal file-exists:dist/app.js · /goal contains:ALL GREEN · /goal the tests pass",
|
|
63
|
+
];
|
|
64
|
+
const s = engine.state;
|
|
65
|
+
return [
|
|
66
|
+
`◎ goal: ${engine.condition.source}`,
|
|
67
|
+
` progress: outer turn ${s.outerTurns}/${engine.budget.maxOuterTurns}` +
|
|
68
|
+
(isDeterministicCondition(engine.condition)
|
|
69
|
+
? ` (${engine.condition.kind} check)`
|
|
70
|
+
: " (model-judged)"),
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Verdict display lines for one evaluation's decision + events. */
|
|
75
|
+
export function renderGoalVerdict(decision, events) {
|
|
76
|
+
const evaluated = (events || []).find((e) => e.type === "goal_evaluated");
|
|
77
|
+
if (decision === GOAL_DECISION.COMPLETE) {
|
|
78
|
+
const done = (events || []).find((e) => e.type === "goal_completed");
|
|
79
|
+
return [
|
|
80
|
+
`✔ goal met: ${done?.reason ?? evaluated?.reason ?? "condition met"}`,
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
if (decision === GOAL_DECISION.EXHAUSTED) {
|
|
84
|
+
const ex = (events || []).find((e) => e.type === "goal_exhausted");
|
|
85
|
+
return [
|
|
86
|
+
`⛔ goal dropped — ${ex?.reason ?? "budget exhausted"}${ex?.limit ? ` (${ex.limit})` : ""}`,
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
return [
|
|
90
|
+
`◎ goal not yet met: ${evaluated?.reason ?? "unmet"} · outer turn ${evaluated?.outerTurns ?? "?"}`,
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Evaluate the session goal against a completed turn's final text. Deterministic
|
|
96
|
+
* conditions run inline (contains/regex on the text; exit-zero/file-exists via
|
|
97
|
+
* injected `spawnSync`/`existsSync`); a model condition delegates to `judge`.
|
|
98
|
+
*
|
|
99
|
+
* @param {GoalConditionEngine} engine
|
|
100
|
+
* @param {string} finalText the assistant's answer this turn
|
|
101
|
+
* @param {object} deps { cwd, spawnSync, existsSync, judge, tokens, costUsd }
|
|
102
|
+
* judge(condition, {finalText}) => {met, reason, evidence}
|
|
103
|
+
* @returns {Promise<{decision, events, done, met}>}
|
|
104
|
+
*/
|
|
105
|
+
export async function evaluateReplGoalTurn(engine, finalText, deps = {}) {
|
|
106
|
+
engine.recordTurnUsage({ tokens: deps.tokens, costUsd: deps.costUsd });
|
|
107
|
+
const cond = engine.condition;
|
|
108
|
+
let evaluation;
|
|
109
|
+
if (isDeterministicCondition(cond)) {
|
|
110
|
+
evaluation = runDeterministicCheck(cond, {
|
|
111
|
+
lastOutput: finalText,
|
|
112
|
+
cwd: deps.cwd,
|
|
113
|
+
spawnSync: deps.spawnSync,
|
|
114
|
+
existsSync: deps.existsSync,
|
|
115
|
+
});
|
|
116
|
+
} else if (typeof deps.judge === "function") {
|
|
117
|
+
evaluation = await deps.judge(cond, { finalText });
|
|
118
|
+
} else {
|
|
119
|
+
evaluation = {
|
|
120
|
+
met: false,
|
|
121
|
+
reason: "no model judge available for this condition",
|
|
122
|
+
evidence: { kind: "model" },
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const { decision, events } = engine.evaluate(evaluation);
|
|
126
|
+
return {
|
|
127
|
+
decision,
|
|
128
|
+
events,
|
|
129
|
+
done: engine.done,
|
|
130
|
+
met: decision === GOAL_DECISION.COMPLETE,
|
|
131
|
+
};
|
|
132
|
+
}
|
package/src/lib/repl-rewind.js
CHANGED
|
@@ -25,6 +25,13 @@
|
|
|
25
25
|
* turn be matched to its pre-mutation snapshot purely from the count.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
+
import {
|
|
29
|
+
buildTurnBindingFromMarks,
|
|
30
|
+
resolveRestorePlan,
|
|
31
|
+
RESTORE_SCOPE,
|
|
32
|
+
TURN_COVERAGE,
|
|
33
|
+
} from "./turn-binding.js";
|
|
34
|
+
|
|
28
35
|
export const DEFAULT_LIST_LIMIT = 10;
|
|
29
36
|
export const PREVIEW_CHARS = 60;
|
|
30
37
|
|
|
@@ -219,6 +226,51 @@ export function restoreClearedConversation(messages, marks, cleared) {
|
|
|
219
226
|
};
|
|
220
227
|
}
|
|
221
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Coverage-aware restore plan for a rewound turn, derived from the REPL's
|
|
231
|
+
* EXISTING implicit state (`listUserTurns()` output + `{atMessageCount,id,tool}`
|
|
232
|
+
* marks) — no new agent-loop events required. Bridges those into the explicit
|
|
233
|
+
* turn-binding table and asks `resolveRestorePlan` for the honest plan, so
|
|
234
|
+
* `/rewind` can warn when a restore can't be fully promised (side-effects, a
|
|
235
|
+
* missing checkpoint, or a conversation/files drift) instead of silently doing a
|
|
236
|
+
* lossy rewind.
|
|
237
|
+
*
|
|
238
|
+
* @param {Array} messages live conversation (for listUserTurns)
|
|
239
|
+
* @param {Array<{atMessageCount:number,id:string,tool?:string}>} marks
|
|
240
|
+
* @param {number} turnIndex message index returned by rewindToTurn()
|
|
241
|
+
* @param {string} [scope] one of RESTORE_SCOPE (default BOTH)
|
|
242
|
+
* @returns {{scope, coverage, conversation, files, warnings:string[]}}
|
|
243
|
+
*/
|
|
244
|
+
export function buildRewindPlan(
|
|
245
|
+
messages,
|
|
246
|
+
marks,
|
|
247
|
+
turnIndex,
|
|
248
|
+
scope = RESTORE_SCOPE.BOTH,
|
|
249
|
+
) {
|
|
250
|
+
const turns = listUserTurns(messages, { limit: 1000 });
|
|
251
|
+
const log = buildTurnBindingFromMarks(turns, marks);
|
|
252
|
+
const turn = log.get(`turn-${Number(turnIndex)}`);
|
|
253
|
+
return resolveRestorePlan(turn, scope);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Human-readable lines for a rewind plan's coverage + warnings, for `/rewind` to
|
|
258
|
+
* print before it truncates. Returns [] for a fully-restorable turn with no
|
|
259
|
+
* warnings (nothing worth interrupting the user for).
|
|
260
|
+
*
|
|
261
|
+
* @param {{coverage:string, warnings:string[]}} plan from buildRewindPlan()
|
|
262
|
+
* @returns {string[]}
|
|
263
|
+
*/
|
|
264
|
+
export function renderRewindWarnings(plan) {
|
|
265
|
+
if (!plan) return [];
|
|
266
|
+
const lines = [];
|
|
267
|
+
if (plan.coverage && plan.coverage !== TURN_COVERAGE.FULL) {
|
|
268
|
+
lines.push(` coverage: ${plan.coverage} (restore is not guaranteed)`);
|
|
269
|
+
}
|
|
270
|
+
for (const w of plan.warnings || []) lines.push(` ⚠ ${w}`);
|
|
271
|
+
return lines;
|
|
272
|
+
}
|
|
273
|
+
|
|
222
274
|
/** Render the picker list (shared by /rewind and double-Esc). */
|
|
223
275
|
export function renderTurnList(turns) {
|
|
224
276
|
if (!turns.length) return " (no user turns yet)";
|
|
@@ -52,11 +52,27 @@ export function baseFireAt(entry) {
|
|
|
52
52
|
return Number.isFinite(n) ? n : null;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* The jitter window that governs an entry: a PER-ENTRY `entry.jitterMs` (a
|
|
57
|
+
* finite, non-negative number stored at create time) is authoritative and wins
|
|
58
|
+
* over the caller's `jitterMs` fallback; an entry without its own field falls
|
|
59
|
+
* back to the passed global. This lets each scheduled task carry its own spread
|
|
60
|
+
* — one noisy cron can jitter without perturbing the rest — while still honoring
|
|
61
|
+
* a daemon-wide default for entries that predate per-entry jitter.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveEntryJitterMs(entry, jitterMs = 0) {
|
|
64
|
+
const per = Number(entry?.jitterMs);
|
|
65
|
+
if (Number.isFinite(per) && per >= 0) return per;
|
|
66
|
+
return Number(jitterMs) || 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
/** Base fire time with the entry's deterministic jitter applied. */
|
|
56
70
|
export function effectiveFireAt(entry, jitterMs = 0) {
|
|
57
71
|
const base = baseFireAt(entry);
|
|
58
72
|
if (base == null) return null;
|
|
59
|
-
return
|
|
73
|
+
return (
|
|
74
|
+
base + jitterOffsetMs(entry?.id, resolveEntryJitterMs(entry, jitterMs))
|
|
75
|
+
);
|
|
60
76
|
}
|
|
61
77
|
|
|
62
78
|
/** Is an entry still pending/active (not in a terminal state)? */
|
|
@@ -19,12 +19,18 @@
|
|
|
19
19
|
const { collectHooks } = require("./settings-hooks.cjs");
|
|
20
20
|
const { runHooks } = require("./hook-runner.cjs");
|
|
21
21
|
const { buildHookEnvelope } = require("./hook-event-bus.cjs");
|
|
22
|
+
const hookEventLog = require("./hook-event-log.cjs");
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Stamp a unified-bus delivery id (event_id) onto a hook payload (P2 event bus).
|
|
25
26
|
* Additive — a hook that ignores it is unaffected; a hook that reads it gets a
|
|
26
27
|
* stable per-delivery id for correlation / `cc hook replay`. traceId/parentId
|
|
27
28
|
* are threaded when the caller provides them (else null).
|
|
29
|
+
*
|
|
30
|
+
* When `CC_HOOK_EVENT_LOG` is enabled, the FULL envelope is best-effort recorded
|
|
31
|
+
* to the hash-chained hook event log so `cc hook replay <event-id>` has a real
|
|
32
|
+
* source. Recording is opt-in and never throws, so the default firing path is
|
|
33
|
+
* byte-for-byte unchanged.
|
|
28
34
|
*/
|
|
29
35
|
function withDeliveryId(event, payload, { sessionId, traceId, parentId } = {}) {
|
|
30
36
|
const env = buildHookEnvelope({
|
|
@@ -34,6 +40,13 @@ function withDeliveryId(event, payload, { sessionId, traceId, parentId } = {}) {
|
|
|
34
40
|
traceId: traceId || null,
|
|
35
41
|
parentId: parentId || null,
|
|
36
42
|
});
|
|
43
|
+
if (hookEventLog.isHookEventLogEnabled()) {
|
|
44
|
+
try {
|
|
45
|
+
hookEventLog.appendHookEvent(env);
|
|
46
|
+
} catch {
|
|
47
|
+
/* best-effort — recording must not break the turn */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
37
50
|
return { ...payload, event_id: env.event_id };
|
|
38
51
|
}
|
|
39
52
|
|