chainlesschain 0.162.162 → 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-ClI2ALgv.js → AIOps-BqRkaHGb.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BDjRcJaK.js → ActionButton-DWO7OZLC.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DikSRsaT.js → Analytics-C5oaVezM.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-9mLhMFmz.js → AppLayout-CsUESDoZ.js} +3 -3
- package/src/assets/web-panel/assets/{Artifacts-BSr88BrH.js → Artifacts-Dragqu5k.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CdvyimOF.js → Audit-CQsaHctY.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-D3_hCedh.js → BackgroundAgents-DdG1jNSM.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-Cyvv9RHf.js → Backup-DN6sjK4z.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BZM14kt8.js → BaseInput-Cxm_ZTCl.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DrQSokVz.js → Chat-DVXGmA56.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-HwJR-C6B.js → ChatBubbleRenderer-BdaCL_iE.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-bFLYeWMV.js → Checkbox-DEc4vD_A.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-7mN7xJXT.js → Codegen-CKiTcv36.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DAB5ZWm7.js → Col-Br9UUNN0.js} +1 -1
- package/src/assets/web-panel/assets/{Community-B_1TymaK.js → Community-NeUH_70T.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Bq1uSWLJ.js → Compact-BqR-z2Df.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CyiE1jEe.js → Compliance-DcWB_SgZ.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Cpk0D7vm.js → Cowork-BfhgcudF.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-d_Lv3rdB.js → Cron-LaU29LgD.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DMqhHjMR.js → Crosschain-B2ZB9uwW.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Br-oPuBw.js → DID-BuHl38A2.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CfLW_RaP.js → Dashboard-m627yqvZ.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DZnTOExV.js → Dropdown-C7bxF8F5.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DHIUHDv1.js → EmailListRenderer-DPLPQzLp.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Bw3t3myu.js → FamilyGuardDashboard-cOiH6ike.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-D9cpQPAY.js → Federation-Bvkzg88z.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Jdczr9Mr.js → FormItemContext-BTsiA7h_.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BT8ZMpC5.js → GenericCardRenderer-BGnVE5qu.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DjARip2b.js → Git-DS_j6r25.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BgWbSRcj.js → Governance-CKRG3Rsl.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CfouJciM.js → Inference-DW8INoNQ.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-50l7mrAr.js → KnowledgeGraph-BhEYeFhC.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-rq50H84B.js → Logs-wqi0uxuR.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-DUPIEdQH.js → MarkdownRenderer-DS8dE8ty.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-BYgPDmHU.js → Marketplace-CMlTuPLT.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-5DVgcFdx.js → McpTools-CSx4m5JJ.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-B70LPk7f.js → Memory-vnQCUy-_.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-De8ssA5Y.js → MobileBridge-BV2NA742.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-Bw0k8e1B.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-RJsOTsIA.js → Mtc-CzK_FPB1.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-Crh-6BEK.js → MtcAudit-Cb7G9oL3.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-wuoR5l3x.js → Multisig-qJxlUyzr.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-U2Mji4fZ.js → NLProgramming-DqaBJ7Te.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CdTZAyDH.js → Notes-CwhTJkRi.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-C-FucWE7.js → NotificationSettings-YDs4Fal4.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DCYFTeaI.js → OrderTableRenderer-CvEALpq7.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-an3OF85L.js → Organization-CEmQm9n8.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-C-1vmYFe.js → Overflow-zby_P_LA.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Cwh47xKg.js → P2P-DsmDNFNT.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Dm38GT0k.js → PdhVaultBrowser-FWwFlZGF.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DS2Y8awD.js → Permissions-DuzQ6me5.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BQNziA0I.js → PersonalDataHub-DZbeN613.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-DF85L1ue.js → Pipeline-CoNSVAUS.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BuLornF-.js → Privacy-OYJEAaMC.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-C3Y31IwX.js → ProjectInit-CWIapV--.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BCPiI8aD.js → ProjectSettings-CX8xMxZn.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BZ6s4k6g.js → Projects-BLCN8YyY.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-D3Wp022X.js → Providers-BV_sxp75.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CUJfQkH4.js → QrScannerModal-BTnRrHRp.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-UAc0EO8l.js → QuickAsk-D1KZg018.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BaxKupcs.js → Recommend-COm5cuNg.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-C75NNnXZ.js → RemoteSession-0saWEkYI.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-BVXynZAk.js → Reputation-Dr7iKLS2.js} +1 -1
- package/src/assets/web-panel/assets/{Row-C5MRZ0bi.js → Row-C1vpPBcr.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-DAKkEYka.js → RssFeed-CxlWrtNj.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DvWv9YOA.js → Search-BT2V8dPN.js} +1 -1
- package/src/assets/web-panel/assets/{Security-B5peafW1.js → Security-bm9V3c5k.js} +4 -4
- package/src/assets/web-panel/assets/{Services-UQmvPEaG.js → Services-CocH1yyX.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-C3bhWpuF.js → Skeleton-DHItlMdd.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DfkKf9_H.js → Skills-C59sLc4w.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DqMSjlBM.js → Sla-Crj4BMc8.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-C-WbxWUX.js → SpeechSettings-DX4KFceY.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CYuHZ4N2.js → SyncSettings-BD_iwQLr.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-CyeAyB9d.js → Tasks-BmCTzYsH.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BTCeQnpq.js → Templates-BGUaxZYl.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BifQuA9Y.js → Tenant-BS0tJx2O.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-Bx_9Bocf.js → Terminal--tHSAsld.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BX0RFCOm.js → TimelineRenderer-D9-MFxS4.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CGHrCmUS.js → Tokens-CiCB485f.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DlqaY4SO.js → Trigger-SmCFuW_l.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DfOmj-ay.js → Trust-DkK3Rqe0.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-B72jH5fE.js → UkeySign-CDM4LGdB.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BT-63fqv.js → VideoEditing-IDB7DJXt.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Dsul3sHr.js → Wallet-Bikj5cmt.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Izkwwdgy.js → WebAuthn-C9abWlAw.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-D3FHSiH1.js → WorkflowEditor-DrbRKVky.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CmVz5z3U.js → chat-CccCSDQf.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CuLgyJBp.js → colors-CXObXLhb.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Ml1YkXpw.js → compact-item-B5kfYAxC.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BvjJ48bs.js → createContext-DmiPBfip.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CvKrADNa.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DVC0157C.js → hasIn-Q6lYteAc.js} +1 -1
- package/src/assets/web-panel/assets/{index-C229ABfy.js → index-25Wlqps4.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ck_52dz_.js → index-33U29UNV.js} +1 -1
- package/src/assets/web-panel/assets/{index-QXzlT9KD.js → index-4oXBDczR.js} +1 -1
- package/src/assets/web-panel/assets/{index-4SBta3d7.js → index-BGYXvMc5.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0S8NzO6.js → index-BJCCa0s_.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJyN28Et.js → index-BJJ5DhKI.js} +1 -1
- package/src/assets/web-panel/assets/{index-DC_OI9H2.js → index-BLOzViJ6.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_XuRgGn.js → index-BRrbUMoG.js} +1 -1
- package/src/assets/web-panel/assets/{index-wvb_FtaV.js → index-BUFpT17Q.js} +1 -1
- package/src/assets/web-panel/assets/{index-KMqhcF7l.js → index-BkRDiqGc.js} +1 -1
- package/src/assets/web-panel/assets/{index-vG4XSJ1d.js → index-BsfvW6my.js} +1 -1
- package/src/assets/web-panel/assets/{index-DYXHFr4r.js → index-By-kfPED.js} +1 -1
- package/src/assets/web-panel/assets/{index-DuGszaWY.js → index-BzFTcQsc.js} +1 -1
- package/src/assets/web-panel/assets/{index-CXkJH-lK.js → index-C2FXdboq.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dt5KMjwJ.js → index-C5XOlTHN.js} +1 -1
- package/src/assets/web-panel/assets/{index-4wcgvPoF.js → index-CLhBDS2w.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_H_S-DY.js → index-CMq7UeoT.js} +1 -1
- package/src/assets/web-panel/assets/{index-BydjdZnl.js → index-CVMTWNKl.js} +1 -1
- package/src/assets/web-panel/assets/{index-BjHBpRYZ.js → index-Caghi-yL.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bzr0rB5T.js → index-CdB6FuSH.js} +1 -1
- package/src/assets/web-panel/assets/{index-BbPtzB8M.js → index-CoK8-kWQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BUt8kTIB.js → index-CsO3GUAY.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_vRgTl9.js → index-CwZBa20Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-DukDVXCv.js → index-D-kASucw.js} +1 -1
- package/src/assets/web-panel/assets/{index-BbepULSc.js → index-D8eBw7vW.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfuKP4qe.js → index-D9wgifk4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BiC6dQZG.js → index-DAYCTc07.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmFNvNLZ.js → index-DF6Q8l2h.js} +1 -1
- package/src/assets/web-panel/assets/index-DTdFUWc_.js +1 -0
- package/src/assets/web-panel/assets/{index-KjgKdZe-.js → index-DgTLApS9.js} +1 -1
- package/src/assets/web-panel/assets/{index-BpFJHiTv.js → index-DgxVJ8pg.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cw0Rjy87.js → index-DlJvIqoR.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFYVQMvu.js → index-Dw4eHfsG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CudmwNBV.js → index-Rh-ytwow.js} +3 -3
- package/src/assets/web-panel/assets/{index-DDGaOWBV.js → index-StAEt2pq.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3lD5qap.js → index-bzfw951k.js} +1 -1
- package/src/assets/web-panel/assets/{index-BzG-x9p4.js → index-fSUMbMvv.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_EvG_GW.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-BDgd2YZ8.js → initDefaultProps-BNq7X72N.js} +1 -1
- package/src/assets/web-panel/assets/{motion-ErtrXdYe.js → motion-DOf9viTc.js} +1 -1
- package/src/assets/web-panel/assets/{move-_GPPNCgs.js → move-CRHTcW4x.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DRLL-8V2.js → mtc-parser-Cyoozgm2.js} +1 -1
- package/src/assets/web-panel/assets/{omit-p86sl023.js → omit-6-TjFNru.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BTu7cMLH.js → pickAttrs-DlUmO1zi.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BEKz6Isr.js → placementArrow-CFOL8mIR.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Ck42c9MP.js → responsiveObserve-BtaFK5zd.js} +1 -1
- package/src/assets/web-panel/assets/{slide-1CEvuPsB.js → slide-BIbc2bur.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BDfdx1GJ.js → statusUtils-BXX9d6-w.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-B4K46m5w.js → styleChecker-BGv10Bpn.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DoIVpJNq.js → useFlexGapSupport-bKnKT8tw.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DdAWi-ln.js → useFs-FY4IL4jn.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-iUu6k8dx.js → usePersonalDataHub-Bwl9TEgu.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DZz4R6Tw.js → vnode-BM3OHxAT.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DXlf9sA3.js → zoom-D1fbFiHB.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +320 -32
- package/src/commands/agent.js +105 -11
- package/src/commands/context.js +125 -14
- package/src/commands/doctor.js +85 -1
- package/src/commands/hook.js +181 -7
- package/src/commands/plugin.js +456 -1
- package/src/commands/review.js +363 -2
- package/src/commands/session.js +191 -0
- package/src/commands/team.js +19 -1
- package/src/data/changelog.json +10 -4
- package/src/gateways/ws/remote-session-protocol.js +39 -0
- package/src/gateways/ws/session-protocol.js +55 -2
- 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 +318 -10
- 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/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/diagnostic-bundle.js +230 -0
- package/src/lib/doctor-checkup.js +275 -4
- package/src/lib/execution-location.js +286 -0
- package/src/lib/headless-manifest.js +13 -40
- package/src/lib/hook-event-log.cjs +193 -0
- package/src/lib/hook-stats-store.cjs +197 -0
- package/src/lib/idempotency.js +109 -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/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 +203 -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/plugin-runtime/policy.js +62 -0
- package/src/lib/plugin-runtime/scopes.js +20 -4
- package/src/lib/pr-automation-policy.js +224 -0
- package/src/lib/project-instructions.js +128 -0
- package/src/lib/remote-approval-bridge.js +66 -13
- package/src/lib/repl-goal.js +132 -0
- package/src/lib/repl-rewind.js +153 -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/schedule-planner.js +17 -1
- package/src/lib/secret-scan.js +409 -0
- package/src/lib/session-branch.js +0 -0
- 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 +401 -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 +110 -9
- package/src/lib/sub-agent-registry.js +19 -0
- package/src/lib/subagent-inheritance.js +112 -0
- package/src/lib/telemetry/span-recorder.js +17 -4
- package/src/lib/terminal-context.js +195 -0
- package/src/lib/turn-binding-store.js +119 -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 +280 -30
- package/src/repl/permission-tier.js +28 -0
- package/src/runtime/agent-core.js +565 -21
- package/src/runtime/coding-agent-contract-shared.cjs +60 -1
- package/src/runtime/diagnostics.js +60 -0
- package/src/runtime/headless-runner.js +393 -1
- package/src/runtime/headless-stream.js +60 -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,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution-location model — the "任务在哪执行是 Session 一等属性" core of P1-7
|
|
3
|
+
* (Local / SSH / Cloud / Remote Control 统一模型) of
|
|
4
|
+
* CLAUDE_CODE_IDE_INCREMENTAL_GAP_ANALYSIS_2026-07-13.md.
|
|
5
|
+
*
|
|
6
|
+
* [[execution-backend.js]] is the runtime EXECUTOR (Local / Docker / SSH backends
|
|
7
|
+
* that actually run a command). It is NOT a session-level, displayable, policy
|
|
8
|
+
* layer: it has no notion of "show the user where this runs, with what
|
|
9
|
+
* permissions, whose credentials (source+scope only) and how results return",
|
|
10
|
+
* and no fail-closed default for an UNKNOWN location.
|
|
11
|
+
*
|
|
12
|
+
* This module is that pure descriptor + policy. The P1-7 table requires every
|
|
13
|
+
* session to surface: Execution Location, Source, Permissions, Credentials
|
|
14
|
+
* (source & scope only — never a value), Lifecycle, Cost and Return Path. This
|
|
15
|
+
* module normalizes those, and enforces the two invariants that give it teeth:
|
|
16
|
+
*
|
|
17
|
+
* 1. Credentials NEVER carry values — only {name, source, scope} survive, so a
|
|
18
|
+
* descriptor can be rendered/logged/shipped without leaking a secret.
|
|
19
|
+
* 2. An UNKNOWN or unrecognized location fails CLOSED to the most-restrictive
|
|
20
|
+
* permission profile (read-only, no shell/network/mcp/external), because we
|
|
21
|
+
* must not grant a remote/unknown environment ambient power by default.
|
|
22
|
+
*
|
|
23
|
+
* PURE: no fs / clock / RNG / process. A caller feeds observed session facts and
|
|
24
|
+
* reads back a safe descriptor plus a list of policy violations.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Canonical execution locations. UNKNOWN is the fail-closed sink. */
|
|
28
|
+
export const EXECUTION_LOCATION = Object.freeze({
|
|
29
|
+
LOCAL: "local",
|
|
30
|
+
WSL: "wsl",
|
|
31
|
+
SSH: "ssh",
|
|
32
|
+
CONTAINER: "container",
|
|
33
|
+
CLOUD: "cloud",
|
|
34
|
+
UNKNOWN: "unknown",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const LOCATION_ALIASES = new Map([
|
|
38
|
+
["local", EXECUTION_LOCATION.LOCAL],
|
|
39
|
+
["localhost", EXECUTION_LOCATION.LOCAL],
|
|
40
|
+
["wsl", EXECUTION_LOCATION.WSL],
|
|
41
|
+
["wsl2", EXECUTION_LOCATION.WSL],
|
|
42
|
+
["ssh", EXECUTION_LOCATION.SSH],
|
|
43
|
+
["remote-ssh", EXECUTION_LOCATION.SSH],
|
|
44
|
+
["remote_ssh", EXECUTION_LOCATION.SSH],
|
|
45
|
+
["container", EXECUTION_LOCATION.CONTAINER],
|
|
46
|
+
["docker", EXECUTION_LOCATION.CONTAINER],
|
|
47
|
+
["devcontainer", EXECUTION_LOCATION.CONTAINER],
|
|
48
|
+
["dev-container", EXECUTION_LOCATION.CONTAINER],
|
|
49
|
+
["cloud", EXECUTION_LOCATION.CLOUD],
|
|
50
|
+
["codespaces", EXECUTION_LOCATION.CLOUD],
|
|
51
|
+
["codespace", EXECUTION_LOCATION.CLOUD],
|
|
52
|
+
["sandbox", EXECUTION_LOCATION.CLOUD],
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
/** Locations that are NOT the user's own trusted machine. */
|
|
56
|
+
const REMOTE_LOCATIONS = new Set([
|
|
57
|
+
EXECUTION_LOCATION.SSH,
|
|
58
|
+
EXECUTION_LOCATION.CONTAINER,
|
|
59
|
+
EXECUTION_LOCATION.CLOUD,
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/** Normalize any location label to a canonical value; unrecognized → UNKNOWN. */
|
|
63
|
+
export function normalizeExecutionLocation(value) {
|
|
64
|
+
if (typeof value !== "string") return EXECUTION_LOCATION.UNKNOWN;
|
|
65
|
+
return (
|
|
66
|
+
LOCATION_ALIASES.get(value.trim().toLowerCase()) ||
|
|
67
|
+
EXECUTION_LOCATION.UNKNOWN
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function isRemoteLocation(location) {
|
|
72
|
+
return REMOTE_LOCATIONS.has(normalizeExecutionLocation(location));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Detect the ambient execution location from process signals. PURE: the caller
|
|
77
|
+
* supplies `env` (process.env) and `dockerEnvFileExists` (existsSync('/.dockerenv'))
|
|
78
|
+
* so this stays fs/process-free and unit-testable. Ordered most-specific-first,
|
|
79
|
+
* so a Codespace (which is also a container) reports CLOUD, and an SSH login
|
|
80
|
+
* outranks a bare WSL marker.
|
|
81
|
+
*
|
|
82
|
+
* @param {{env?:object, dockerEnvFileExists?:boolean}} [signals]
|
|
83
|
+
* @returns {string} a canonical EXECUTION_LOCATION value
|
|
84
|
+
*/
|
|
85
|
+
export function detectAmbientLocation({ env = {}, dockerEnvFileExists } = {}) {
|
|
86
|
+
const e = env && typeof env === "object" ? env : {};
|
|
87
|
+
const has = (k) => typeof e[k] === "string" && e[k].trim() !== "";
|
|
88
|
+
|
|
89
|
+
// Cloud dev environments (GitHub Codespaces) — most specific.
|
|
90
|
+
if (e.CODESPACES === "true" || has("GITHUB_CODESPACE_TOKEN")) {
|
|
91
|
+
return EXECUTION_LOCATION.CLOUD;
|
|
92
|
+
}
|
|
93
|
+
// An interactive SSH login carries these — a remote shell, not the box's own tty.
|
|
94
|
+
if (has("SSH_CONNECTION") || has("SSH_CLIENT") || has("SSH_TTY")) {
|
|
95
|
+
return EXECUTION_LOCATION.SSH;
|
|
96
|
+
}
|
|
97
|
+
// Containers: the classic /.dockerenv marker, an explicit `container` env, or k8s.
|
|
98
|
+
if (
|
|
99
|
+
dockerEnvFileExists === true ||
|
|
100
|
+
has("container") ||
|
|
101
|
+
has("KUBERNETES_SERVICE_HOST")
|
|
102
|
+
) {
|
|
103
|
+
return EXECUTION_LOCATION.CONTAINER;
|
|
104
|
+
}
|
|
105
|
+
// WSL exports these into every shell.
|
|
106
|
+
if (has("WSL_DISTRO_NAME") || has("WSL_INTEROP")) {
|
|
107
|
+
return EXECUTION_LOCATION.WSL;
|
|
108
|
+
}
|
|
109
|
+
return EXECUTION_LOCATION.LOCAL;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The most-restrictive permission profile — the fail-closed floor. */
|
|
113
|
+
function lockedDownPermissions() {
|
|
114
|
+
return Object.freeze({
|
|
115
|
+
file: "read", // read-only
|
|
116
|
+
shell: false,
|
|
117
|
+
network: false,
|
|
118
|
+
mcp: false,
|
|
119
|
+
externalSystems: false,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeFileAccess(v) {
|
|
124
|
+
const s = String(v || "").toLowerCase();
|
|
125
|
+
if (s === "write" || s === "readwrite" || s === "read-write") return "write";
|
|
126
|
+
if (s === "none") return "none";
|
|
127
|
+
return "read";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Clamp a requested permission set to what a location may grant. Every ambient
|
|
132
|
+
* power (shell / network / mcp / external) requires an EXPLICIT `true` — an
|
|
133
|
+
* unspecified or truthy-but-not-true power stays off. File access defaults to
|
|
134
|
+
* read; only a write-class request ("write" / "readwrite") grants write. An
|
|
135
|
+
* UNKNOWN location ignores the request entirely and returns the locked-down
|
|
136
|
+
* floor — the fail-closed core.
|
|
137
|
+
*/
|
|
138
|
+
export function clampPermissionsForLocation(location, requested = {}) {
|
|
139
|
+
const loc = normalizeExecutionLocation(location);
|
|
140
|
+
if (loc === EXECUTION_LOCATION.UNKNOWN) return lockedDownPermissions();
|
|
141
|
+
|
|
142
|
+
const req = requested && typeof requested === "object" ? requested : {};
|
|
143
|
+
const grant = (key) => req[key] === true;
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
file: normalizeFileAccess(req.file),
|
|
147
|
+
shell: grant("shell"),
|
|
148
|
+
network: grant("network"),
|
|
149
|
+
mcp: grant("mcp"),
|
|
150
|
+
externalSystems: grant("externalSystems"),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Strip credential references down to what may be displayed: name, source and
|
|
156
|
+
* scope only. Any `value` / `token` / `secret` field is dropped — this is the
|
|
157
|
+
* enforcement point behind "Credentials 只显示来源和作用域,不显示值".
|
|
158
|
+
*/
|
|
159
|
+
export function redactCredentialRefs(creds) {
|
|
160
|
+
const list = Array.isArray(creds) ? creds : [];
|
|
161
|
+
return list
|
|
162
|
+
.filter((c) => c && typeof c === "object")
|
|
163
|
+
.map((c) => ({
|
|
164
|
+
name: c.name == null ? null : String(c.name),
|
|
165
|
+
source: c.source == null ? null : String(c.source),
|
|
166
|
+
scope: c.scope == null ? null : String(c.scope),
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const RETURN_PATHS = new Set([
|
|
171
|
+
"commit",
|
|
172
|
+
"pr",
|
|
173
|
+
"patch",
|
|
174
|
+
"artifact",
|
|
175
|
+
"report",
|
|
176
|
+
"none",
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
function normalizeReturnPath(v) {
|
|
180
|
+
const s = String(v || "").toLowerCase();
|
|
181
|
+
return RETURN_PATHS.has(s) ? s : "none";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Build the safe, displayable execution-context descriptor for a session.
|
|
186
|
+
*
|
|
187
|
+
* @param {object} input
|
|
188
|
+
* @param {string} input.location raw location label
|
|
189
|
+
* @param {object} [input.source] {dir, repo, commit, repos?}
|
|
190
|
+
* @param {object} [input.permissions] requested permissions (clamped)
|
|
191
|
+
* @param {Array} [input.credentials] credential refs (values stripped)
|
|
192
|
+
* @param {object} [input.lifecycle] {foreground?, onDisconnect?, onSleep?}
|
|
193
|
+
* @param {object} [input.cost] {model?, tokenBudget?, remote?}
|
|
194
|
+
* @param {string} [input.returnPath] commit|pr|patch|artifact|report|none
|
|
195
|
+
* @returns {object} normalized descriptor (safe to render/log/ship)
|
|
196
|
+
*/
|
|
197
|
+
export function describeExecutionContext(input = {}) {
|
|
198
|
+
const location = normalizeExecutionLocation(input.location);
|
|
199
|
+
const permissions = clampPermissionsForLocation(location, input.permissions);
|
|
200
|
+
const src =
|
|
201
|
+
input.source && typeof input.source === "object" ? input.source : {};
|
|
202
|
+
return {
|
|
203
|
+
location,
|
|
204
|
+
remote: REMOTE_LOCATIONS.has(location),
|
|
205
|
+
source: {
|
|
206
|
+
dir: src.dir == null ? null : String(src.dir),
|
|
207
|
+
repo: src.repo == null ? null : String(src.repo),
|
|
208
|
+
commit: src.commit == null ? null : String(src.commit),
|
|
209
|
+
repos: Array.isArray(src.repos) ? src.repos.map((r) => String(r)) : [],
|
|
210
|
+
},
|
|
211
|
+
permissions,
|
|
212
|
+
credentials: redactCredentialRefs(input.credentials),
|
|
213
|
+
lifecycle: {
|
|
214
|
+
foreground: input.lifecycle?.foreground !== false,
|
|
215
|
+
onDisconnect: input.lifecycle?.onDisconnect
|
|
216
|
+
? String(input.lifecycle.onDisconnect)
|
|
217
|
+
: "pause",
|
|
218
|
+
onSleep: input.lifecycle?.onSleep
|
|
219
|
+
? String(input.lifecycle.onSleep)
|
|
220
|
+
: "pause",
|
|
221
|
+
},
|
|
222
|
+
cost: {
|
|
223
|
+
model: input.cost?.model ? String(input.cost.model) : null,
|
|
224
|
+
tokenBudget: Number.isFinite(Number(input.cost?.tokenBudget))
|
|
225
|
+
? Number(input.cost.tokenBudget)
|
|
226
|
+
: null,
|
|
227
|
+
remote: input.cost?.remote === true,
|
|
228
|
+
},
|
|
229
|
+
returnPath: normalizeReturnPath(input.returnPath),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Keys that must never appear on a credential ref (a value leak). */
|
|
234
|
+
const CREDENTIAL_VALUE_KEYS = ["value", "token", "secret", "password", "key"];
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Validate a raw execution-context input for policy compliance. EXHAUSTIVE:
|
|
238
|
+
* collects every violation. Fail-closed reads:
|
|
239
|
+
* - a credential ref carrying a value/token/secret/password/key → leak
|
|
240
|
+
* - location UNKNOWN → the session runs somewhere we cannot vouch for
|
|
241
|
+
* - a remote location with no return path → results have nowhere safe to go
|
|
242
|
+
* - a remote session granted network / external egress → surfaced as
|
|
243
|
+
* `remote-egress-granted` so it must be explicitly acknowledged, not ambient
|
|
244
|
+
*
|
|
245
|
+
* @returns {{ok:boolean, violations:string[], descriptor:object}}
|
|
246
|
+
*/
|
|
247
|
+
export function validateExecutionContext(input = {}) {
|
|
248
|
+
const violations = [];
|
|
249
|
+
const rawCreds = Array.isArray(input.credentials) ? input.credentials : [];
|
|
250
|
+
for (const c of rawCreds) {
|
|
251
|
+
if (c && typeof c === "object") {
|
|
252
|
+
for (const k of CREDENTIAL_VALUE_KEYS) {
|
|
253
|
+
if (c[k] != null) {
|
|
254
|
+
violations.push("credential-value-present");
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const location = normalizeExecutionLocation(input.location);
|
|
262
|
+
if (location === EXECUTION_LOCATION.UNKNOWN) {
|
|
263
|
+
violations.push("unknown-location");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const descriptor = describeExecutionContext(input);
|
|
267
|
+
|
|
268
|
+
if (descriptor.remote && descriptor.returnPath === "none") {
|
|
269
|
+
violations.push("remote-without-return-path");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Network / external egress FROM a remote box is the highest-risk grant — it
|
|
273
|
+
// must be an explicit, acknowledged decision, never ambient.
|
|
274
|
+
if (
|
|
275
|
+
descriptor.remote &&
|
|
276
|
+
(descriptor.permissions.network || descriptor.permissions.externalSystems)
|
|
277
|
+
) {
|
|
278
|
+
violations.push("remote-egress-granted");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
ok: violations.length === 0,
|
|
283
|
+
violations,
|
|
284
|
+
descriptor,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
import crypto from "node:crypto";
|
|
18
18
|
import { createRequire } from "node:module";
|
|
19
19
|
import { PROTOCOL_MIN_VERSION } from "./capability-negotiation.js";
|
|
20
|
+
import {
|
|
21
|
+
CAPABILITY_MANIFEST,
|
|
22
|
+
toAgentFeatureFlags,
|
|
23
|
+
} from "./capability-manifest.js";
|
|
20
24
|
|
|
21
25
|
export const STREAM_PROTOCOL_VERSION = 1;
|
|
22
26
|
|
|
@@ -143,46 +147,15 @@ export function buildAgentCapabilities() {
|
|
|
143
147
|
// older line shape, so min === current.
|
|
144
148
|
min_protocol_version: PROTOCOL_MIN_VERSION,
|
|
145
149
|
agent_tools: contract.listCodingAgentToolNames(),
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
],
|
|
155
|
-
output_formats: ["text", "json", "stream-json"],
|
|
156
|
-
input_formats: ["text", "stream-json"],
|
|
150
|
+
// P1-9: permission modes, formats and the feature flags all project from the
|
|
151
|
+
// single canonical [[capability-manifest.js]] so `cc agent --capabilities`,
|
|
152
|
+
// the negotiator and the docs can no longer drift. Additive protocol-v1
|
|
153
|
+
// stream fields (event_seq / tool_use_id / trace_id) come in via the wire
|
|
154
|
+
// features; the mcp object and sandbox_engines come from the manifest too.
|
|
155
|
+
permission_modes: [...CAPABILITY_MANIFEST.permissionModes],
|
|
156
|
+
output_formats: [...CAPABILITY_MANIFEST.outputFormats],
|
|
157
|
+
input_formats: [...CAPABILITY_MANIFEST.inputFormats],
|
|
157
158
|
exit_codes: HEADLESS_EXIT_CODES,
|
|
158
|
-
features:
|
|
159
|
-
bare: true,
|
|
160
|
-
safe_mode: true,
|
|
161
|
-
ephemeral: true,
|
|
162
|
-
background: true,
|
|
163
|
-
worktree: true,
|
|
164
|
-
checkpoint: true,
|
|
165
|
-
json_schema_output: true,
|
|
166
|
-
partial_messages: true,
|
|
167
|
-
interactive_approvals: true,
|
|
168
|
-
permission_prompt_tool: true,
|
|
169
|
-
remote_control: true,
|
|
170
|
-
// Additive protocol-v1 stream fields (agent-sdk docs/PROTOCOL.md §1.2.1):
|
|
171
|
-
// every stream-json stdout line carries a monotonic `seq` and a
|
|
172
|
-
// run-scoped `trace_id`, and tool_use/tool_result lines carry a pairing
|
|
173
|
-
// `id` ("tu-<n>"). `trace_id` is caller-injectable (--trace-id /
|
|
174
|
-
// CC_TRACE_ID) for end-to-end IDE ↔ CLI correlation.
|
|
175
|
-
tool_use_id: true,
|
|
176
|
-
event_seq: true,
|
|
177
|
-
trace_id: true,
|
|
178
|
-
mcp: {
|
|
179
|
-
config_file: true,
|
|
180
|
-
registry: true,
|
|
181
|
-
tool_search: true,
|
|
182
|
-
oauth: true,
|
|
183
|
-
managed_allowlist: true,
|
|
184
|
-
},
|
|
185
|
-
sandbox_engines: ["docker", "bubblewrap"],
|
|
186
|
-
},
|
|
159
|
+
features: toAgentFeatureFlags(),
|
|
187
160
|
};
|
|
188
161
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* hook-event-log — a persistent, hash-chained, replay-safe log of the hook
|
|
5
|
+
* delivery envelopes produced by the unified event bus ([[hook-event-bus.cjs]]).
|
|
6
|
+
*
|
|
7
|
+
* The event bus already stamps every settings-hook delivery with a stable
|
|
8
|
+
* `event_id` (see [[settings-hook-events.cjs]] `withDeliveryId`), but until now
|
|
9
|
+
* the full envelope was discarded — so there was nothing to look up when a user
|
|
10
|
+
* wants `cc hook replay <event-id>`. This store persists each envelope to a
|
|
11
|
+
* tamper-evident JSONL log so the replay command has a real source.
|
|
12
|
+
*
|
|
13
|
+
* Design:
|
|
14
|
+
* - OPT-IN: recording only happens when `CC_HOOK_EVENT_LOG` is enabled, so the
|
|
15
|
+
* default hook-firing path is byte-for-byte unchanged (no new IO per turn).
|
|
16
|
+
* - Hash chain: each line is `{envelope, prevHash, hash}` where
|
|
17
|
+
* `hash = sha256(prevHash + stableStringify(envelope))`. Editing / deleting /
|
|
18
|
+
* reordering any recorded line breaks the chain (mirrors the session
|
|
19
|
+
* transcript integrity design), which `verifyHookEventChain` detects.
|
|
20
|
+
* - Size guard: the log rotates to `<file>.1` once it exceeds MAX_LOG_BYTES so
|
|
21
|
+
* an always-on session can't grow it without bound.
|
|
22
|
+
* - Pure over injected IO (`_deps.fs` / `_deps.os`) so it is fully unit-testable
|
|
23
|
+
* without touching the real home directory.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require("fs");
|
|
27
|
+
const os = require("os");
|
|
28
|
+
const path = require("path");
|
|
29
|
+
const crypto = require("crypto");
|
|
30
|
+
const { stableStringify } = require("./hook-event-bus.cjs");
|
|
31
|
+
|
|
32
|
+
const _deps = { fs, os };
|
|
33
|
+
|
|
34
|
+
/** Cap the always-on log at 5 MiB, then rotate to `<file>.1`. */
|
|
35
|
+
const MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
36
|
+
|
|
37
|
+
/** Default log location: `~/.chainlesschain/hook-events.jsonl`. */
|
|
38
|
+
function hookEventLogPath(homedir) {
|
|
39
|
+
const home = homedir || _deps.os.homedir();
|
|
40
|
+
return path.join(home, ".chainlesschain", "hook-events.jsonl");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Recording is opt-in — `CC_HOOK_EVENT_LOG=1` (or `true`) turns it on. */
|
|
44
|
+
function isHookEventLogEnabled(env = process.env) {
|
|
45
|
+
const v = String(env.CC_HOOK_EVENT_LOG || "").toLowerCase();
|
|
46
|
+
return v === "1" || v === "true" || v === "yes" || v === "on";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Hash one envelope against the previous chain hash (deterministic). */
|
|
50
|
+
function computeChainHash(prevHash, envelope) {
|
|
51
|
+
return crypto
|
|
52
|
+
.createHash("sha256")
|
|
53
|
+
.update(`${prevHash || "genesis"}\n${stableStringify(envelope)}`, "utf-8")
|
|
54
|
+
.digest("hex");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The hash the NEXT record must chain from (last parseable line's `hash`). */
|
|
58
|
+
function latestChainHash(text) {
|
|
59
|
+
const lines = String(text || "").split("\n");
|
|
60
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
61
|
+
if (!lines[i].trim()) continue;
|
|
62
|
+
try {
|
|
63
|
+
const rec = JSON.parse(lines[i]);
|
|
64
|
+
return typeof rec?.hash === "string" ? rec.hash : null;
|
|
65
|
+
} catch {
|
|
66
|
+
// half-written tail line — chain from the previous intact record
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Append a hook envelope to the chained log. Best-effort: never throws — a
|
|
75
|
+
* failure to record must not break the turn that fired the hook.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} envelope a full envelope from buildHookEnvelope
|
|
78
|
+
* @param {object} [opts] { filePath, homedir }
|
|
79
|
+
* @returns {{ok:boolean, event_id?:string, hash?:string, reason?:string}}
|
|
80
|
+
*/
|
|
81
|
+
function appendHookEvent(envelope, { filePath, homedir } = {}) {
|
|
82
|
+
if (!envelope || typeof envelope !== "object" || !envelope.event_id) {
|
|
83
|
+
return { ok: false, reason: "invalid envelope (no event_id)" };
|
|
84
|
+
}
|
|
85
|
+
const f = _deps.fs;
|
|
86
|
+
const fp = filePath || hookEventLogPath(homedir);
|
|
87
|
+
try {
|
|
88
|
+
const dir = path.dirname(fp);
|
|
89
|
+
if (!f.existsSync(dir)) f.mkdirSync(dir, { recursive: true });
|
|
90
|
+
let prevHash = null;
|
|
91
|
+
if (f.existsSync(fp)) {
|
|
92
|
+
let rotated = false;
|
|
93
|
+
try {
|
|
94
|
+
if (f.statSync(fp).size > MAX_LOG_BYTES) {
|
|
95
|
+
f.renameSync(fp, `${fp}.1`);
|
|
96
|
+
rotated = true;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
/* stat/rename best-effort */
|
|
100
|
+
}
|
|
101
|
+
if (!rotated) prevHash = latestChainHash(f.readFileSync(fp, "utf-8"));
|
|
102
|
+
}
|
|
103
|
+
const hash = computeChainHash(prevHash, envelope);
|
|
104
|
+
const line = `${JSON.stringify({ envelope, prevHash, hash })}\n`;
|
|
105
|
+
f.appendFileSync(fp, line, "utf-8");
|
|
106
|
+
return { ok: true, event_id: envelope.event_id, hash };
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return { ok: false, reason: e.message };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Read every recorded record (skips malformed / half-written lines). */
|
|
113
|
+
function readHookEventRecords({ filePath, homedir } = {}) {
|
|
114
|
+
const f = _deps.fs;
|
|
115
|
+
const fp = filePath || hookEventLogPath(homedir);
|
|
116
|
+
if (!f.existsSync(fp)) return [];
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const line of f.readFileSync(fp, "utf-8").split("\n")) {
|
|
119
|
+
if (!line.trim()) continue;
|
|
120
|
+
try {
|
|
121
|
+
out.push(JSON.parse(line));
|
|
122
|
+
} catch {
|
|
123
|
+
/* skip malformed / partial line */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Look up the envelope for a delivery id. The LATEST matching record wins (an
|
|
131
|
+
* event_id can recur across processes; the most recent delivery is what a user
|
|
132
|
+
* replaying "the last one" means).
|
|
133
|
+
*/
|
|
134
|
+
function findHookEvent(eventId, { filePath, homedir } = {}) {
|
|
135
|
+
if (!eventId) return null;
|
|
136
|
+
const records = readHookEventRecords({ filePath, homedir });
|
|
137
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
138
|
+
if (records[i]?.envelope?.event_id === eventId) return records[i].envelope;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The most recent `limit` envelopes (newest last), for `cc hook events-log`. */
|
|
144
|
+
function listHookEvents({ limit = 20, filePath, homedir } = {}) {
|
|
145
|
+
const records = readHookEventRecords({ filePath, homedir });
|
|
146
|
+
const n = Math.max(0, Number(limit) || 0);
|
|
147
|
+
return records.slice(n ? -n : 0).map((r) => r.envelope);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Recompute the chain and report the first break, if any.
|
|
152
|
+
* @returns {{ok:boolean, length:number, brokenAt:number, reason?:string}}
|
|
153
|
+
*/
|
|
154
|
+
function verifyHookEventChain({ filePath, homedir } = {}) {
|
|
155
|
+
const records = readHookEventRecords({ filePath, homedir });
|
|
156
|
+
let prev = null;
|
|
157
|
+
for (let i = 0; i < records.length; i++) {
|
|
158
|
+
const { envelope, prevHash, hash } = records[i] || {};
|
|
159
|
+
if ((prevHash ?? null) !== prev) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
length: records.length,
|
|
163
|
+
brokenAt: i,
|
|
164
|
+
reason: "prevHash does not match the previous record's hash",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const expect = computeChainHash(prev, envelope);
|
|
168
|
+
if (expect !== hash) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
length: records.length,
|
|
172
|
+
brokenAt: i,
|
|
173
|
+
reason: "record hash does not match its envelope (tampered)",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
prev = hash;
|
|
177
|
+
}
|
|
178
|
+
return { ok: true, length: records.length, brokenAt: -1 };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
MAX_LOG_BYTES,
|
|
183
|
+
hookEventLogPath,
|
|
184
|
+
isHookEventLogEnabled,
|
|
185
|
+
computeChainHash,
|
|
186
|
+
latestChainHash,
|
|
187
|
+
appendHookEvent,
|
|
188
|
+
readHookEventRecords,
|
|
189
|
+
findHookEvent,
|
|
190
|
+
listHookEvents,
|
|
191
|
+
verifyHookEventChain,
|
|
192
|
+
_deps,
|
|
193
|
+
};
|