chainlesschain 0.162.163 → 0.162.164
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 -1
- package/src/assets/web-panel/assets/{AIOps-BQ0kgfA1.js → AIOps-BqRkaHGb.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Dr1MDANe.js → ActionButton-DWO7OZLC.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CZ5vsaXW.js → Analytics-C5oaVezM.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-BIBga881.js → AppLayout-CsUESDoZ.js} +3 -3
- package/src/assets/web-panel/assets/{Artifacts-vpXHRU3u.js → Artifacts-Dragqu5k.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-B8exuoub.js → Audit-CQsaHctY.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-923evRRN.js → BackgroundAgents-DdG1jNSM.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-sr_sswlQ.js → Backup-DN6sjK4z.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CnW-4XKT.js → BaseInput-Cxm_ZTCl.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-FZCsTZIL.js → Chat-DVXGmA56.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-DDPd9qT0.js → ChatBubbleRenderer-BdaCL_iE.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-DFrxB59H.js → Checkbox-DEc4vD_A.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-_b6WEel8.js → Codegen-CKiTcv36.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CkbnnZ-K.js → Col-Br9UUNN0.js} +1 -1
- package/src/assets/web-panel/assets/{Community-BxM_82zW.js → Community-NeUH_70T.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DYinP45R.js → Compact-BqR-z2Df.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DyXp3sSP.js → Compliance-DcWB_SgZ.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BEr2DhW7.js → Cowork-BfhgcudF.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-B-Vwo1hN.js → Cron-LaU29LgD.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DbLxKddd.js → Crosschain-B2ZB9uwW.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BsQ_LFw9.js → DID-BuHl38A2.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-lVlXQHu5.js → Dashboard-m627yqvZ.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-Beo_foO7.js → Dropdown-C7bxF8F5.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DszHIXED.js → EmailListRenderer-DPLPQzLp.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DcAwoh81.js → FamilyGuardDashboard-cOiH6ike.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CuJLa2eb.js → Federation-Bvkzg88z.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Cxd1lxhc.js → FormItemContext-BTsiA7h_.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BBvq5Jsw.js → GenericCardRenderer-BGnVE5qu.js} +1 -1
- package/src/assets/web-panel/assets/{Git-WqPKetx6.js → Git-DS_j6r25.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CWYtY3ke.js → Governance-CKRG3Rsl.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BnX1iiNV.js → Inference-DW8INoNQ.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-BJuM8GN5.js → KnowledgeGraph-BhEYeFhC.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DqLMNntm.js → Logs-wqi0uxuR.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-BqYwEYou.js → MarkdownRenderer-DS8dE8ty.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-_vGfmhNl.js → Marketplace-CMlTuPLT.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BeJDOkDj.js → McpTools-CSx4m5JJ.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-Da0EEZDO.js → Memory-vnQCUy-_.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DSuzcCCy.js → MobileBridge-BV2NA742.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-Bw0k8e1B.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-BE9YgdA0.js → Mtc-CzK_FPB1.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-fC2BpO5S.js → MtcAudit-Cb7G9oL3.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-D24Xxqw3.js → Multisig-qJxlUyzr.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-Bs3oGvS8.js → NLProgramming-DqaBJ7Te.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BFjmAA-3.js → Notes-CwhTJkRi.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BX_9Bhv0.js → NotificationSettings-YDs4Fal4.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-wjkZwJXI.js → OrderTableRenderer-CvEALpq7.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-DBcdsVEP.js → Organization-CEmQm9n8.js} +2 -2
- package/src/assets/web-panel/assets/{Overflow-DDyaF9vC.js → Overflow-zby_P_LA.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BB78L4he.js → P2P-DsmDNFNT.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DqTiZFum.js → PdhVaultBrowser-FWwFlZGF.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-INbnWv4j.js → Permissions-DuzQ6me5.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BST-VmTq.js → PersonalDataHub-DZbeN613.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-CE7_tS2k.js → Pipeline-CoNSVAUS.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-hK5L8dOB.js → Privacy-OYJEAaMC.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BvkkUoDn.js → ProjectInit-CWIapV--.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-u7_EUtkm.js → ProjectSettings-CX8xMxZn.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-5Fmt_8Nw.js → Projects-BLCN8YyY.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-B9eanarR.js → Providers-BV_sxp75.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-C0dX5N2f.js → QrScannerModal-BTnRrHRp.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CKTpG-PZ.js → QuickAsk-D1KZg018.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Px_w_XQM.js → Recommend-COm5cuNg.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-Di0fnYkM.js → RemoteSession-0saWEkYI.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-vesnWjXt.js → Reputation-Dr7iKLS2.js} +1 -1
- package/src/assets/web-panel/assets/{Row-exbY6LkT.js → Row-C1vpPBcr.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B3O3x70t.js → RssFeed-CxlWrtNj.js} +2 -2
- package/src/assets/web-panel/assets/{Search-uRA0DhNz.js → Search-BT2V8dPN.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CViqdH6K.js → Security-bm9V3c5k.js} +4 -4
- package/src/assets/web-panel/assets/{Services-CkXS97WN.js → Services-CocH1yyX.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DxW5WYnE.js → Skeleton-DHItlMdd.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CLxmGFse.js → Skills-C59sLc4w.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-Cr5fziXq.js → Sla-Crj4BMc8.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-H0JSb3cp.js → SpeechSettings-DX4KFceY.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DMYWMae7.js → SyncSettings-BD_iwQLr.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-CbYyDdF4.js → Tasks-BmCTzYsH.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Cpc8JjiK.js → Templates-BGUaxZYl.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CRh77Jda.js → Tenant-BS0tJx2O.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-4D8tNBO5.js → Terminal--tHSAsld.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D41jzzbG.js → TimelineRenderer-D9-MFxS4.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-C_jWEy6b.js → Tokens-CiCB485f.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Dk1ZUhsy.js → Trigger-SmCFuW_l.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CLVy4l7m.js → Trust-DkK3Rqe0.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-C-FeqFDj.js → UkeySign-CDM4LGdB.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-CqVZzyMm.js → VideoEditing-IDB7DJXt.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DX9BW9pu.js → Wallet-Bikj5cmt.js} +2 -2
- package/src/assets/web-panel/assets/{WebAuthn-7hQDpWw_.js → WebAuthn-C9abWlAw.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-6XqUAO0j.js → WorkflowEditor-DrbRKVky.js} +1 -1
- package/src/assets/web-panel/assets/{chat-5R_vkaJG.js → chat-CccCSDQf.js} +1 -1
- package/src/assets/web-panel/assets/{colors-huUPZdC-.js → colors-CXObXLhb.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CfJlgisE.js → compact-item-B5kfYAxC.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-jO8heofr.js → createContext-DmiPBfip.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CvKrADNa.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DY8Vxqx7.js → hasIn-Q6lYteAc.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtEXSoPL.js → index-25Wlqps4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BsBtRWbA.js → index-33U29UNV.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQS-uPOK.js → index-4oXBDczR.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXBcvUnf.js → index-BGYXvMc5.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2iVSFpy.js → index-BJCCa0s_.js} +1 -1
- package/src/assets/web-panel/assets/{index-A48PtXw9.js → index-BJJ5DhKI.js} +1 -1
- package/src/assets/web-panel/assets/{index-Db1cLWir.js → index-BLOzViJ6.js} +1 -1
- package/src/assets/web-panel/assets/{index-vXs3B8GJ.js → index-BRrbUMoG.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHE8OIls.js → index-BUFpT17Q.js} +1 -1
- package/src/assets/web-panel/assets/{index-BmqGerpC.js → index-BkRDiqGc.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzKmteNS.js → index-BsfvW6my.js} +1 -1
- package/src/assets/web-panel/assets/{index-BAON7TzU.js → index-By-kfPED.js} +1 -1
- package/src/assets/web-panel/assets/{index-DoI2sZm7.js → index-BzFTcQsc.js} +1 -1
- package/src/assets/web-panel/assets/{index-j3O2EEJe.js → index-C2FXdboq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CRElRhyS.js → index-C5XOlTHN.js} +1 -1
- package/src/assets/web-panel/assets/{index-KRHroryI.js → index-CLhBDS2w.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bxax9vQ6.js → index-CMq7UeoT.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfCx4pFT.js → index-CVMTWNKl.js} +1 -1
- package/src/assets/web-panel/assets/{index-CA6sO5VD.js → index-Caghi-yL.js} +1 -1
- package/src/assets/web-panel/assets/{index-CA90TZxG.js → index-CdB6FuSH.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cbe9G0V4.js → index-CoK8-kWQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0pet5Uy.js → index-CsO3GUAY.js} +1 -1
- package/src/assets/web-panel/assets/{index-ChvHubX3.js → index-CwZBa20Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRXOP09X.js → index-D-kASucw.js} +1 -1
- package/src/assets/web-panel/assets/{index-B74OXFxC.js → index-D8eBw7vW.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1VLuww0.js → index-D9wgifk4.js} +1 -1
- package/src/assets/web-panel/assets/{index-p85YaaKw.js → index-DAYCTc07.js} +1 -1
- package/src/assets/web-panel/assets/{index-BliaT02i.js → index-DF6Q8l2h.js} +1 -1
- package/src/assets/web-panel/assets/index-DTdFUWc_.js +1 -0
- package/src/assets/web-panel/assets/{index-Cjxbu68P.js → index-DgTLApS9.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cis_Ip7H.js → index-DgxVJ8pg.js} +1 -1
- package/src/assets/web-panel/assets/{index-v3pTaEQm.js → index-DlJvIqoR.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYLBPpt8.js → index-Dw4eHfsG.js} +1 -1
- package/src/assets/web-panel/assets/{index-B300RPnT.js → index-Rh-ytwow.js} +3 -3
- package/src/assets/web-panel/assets/{index-CLhBh-qE.js → index-StAEt2pq.js} +1 -1
- package/src/assets/web-panel/assets/{index-d1xkbtNz.js → index-bzfw951k.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCqxyvqp.js → index-fSUMbMvv.js} +1 -1
- package/src/assets/web-panel/assets/{index-dKx57io7.js → index-tFHy5Wol.js} +1 -1
- package/src/assets/web-panel/assets/index-zQ0M_kz-.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-MCB7-raZ.js → initDefaultProps-BNq7X72N.js} +1 -1
- package/src/assets/web-panel/assets/{motion-UWDF9swV.js → motion-DOf9viTc.js} +1 -1
- package/src/assets/web-panel/assets/{move-CsTmgXZU.js → move-CRHTcW4x.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-fQy6KGLg.js → mtc-parser-Cyoozgm2.js} +1 -1
- package/src/assets/web-panel/assets/{omit-woPnEjd1.js → omit-6-TjFNru.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-Bs2dZ1gf.js → pickAttrs-DlUmO1zi.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-C7EVgPkg.js → placementArrow-CFOL8mIR.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BH2kYCTN.js → responsiveObserve-BtaFK5zd.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BFBP2V_e.js → slide-BIbc2bur.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-xdDxDYse.js → statusUtils-BXX9d6-w.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-aAWgPPQf.js → styleChecker-BGv10Bpn.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DkGSAWoO.js → useFlexGapSupport-bKnKT8tw.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BrOih40N.js → useFs-FY4IL4jn.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-eIYrgMqO.js → usePersonalDataHub-Bwl9TEgu.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-B8lutNSE.js → vnode-BM3OHxAT.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CffNre7V.js → zoom-D1fbFiHB.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +71 -14
- package/src/commands/doctor.js +85 -1
- package/src/commands/plugin.js +226 -9
- package/src/commands/review.js +17 -1
- package/src/commands/session.js +191 -0
- package/src/commands/team.js +19 -1
- package/src/data/changelog.json +5 -4
- package/src/gateways/ws/session-protocol.js +50 -0
- package/src/gateways/ws/ws-agent-handler.js +50 -0
- package/src/harness/jsonl-session-store.js +60 -0
- package/src/harness/worktree-isolator.js +140 -5
- package/src/lib/agent-schedule-store.js +30 -1
- package/src/lib/agent-team/team-worktree.js +37 -2
- package/src/lib/agent-tool-admission.js +125 -0
- package/src/lib/app-preview.js +360 -0
- package/src/lib/async-hook-supervisor.cjs +54 -0
- package/src/lib/capability-manifest.js +319 -0
- package/src/lib/capability-negotiation.js +23 -12
- package/src/lib/diagnostic-bundle.js +230 -0
- package/src/lib/doctor-checkup.js +223 -4
- package/src/lib/execution-location.js +286 -0
- package/src/lib/headless-manifest.js +13 -40
- package/src/lib/hook-stats-store.cjs +197 -0
- package/src/lib/idempotency.js +109 -0
- package/src/lib/jsonl-session-store.js +1 -0
- package/src/lib/lsp/__tests__/lsp-manager.test.js +58 -0
- package/src/lib/lsp/lsp-manager.js +56 -0
- package/src/lib/monitor-event.js +290 -0
- package/src/lib/operation-fingerprint.js +217 -0
- package/src/lib/plugin-runtime/bin.js +37 -0
- package/src/lib/plugin-runtime/capabilities.js +19 -0
- package/src/lib/plugin-runtime/capability-consent.js +28 -0
- package/src/lib/plugin-runtime/hooks.js +37 -0
- package/src/lib/plugin-runtime/lsp.js +38 -0
- package/src/lib/plugin-runtime/mcp.js +40 -0
- package/src/lib/plugin-runtime/monitors.js +37 -0
- package/src/lib/plugin-runtime/plugin-options.js +151 -0
- package/src/lib/pr-automation-policy.js +224 -0
- package/src/lib/remote-approval-bridge.js +66 -13
- package/src/lib/repl-rewind.js +101 -0
- package/src/lib/review-comment-anchor.js +241 -0
- package/src/lib/review-pipeline.js +46 -7
- package/src/lib/runtime-checkup.js +221 -0
- package/src/lib/secret-scan.js +409 -0
- package/src/lib/session-branch.js +0 -0
- package/src/lib/side-effect-ledger.js +105 -0
- package/src/lib/skill-loader.js +32 -0
- package/src/lib/skill-path-scope.js +63 -0
- package/src/lib/sub-agent-context.js +36 -4
- package/src/lib/sub-agent-registry.js +19 -0
- package/src/lib/telemetry/span-recorder.js +17 -4
- package/src/lib/terminal-context.js +195 -0
- package/src/lib/unattended-action-policy.js +182 -0
- package/src/lib/worktree-cleanup-safety.js +138 -0
- package/src/lib/worktree-isolator.js +8 -0
- package/src/lib/worktree-sparse.js +131 -0
- package/src/repl/agent-repl.js +152 -38
- package/src/runtime/agent-core.js +191 -13
- package/src/runtime/diagnostics.js +60 -0
- package/src/runtime/headless-runner.js +126 -1
- package/src/assets/web-panel/assets/MobileProjects-D0RAk4e8.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DeIrCfH-.js +0 -1
- package/src/assets/web-panel/assets/index-BxUKMRpP.js +0 -1
- package/src/assets/web-panel/assets/index-DscU9iPI.js +0 -1
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitor event envelope + delivery guards (P1 daemon Event Runtime —
|
|
3
|
+
* CLAUDE_CODE_CLI_INCREMENTAL_GAP_ANALYSIS_2026-07-12 §"守护进程事件运行时闭环").
|
|
4
|
+
*
|
|
5
|
+
* A Monitor watches a source (command stdout, file change, URL body, …) and
|
|
6
|
+
* "fires" when its condition matches. Before that firing reaches the agent as a
|
|
7
|
+
* turn it needs the same delivery guarantees Claude Code gives scheduled/event
|
|
8
|
+
* tasks:
|
|
9
|
+
*
|
|
10
|
+
* - **event_id** — a *deterministic* identity (no RNG) derived from the
|
|
11
|
+
* monitor id + the observation, so the same observation across a daemon
|
|
12
|
+
* restart hashes to the same id and can be de-duplicated.
|
|
13
|
+
* - **dedup window** — a time+count bounded set of recently-seen ids so a
|
|
14
|
+
* monitor that matches every poll does not re-fire the identical event.
|
|
15
|
+
* - **size cap** — oversized payloads are truncated (with a flag) so a
|
|
16
|
+
* runaway command output cannot blow up a turn / the audit log.
|
|
17
|
+
* - **backpressure** — a bounded queue that drops (and counts) events rather
|
|
18
|
+
* than growing without limit when the agent is slower than the source.
|
|
19
|
+
* - **authority** — a monitor event is an internal SYSTEM event: it tops out
|
|
20
|
+
* at `steer` (agent-authority.js) and can NEVER answer a permission gate.
|
|
21
|
+
*
|
|
22
|
+
* Pure + dependency-light (node:crypto + agent-authority.js) so any monitor
|
|
23
|
+
* firing seam — daemon runtime, `cc agenda run`, a future in-process runtime —
|
|
24
|
+
* can share exactly one implementation of these invariants.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { createHash } from "node:crypto";
|
|
28
|
+
import {
|
|
29
|
+
ORIGIN,
|
|
30
|
+
authorityForOrigin,
|
|
31
|
+
canApprove,
|
|
32
|
+
describeAuthorityChain,
|
|
33
|
+
} from "./agent-authority.js";
|
|
34
|
+
|
|
35
|
+
/** Default per-event payload cap (64 KiB) — matches the schedule store's audit budget. */
|
|
36
|
+
export const DEFAULT_MAX_EVENT_BYTES = 64 * 1024;
|
|
37
|
+
|
|
38
|
+
/** Default dedup window: an id seen within 5 minutes (or the last 1000 ids) is a repeat. */
|
|
39
|
+
export const DEFAULT_DEDUP_WINDOW_MS = 5 * 60 * 1000;
|
|
40
|
+
export const DEFAULT_DEDUP_MAX_ENTRIES = 1000;
|
|
41
|
+
|
|
42
|
+
/** Default bounded queue depth before backpressure drops kick in. */
|
|
43
|
+
export const DEFAULT_QUEUE_MAX_SIZE = 256;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A stable string form of an observation so structurally-equal observations
|
|
47
|
+
* hash identically and any real change hashes differently. Mirrors
|
|
48
|
+
* agent-authority's `normalizeToolArgs` intent (key-order independent).
|
|
49
|
+
*/
|
|
50
|
+
function stablePayload(payload) {
|
|
51
|
+
return stableStringify(payload === undefined ? null : payload);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stableStringify(value) {
|
|
55
|
+
if (value === null || typeof value !== "object") {
|
|
56
|
+
return JSON.stringify(value === undefined ? null : value);
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
return "[" + value.map(stableStringify).join(",") + "]";
|
|
60
|
+
}
|
|
61
|
+
const keys = Object.keys(value).sort();
|
|
62
|
+
return (
|
|
63
|
+
"{" +
|
|
64
|
+
keys
|
|
65
|
+
.map((k) => JSON.stringify(k) + ":" + stableStringify(value[k]))
|
|
66
|
+
.join(",") +
|
|
67
|
+
"}"
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Deterministic event id for a monitor observation. Same `(monitorId, payload)`
|
|
73
|
+
* → same id, forever, with no RNG — so a daemon restart re-observing the same
|
|
74
|
+
* thing is recognised as a repeat instead of re-firing.
|
|
75
|
+
*/
|
|
76
|
+
export function monitorEventId(monitorId, payload) {
|
|
77
|
+
const h = createHash("sha256");
|
|
78
|
+
h.update("cc-monitor-event-v1\n");
|
|
79
|
+
h.update(String(monitorId ?? "") + "\n");
|
|
80
|
+
h.update(stablePayload(payload) + "\n");
|
|
81
|
+
return "ev_" + h.digest("hex").slice(0, 24);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Cap a payload's serialized size. Returns `{ value, truncated, bytes }`.
|
|
86
|
+
* Strings are truncated by UTF-8 byte length; non-strings are JSON-serialized,
|
|
87
|
+
* and if the JSON is over budget the *serialized string* is truncated and
|
|
88
|
+
* returned (so the event still carries a bounded, human-readable snippet rather
|
|
89
|
+
* than an unbounded object).
|
|
90
|
+
*/
|
|
91
|
+
export function capEventPayload(payload, maxBytes = DEFAULT_MAX_EVENT_BYTES) {
|
|
92
|
+
const limit = Math.max(0, Number(maxBytes) || 0);
|
|
93
|
+
const asString =
|
|
94
|
+
typeof payload === "string" ? payload : safeJsonStringify(payload);
|
|
95
|
+
const bytes = Buffer.byteLength(asString, "utf8");
|
|
96
|
+
if (limit === 0 || bytes <= limit) {
|
|
97
|
+
return { value: payload, truncated: false, bytes };
|
|
98
|
+
}
|
|
99
|
+
const truncated = truncateUtf8(asString, limit);
|
|
100
|
+
return {
|
|
101
|
+
value: truncated,
|
|
102
|
+
truncated: true,
|
|
103
|
+
bytes: Buffer.byteLength(truncated, "utf8"),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function safeJsonStringify(value) {
|
|
108
|
+
try {
|
|
109
|
+
return JSON.stringify(value === undefined ? null : value);
|
|
110
|
+
} catch {
|
|
111
|
+
return String(value);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Truncate a string to at most `maxBytes` UTF-8 bytes without splitting a code point. */
|
|
116
|
+
function truncateUtf8(str, maxBytes) {
|
|
117
|
+
const buf = Buffer.from(str, "utf8");
|
|
118
|
+
if (buf.length <= maxBytes) return str;
|
|
119
|
+
let end = maxBytes;
|
|
120
|
+
// Back off if we would land in the middle of a multi-byte sequence.
|
|
121
|
+
while (end > 0 && (buf[end] & 0xc0) === 0x80) end--;
|
|
122
|
+
return buf.toString("utf8", 0, end);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The unforgeable authority envelope for a monitor firing. A monitor event is
|
|
127
|
+
* an internal SYSTEM observation: it may add a turn (`steer`) but can NEVER
|
|
128
|
+
* answer a permission gate — matching the channel/hook ceiling. `origin` is set
|
|
129
|
+
* here by WHAT produced the event (a monitor), never read from its content.
|
|
130
|
+
*
|
|
131
|
+
* Returns `{ event_id, monitorId, origin, authority, canApprove, provenance,
|
|
132
|
+
* at, truncated?, source? }` — a caller merges its own observation fields in.
|
|
133
|
+
*/
|
|
134
|
+
export function monitorEventEnvelope(
|
|
135
|
+
monitorId,
|
|
136
|
+
payload,
|
|
137
|
+
{ at = null, source = null, maxBytes = DEFAULT_MAX_EVENT_BYTES } = {},
|
|
138
|
+
) {
|
|
139
|
+
const capped = capEventPayload(payload, maxBytes);
|
|
140
|
+
const envelope = {
|
|
141
|
+
origin: ORIGIN.SYSTEM,
|
|
142
|
+
principalId: monitorId != null ? String(monitorId) : null,
|
|
143
|
+
correlationId: source != null ? String(source) : null,
|
|
144
|
+
};
|
|
145
|
+
return {
|
|
146
|
+
event_id: monitorEventId(monitorId, payload),
|
|
147
|
+
monitorId: monitorId != null ? String(monitorId) : null,
|
|
148
|
+
source: source != null ? String(source) : null,
|
|
149
|
+
at: at != null ? Number(at) : null,
|
|
150
|
+
payload: capped.value,
|
|
151
|
+
truncated: capped.truncated,
|
|
152
|
+
origin: envelope.origin,
|
|
153
|
+
authority: authorityForOrigin(ORIGIN.SYSTEM, envelope), // always "steer"
|
|
154
|
+
canApprove: canApprove(envelope), // always false
|
|
155
|
+
provenance: describeAuthorityChain(envelope),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A time + count bounded set of recently-seen event ids. `shouldEmit(id, now)`
|
|
161
|
+
* records the id and returns `true` only when it is NOT a repeat within the
|
|
162
|
+
* window — so a monitor matching every poll fires the identical event once.
|
|
163
|
+
* Eviction is by age (older than `windowMs`) and by count (`maxEntries`, oldest
|
|
164
|
+
* first), both bounded so memory never grows without limit.
|
|
165
|
+
*/
|
|
166
|
+
export class DedupWindow {
|
|
167
|
+
constructor({
|
|
168
|
+
windowMs = DEFAULT_DEDUP_WINDOW_MS,
|
|
169
|
+
maxEntries = DEFAULT_DEDUP_MAX_ENTRIES,
|
|
170
|
+
} = {}) {
|
|
171
|
+
this._windowMs = Math.max(0, Number(windowMs) || 0);
|
|
172
|
+
this._maxEntries = Math.max(1, Number(maxEntries) || 1);
|
|
173
|
+
// Insertion-ordered id → lastSeen epoch-ms (Map preserves insertion order).
|
|
174
|
+
this._seen = new Map();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Drop entries older than the window relative to `now`. A full sweep (no
|
|
179
|
+
* early break): refreshing an id updates its timestamp but keeps its Map
|
|
180
|
+
* position, so timestamps are NOT monotonic in iteration order and an early
|
|
181
|
+
* break could skip a genuinely-expired later entry — which would then wrongly
|
|
182
|
+
* suppress its legitimate re-emit. The map is bounded by `maxEntries`, so a
|
|
183
|
+
* full pass is cheap and strictly correct.
|
|
184
|
+
*/
|
|
185
|
+
_evictExpired(now) {
|
|
186
|
+
if (this._windowMs === 0) return;
|
|
187
|
+
const cutoff = now - this._windowMs;
|
|
188
|
+
for (const [id, ts] of this._seen) {
|
|
189
|
+
if (ts <= cutoff) this._seen.delete(id);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Enforce the count cap by evicting the oldest entries. */
|
|
194
|
+
_evictOverflow() {
|
|
195
|
+
while (this._seen.size > this._maxEntries) {
|
|
196
|
+
const oldest = this._seen.keys().next().value;
|
|
197
|
+
this._seen.delete(oldest);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* True if `eventId` is new within the window (and records it); false if it is
|
|
203
|
+
* a repeat (refreshes its timestamp so a steady stream keeps it suppressed).
|
|
204
|
+
*/
|
|
205
|
+
shouldEmit(eventId, now = 0) {
|
|
206
|
+
const nowMs = Number(now) || 0;
|
|
207
|
+
this._evictExpired(nowMs);
|
|
208
|
+
if (this._seen.has(eventId)) {
|
|
209
|
+
// Refresh recency without changing insertion order semantics for eviction.
|
|
210
|
+
this._seen.set(eventId, nowMs);
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
this._seen.set(eventId, nowMs);
|
|
214
|
+
this._evictOverflow();
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Number of live (un-evicted) ids currently tracked. */
|
|
219
|
+
get size() {
|
|
220
|
+
return this._seen.size;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Forget everything (e.g. on monitor removal). */
|
|
224
|
+
clear() {
|
|
225
|
+
this._seen.clear();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A bounded event queue providing backpressure: once full, a `push` drops an
|
|
231
|
+
* event rather than growing the queue, and counts what it dropped so the loss
|
|
232
|
+
* is observable (and auditable) instead of silent.
|
|
233
|
+
*
|
|
234
|
+
* - `dropPolicy: "oldest"` (default) — evict the head to admit the newcomer
|
|
235
|
+
* (keep the freshest signal; good for state monitors).
|
|
236
|
+
* - `dropPolicy: "newest"` — reject the newcomer (preserve order; good when
|
|
237
|
+
* the first observations matter most).
|
|
238
|
+
*/
|
|
239
|
+
export class BoundedEventQueue {
|
|
240
|
+
constructor({
|
|
241
|
+
maxSize = DEFAULT_QUEUE_MAX_SIZE,
|
|
242
|
+
dropPolicy = "oldest",
|
|
243
|
+
} = {}) {
|
|
244
|
+
this._maxSize = Math.max(1, Number(maxSize) || 1);
|
|
245
|
+
this._dropPolicy = dropPolicy === "newest" ? "newest" : "oldest";
|
|
246
|
+
this._items = [];
|
|
247
|
+
this._dropped = 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Push an event. Returns `{ accepted, dropped }` where `dropped` is the event
|
|
252
|
+
* evicted/rejected to stay within bounds (or null when nothing was dropped).
|
|
253
|
+
*/
|
|
254
|
+
push(event) {
|
|
255
|
+
if (this._items.length < this._maxSize) {
|
|
256
|
+
this._items.push(event);
|
|
257
|
+
return { accepted: true, dropped: null };
|
|
258
|
+
}
|
|
259
|
+
if (this._dropPolicy === "newest") {
|
|
260
|
+
this._dropped++;
|
|
261
|
+
return { accepted: false, dropped: event };
|
|
262
|
+
}
|
|
263
|
+
// oldest: evict head, admit newcomer.
|
|
264
|
+
const dropped = this._items.shift();
|
|
265
|
+
this._items.push(event);
|
|
266
|
+
this._dropped++;
|
|
267
|
+
return { accepted: true, dropped };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Remove and return the next event, or undefined when empty. */
|
|
271
|
+
shift() {
|
|
272
|
+
return this._items.shift();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Remove and return every queued event, oldest first. */
|
|
276
|
+
drain() {
|
|
277
|
+
const out = this._items;
|
|
278
|
+
this._items = [];
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
get size() {
|
|
283
|
+
return this._items.length;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Total events dropped due to backpressure since construction. */
|
|
287
|
+
get dropped() {
|
|
288
|
+
return this._dropped;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -93,3 +93,220 @@ export function approvalFingerprintOk(pending, resolved) {
|
|
|
93
93
|
if (!pending) return true; // nothing to bind against (legacy pending ask)
|
|
94
94
|
return fingerprintsMatch(pending, resolved);
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
// ── §8.2 cross-device operation fingerprint ──────────────────────────────────
|
|
98
|
+
//
|
|
99
|
+
// The fingerprint above binds tool + params only. IDE gap §8.2 requires a
|
|
100
|
+
// CROSS-DEVICE identity that also covers target environment, workspace, session,
|
|
101
|
+
// policy VERSION and a validity window, plus a fail-closed resolver: offline
|
|
102
|
+
// replay, multi-card concurrency, timeout and duplicate submission must all be
|
|
103
|
+
// rejected. The functions below add that richer tuple WITHOUT changing the
|
|
104
|
+
// legacy fingerprint (existing devices/tests keep working).
|
|
105
|
+
//
|
|
106
|
+
// Two layers of identity:
|
|
107
|
+
// - the LOGICAL key = tool + params + env + workspace + session + policyVersion
|
|
108
|
+
// (what the human is approving). Any change here invalidates an old approval.
|
|
109
|
+
// - the FINGERPRINT = the logical key PLUS the validity window, so a card
|
|
110
|
+
// re-issued with a fresh window is a distinct fingerprint (an old card can
|
|
111
|
+
// never be replayed) while still mapping to the same logical operation.
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The logical-operation key preimage: everything the human is authorizing,
|
|
115
|
+
* EXCLUDING the validity window. Two asks with this equal are the same
|
|
116
|
+
* operation (so re-issuing one supersedes the other). Deterministic.
|
|
117
|
+
* @param {object} desc
|
|
118
|
+
*/
|
|
119
|
+
export function operationDescriptorKey(desc = {}) {
|
|
120
|
+
return canonicalize({
|
|
121
|
+
tool: desc.toolName ?? desc.tool ?? null,
|
|
122
|
+
params: desc.params !== undefined ? desc.params : (desc.detail ?? null),
|
|
123
|
+
targetEnv: desc.targetEnv ?? null,
|
|
124
|
+
workspace: desc.workspace ?? null,
|
|
125
|
+
session: desc.session ?? null,
|
|
126
|
+
policyVersion: desc.policyVersion ?? null,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function intOrNull(value) {
|
|
131
|
+
const n = Number(value);
|
|
132
|
+
return Number.isFinite(n) ? n : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The full §8.2 cross-device fingerprint: the logical key plus the validity
|
|
137
|
+
* window `[notBefore, notAfter]`. Prefixed `opf_` (40 hex) so it is never
|
|
138
|
+
* confused with the 32-hex legacy fingerprint in a log or on the wire.
|
|
139
|
+
*
|
|
140
|
+
* @param {object} desc
|
|
141
|
+
* @param {string} [desc.toolName]
|
|
142
|
+
* @param {*} [desc.params]
|
|
143
|
+
* @param {string} [desc.targetEnv] e.g. local / wsl / ssh:host / container
|
|
144
|
+
* @param {string} [desc.workspace]
|
|
145
|
+
* @param {string} [desc.session]
|
|
146
|
+
* @param {string} [desc.policyVersion] a REAL policy version, not a rule label
|
|
147
|
+
* @param {number} [desc.notBefore] ms epoch the approval becomes valid
|
|
148
|
+
* @param {number} [desc.notAfter] ms epoch the approval expires
|
|
149
|
+
* @returns {string} `opf_` + 40 lowercase hex
|
|
150
|
+
*/
|
|
151
|
+
export function computeOperationFingerprint(desc = {}) {
|
|
152
|
+
const preimage =
|
|
153
|
+
"cc-operation-fingerprint-v2\n" +
|
|
154
|
+
operationDescriptorKey(desc) +
|
|
155
|
+
"\n" +
|
|
156
|
+
canonicalize({
|
|
157
|
+
notBefore: intOrNull(desc.notBefore),
|
|
158
|
+
notAfter: intOrNull(desc.notAfter),
|
|
159
|
+
});
|
|
160
|
+
return (
|
|
161
|
+
"opf_" +
|
|
162
|
+
createHash("sha256").update(preimage, "utf8").digest("hex").slice(0, 40)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* A short, human-verifiable id for the approval card — the operator eyeballs it
|
|
168
|
+
* on the device against the terminal to confirm they answer the SAME card. Two
|
|
169
|
+
* groups of 4 uppercase hex from the fingerprint (never the secret params).
|
|
170
|
+
* @param {string} fingerprint
|
|
171
|
+
* @returns {string} e.g. "A1B2-C3D4"
|
|
172
|
+
*/
|
|
173
|
+
export function shortOperationId(fingerprint) {
|
|
174
|
+
const hex = String(fingerprint || "").replace(/^opf_/, "");
|
|
175
|
+
const s = (hex.slice(0, 8) || "00000000").toUpperCase();
|
|
176
|
+
return `${s.slice(0, 4)}-${s.slice(4, 8)}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* A readable, SECRET-FREE one-line summary for the approval card. It NEVER
|
|
181
|
+
* echoes param VALUES (a shell command / token could be secret) — only the
|
|
182
|
+
* tool, the param key names (or an opaque marker), and the target coordinates.
|
|
183
|
+
* @param {object} desc
|
|
184
|
+
* @returns {string}
|
|
185
|
+
*/
|
|
186
|
+
export function summarizeOperation(desc = {}) {
|
|
187
|
+
const tool = String(desc.toolName ?? desc.tool ?? "operation");
|
|
188
|
+
let paramHint = "";
|
|
189
|
+
const p = desc.params;
|
|
190
|
+
if (p && typeof p === "object" && !Array.isArray(p)) {
|
|
191
|
+
const keys = Object.keys(p).sort();
|
|
192
|
+
paramHint = keys.length ? `(${keys.join(", ")})` : "()";
|
|
193
|
+
} else if (p !== undefined && p !== null && p !== "") {
|
|
194
|
+
paramHint = "(…)"; // opaque scalar/string/array — do not reveal the value
|
|
195
|
+
}
|
|
196
|
+
const coords = [];
|
|
197
|
+
if (desc.workspace) coords.push(`ws:${desc.workspace}`);
|
|
198
|
+
if (desc.targetEnv) coords.push(`env:${desc.targetEnv}`);
|
|
199
|
+
if (desc.session) coords.push(`sess:${desc.session}`);
|
|
200
|
+
if (desc.policyVersion) coords.push(`pol:${desc.policyVersion}`);
|
|
201
|
+
return `${tool}${paramHint}${coords.length ? " · " + coords.join(" · ") : ""}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Fail-closed verdict for resolving one pending approval. Every rejection is
|
|
206
|
+
* named so the audit log records WHY. Ordering matters: a missing input or a
|
|
207
|
+
* superseded/duplicate card is rejected before the fingerprint compare so a
|
|
208
|
+
* stale card can never settle a gate.
|
|
209
|
+
*
|
|
210
|
+
* @param {object} pending {fingerprint, notBefore?, notAfter?, resolved?, supersededBy?}
|
|
211
|
+
* @param {object} resolution {fingerprint}
|
|
212
|
+
* @param {object} [opts]
|
|
213
|
+
* @param {number} [opts.now] ms epoch (injected clock; required for window checks)
|
|
214
|
+
* @returns {{ok:boolean, reason:(string|null)}}
|
|
215
|
+
*/
|
|
216
|
+
export function resolveOperationApproval(pending, resolution, opts = {}) {
|
|
217
|
+
if (!pending || typeof pending !== "object")
|
|
218
|
+
return { ok: false, reason: "no-pending" };
|
|
219
|
+
if (!resolution || typeof resolution !== "object")
|
|
220
|
+
return { ok: false, reason: "no-resolution" };
|
|
221
|
+
if (pending.resolved) return { ok: false, reason: "duplicate" };
|
|
222
|
+
if (pending.supersededBy != null) return { ok: false, reason: "superseded" };
|
|
223
|
+
|
|
224
|
+
if (!fingerprintsMatch(pending.fingerprint, resolution.fingerprint)) {
|
|
225
|
+
return { ok: false, reason: "fingerprint-mismatch" };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const now = intOrNull(opts.now);
|
|
229
|
+
const nb = intOrNull(pending.notBefore);
|
|
230
|
+
const na = intOrNull(pending.notAfter);
|
|
231
|
+
if (nb != null || na != null) {
|
|
232
|
+
if (now == null) return { ok: false, reason: "no-clock" }; // can't prove validity → closed
|
|
233
|
+
if (nb != null && now < nb) return { ok: false, reason: "not-yet-valid" };
|
|
234
|
+
if (na != null && now > na) return { ok: false, reason: "expired" };
|
|
235
|
+
}
|
|
236
|
+
return { ok: true, reason: null };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* In-memory registry that enforces the §8.2 cross-device guarantees for a set
|
|
241
|
+
* of live approval cards: single-winner across concurrent cards for one logical
|
|
242
|
+
* operation (multi-card), at-most-once (duplicate submission), and validity
|
|
243
|
+
* window (timeout / not-yet-valid). Pure aside from an injected `clock` — no
|
|
244
|
+
* fs/RNG — so it is fully unit-testable and serializable alongside a session.
|
|
245
|
+
*/
|
|
246
|
+
export class OperationApprovalRegistry {
|
|
247
|
+
constructor({ clock } = {}) {
|
|
248
|
+
this._byFingerprint = new Map(); // fingerprint -> pending record
|
|
249
|
+
this._activeByLogicalKey = new Map(); // logical key -> active fingerprint
|
|
250
|
+
this._clock = typeof clock === "function" ? clock : null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
_now() {
|
|
254
|
+
return this._clock ? intOrNull(this._clock()) : null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Issue a card for an operation. Any still-unresolved card for the SAME
|
|
259
|
+
* logical operation is superseded (single-winner) so only this newest card
|
|
260
|
+
* can resolve.
|
|
261
|
+
* @returns {{fingerprint:string, shortId:string, summary:string}}
|
|
262
|
+
*/
|
|
263
|
+
issue(desc = {}) {
|
|
264
|
+
const fingerprint = computeOperationFingerprint(desc);
|
|
265
|
+
const logicalKey = operationDescriptorKey(desc);
|
|
266
|
+
|
|
267
|
+
const priorFp = this._activeByLogicalKey.get(logicalKey);
|
|
268
|
+
if (priorFp && priorFp !== fingerprint) {
|
|
269
|
+
const prior = this._byFingerprint.get(priorFp);
|
|
270
|
+
if (prior && !prior.resolved) prior.supersededBy = fingerprint;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
this._byFingerprint.set(fingerprint, {
|
|
274
|
+
fingerprint,
|
|
275
|
+
logicalKey,
|
|
276
|
+
notBefore: intOrNull(desc.notBefore),
|
|
277
|
+
notAfter: intOrNull(desc.notAfter),
|
|
278
|
+
resolved: false,
|
|
279
|
+
supersededBy: null,
|
|
280
|
+
});
|
|
281
|
+
this._activeByLogicalKey.set(logicalKey, fingerprint);
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
fingerprint,
|
|
285
|
+
shortId: shortOperationId(fingerprint),
|
|
286
|
+
summary: summarizeOperation(desc),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Resolve a card by fingerprint. Fails closed (with a named reason) on an
|
|
292
|
+
* unknown, superseded, duplicate, mismatched, not-yet-valid or expired card.
|
|
293
|
+
* On success the card is marked resolved so a replay is a `duplicate`.
|
|
294
|
+
* @param {string} fingerprint
|
|
295
|
+
* @param {object} [opts] {now?} overrides the injected clock
|
|
296
|
+
* @returns {{ok:boolean, reason:(string|null)}}
|
|
297
|
+
*/
|
|
298
|
+
resolve(fingerprint, opts = {}) {
|
|
299
|
+
const pending = this._byFingerprint.get(fingerprint);
|
|
300
|
+
if (!pending) return { ok: false, reason: "unknown" };
|
|
301
|
+
const now = opts.now !== undefined ? intOrNull(opts.now) : this._now();
|
|
302
|
+
const verdict = resolveOperationApproval(pending, { fingerprint }, { now });
|
|
303
|
+
if (verdict.ok) pending.resolved = true;
|
|
304
|
+
return verdict;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** A read-only view of a pending card (or null). */
|
|
308
|
+
get(fingerprint) {
|
|
309
|
+
const p = this._byFingerprint.get(fingerprint);
|
|
310
|
+
return p ? { ...p } : null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -19,6 +19,33 @@
|
|
|
19
19
|
import path from "path";
|
|
20
20
|
import { discoverPlugins } from "./scopes.js";
|
|
21
21
|
import { partitionByTrust, warnUntrustedOnce } from "./trust.js";
|
|
22
|
+
import { componentCapabilityDenial } from "./capabilities.js";
|
|
23
|
+
|
|
24
|
+
// One-time stderr notice when a plugin's bin dir is refused at the COMPONENT
|
|
25
|
+
// level because the plugin opted into the capability model but did not declare
|
|
26
|
+
// the `process` capability its executables need. Distinct from the trust gate:
|
|
27
|
+
// these plugins ARE trusted, but their bin component is denied.
|
|
28
|
+
const _capabilityDenied = new Set();
|
|
29
|
+
function warnBinCapabilityDeniedOnce(entries) {
|
|
30
|
+
if (!entries || entries.length === 0) return;
|
|
31
|
+
if (_capabilityDenied.has("bin-capability")) return;
|
|
32
|
+
_capabilityDenied.add("bin-capability");
|
|
33
|
+
const list = entries.map((e) => `${e.name} (${e.reason})`).join("; ");
|
|
34
|
+
try {
|
|
35
|
+
process.stderr.write(
|
|
36
|
+
`[plugins] refused bin dir(s) from plugin(s) that declared a permissions ` +
|
|
37
|
+
`block but did not declare the 'process' capability: ${list}\n` +
|
|
38
|
+
` add 'process' to the plugin's permissions block to enable them.\n`,
|
|
39
|
+
);
|
|
40
|
+
} catch {
|
|
41
|
+
/* stderr notice is best-effort */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Test hook: reset the one-time capability-denied warning guard. */
|
|
46
|
+
export function _resetBinWarnings() {
|
|
47
|
+
_capabilityDenied.clear();
|
|
48
|
+
}
|
|
22
49
|
|
|
23
50
|
/**
|
|
24
51
|
* Collect trusted, installed plugins' `bin/` directories.
|
|
@@ -40,10 +67,19 @@ export function collectPluginBinDirs(opts = {}) {
|
|
|
40
67
|
"bin",
|
|
41
68
|
);
|
|
42
69
|
const out = [];
|
|
70
|
+
const denied = [];
|
|
43
71
|
for (const p of trusted) {
|
|
44
72
|
if (!p.manifest || p.manifest.ok !== true) continue;
|
|
45
73
|
const bins = p.manifest.components?.bin;
|
|
46
74
|
if (!Array.isArray(bins) || bins.length === 0) continue;
|
|
75
|
+
// Component-level capability gate: a plugin that declared a permissions
|
|
76
|
+
// block but did not declare the `process` capability its executables need
|
|
77
|
+
// gets its bin dir refused here (mirrors the MCP collector's denial).
|
|
78
|
+
const denial = componentCapabilityDenial(p.manifest, ["process"]);
|
|
79
|
+
if (denial) {
|
|
80
|
+
denied.push({ name: p.name, reason: denial.reason });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
47
83
|
// Every bin entry's absPath is a file inside the plugin's bin dir; collect
|
|
48
84
|
// the unique parent directories (usually just `<root>/bin`).
|
|
49
85
|
const dirs = new Set(bins.map((b) => path.dirname(b.absPath)));
|
|
@@ -56,6 +92,7 @@ export function collectPluginBinDirs(opts = {}) {
|
|
|
56
92
|
});
|
|
57
93
|
}
|
|
58
94
|
}
|
|
95
|
+
warnBinCapabilityDeniedOnce(denied);
|
|
59
96
|
return out;
|
|
60
97
|
}
|
|
61
98
|
|
|
@@ -220,6 +220,25 @@ export function auditDeclaredCapabilities(manifest) {
|
|
|
220
220
|
return findings;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Component-level capability denial. A plugin that opted into the capability
|
|
225
|
+
* model (declared a `permissions` block, so `capabilitiesDeclared === true`) but
|
|
226
|
+
* ships a component whose required capability it did NOT declare should have that
|
|
227
|
+
* single component refused — not just flagged by the whole-plugin consent notice.
|
|
228
|
+
* `capabilityNames` is the set of capability names whose absence denies this
|
|
229
|
+
* component (e.g. `["process"]` for a hook/bin/lsp, `["monitor"]` for a monitor,
|
|
230
|
+
* `["mcp","network"]` for an MCP server). Legacy plugins (no permissions block)
|
|
231
|
+
* are unrestricted by design and never denied. Returns `{reason}` or null.
|
|
232
|
+
*/
|
|
233
|
+
export function componentCapabilityDenial(manifest, capabilityNames) {
|
|
234
|
+
if (!manifest || manifest.capabilitiesDeclared !== true) return null;
|
|
235
|
+
const wanted = new Set(capabilityNames || []);
|
|
236
|
+
const finding = auditDeclaredCapabilities(manifest).find((f) =>
|
|
237
|
+
wanted.has(f.capability),
|
|
238
|
+
);
|
|
239
|
+
return finding ? { reason: finding.reason } : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
223
242
|
// ─── options schema ─────────────────────────────────────────────────────────
|
|
224
243
|
|
|
225
244
|
export const OPTION_TYPES = Object.freeze([
|
|
@@ -127,6 +127,34 @@ export function capabilityConsentStatus(declaredCaps, entry) {
|
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Decide how an install/upgrade command should handle capability consent at the
|
|
132
|
+
* point of installation (pure — no I/O). `notice` is the shape returned by the
|
|
133
|
+
* command layer's capability resolver: `{ consented, added, ... }` or null when
|
|
134
|
+
* the plugin declares no capabilities.
|
|
135
|
+
*
|
|
136
|
+
* Returns one of:
|
|
137
|
+
* - "advisory" — nothing to consent (no caps, or already consented), OR the
|
|
138
|
+
* caller is non-interactive and did not pass an explicit grant flag: print
|
|
139
|
+
* the notice and require a later `cc plugin consent --grant`.
|
|
140
|
+
* - "grant" — an explicit grant flag was given: record consent immediately
|
|
141
|
+
* (scriptable / CI path).
|
|
142
|
+
* - "prompt" — consent is required and the session is interactive: ask the
|
|
143
|
+
* user, then grant on confirmation.
|
|
144
|
+
*
|
|
145
|
+
* Precedence: an explicit grant flag always wins over an interactive prompt, so
|
|
146
|
+
* `--grant-capabilities` is honored even under a TTY without a second question.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveConsentAction(
|
|
149
|
+
notice,
|
|
150
|
+
{ grant = false, interactive = false } = {},
|
|
151
|
+
) {
|
|
152
|
+
if (!notice || notice.consented) return "advisory";
|
|
153
|
+
if (grant) return "grant";
|
|
154
|
+
if (interactive) return "prompt";
|
|
155
|
+
return "advisory";
|
|
156
|
+
}
|
|
157
|
+
|
|
130
158
|
/** Load the store and report whether one plugin's declared caps are consented. */
|
|
131
159
|
export function isPluginCapabilityConsented(plugin, declaredCaps) {
|
|
132
160
|
if (!plugin || !plugin.name) return false;
|