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
|
@@ -216,6 +216,227 @@ export function checkHooks(hooks = [], t) {
|
|
|
216
216
|
return out;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* STATICALLY validate a settings.json `hooks` block for reliably-detectable
|
|
221
|
+
* breakage — a hook that will silently NEVER fire (unknown event name, an
|
|
222
|
+
* uncompilable `/…/` regex matcher, a missing command) or is misconfigured (a
|
|
223
|
+
* non-positive / over-cap timeout). This is the config subset a doctor can
|
|
224
|
+
* catch without running anything; RUNTIME health (slow / circuit-broken hooks)
|
|
225
|
+
* needs live per-process stats and is handled by `checkHooks`, not here.
|
|
226
|
+
*
|
|
227
|
+
* @param {object} hooksBlock raw `.hooks` object from settings.json
|
|
228
|
+
* @param {object} [opts] { validEvents: Set<string>|string[], maxTimeoutSec }
|
|
229
|
+
*/
|
|
230
|
+
export function checkHookConfig(
|
|
231
|
+
hooksBlock,
|
|
232
|
+
{ validEvents, maxTimeoutSec = 600 } = {},
|
|
233
|
+
) {
|
|
234
|
+
const out = [];
|
|
235
|
+
if (!hooksBlock || typeof hooksBlock !== "object") return out;
|
|
236
|
+
const known =
|
|
237
|
+
validEvents instanceof Set ? validEvents : new Set(validEvents || []);
|
|
238
|
+
|
|
239
|
+
for (const [event, groups] of Object.entries(hooksBlock)) {
|
|
240
|
+
// An event key the loader doesn't understand → every hook under it is dead
|
|
241
|
+
// (silently never fires). This is the highest-value static catch.
|
|
242
|
+
if (known.size && !known.has(event)) {
|
|
243
|
+
out.push(
|
|
244
|
+
finding(
|
|
245
|
+
"hook-unknown-event",
|
|
246
|
+
"hooks",
|
|
247
|
+
"warn",
|
|
248
|
+
`Hook event "${event}" is not recognized — its hooks never fire`,
|
|
249
|
+
"fix the event name in settings.json hooks",
|
|
250
|
+
{ ref: event },
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (!Array.isArray(groups)) {
|
|
256
|
+
out.push(
|
|
257
|
+
finding(
|
|
258
|
+
"hook-malformed",
|
|
259
|
+
"hooks",
|
|
260
|
+
"warn",
|
|
261
|
+
`Hook event "${event}" is not an array`,
|
|
262
|
+
"settings.json hooks[<event>] must be an array",
|
|
263
|
+
{ ref: event },
|
|
264
|
+
),
|
|
265
|
+
);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
groups.forEach((g, gi) => {
|
|
269
|
+
const at = `${event}[${gi}]`;
|
|
270
|
+
// A `/body/flags` regex matcher that won't compile silently never matches.
|
|
271
|
+
if (g && typeof g.matcher === "string" && g.matcher.startsWith("/")) {
|
|
272
|
+
const m = g.matcher.match(/^\/(.*)\/([a-z]*)$/);
|
|
273
|
+
try {
|
|
274
|
+
new RegExp(m ? m[1] : g.matcher.slice(1), m ? m[2] : "");
|
|
275
|
+
} catch {
|
|
276
|
+
out.push(
|
|
277
|
+
finding(
|
|
278
|
+
"hook-bad-matcher",
|
|
279
|
+
"hooks",
|
|
280
|
+
"warn",
|
|
281
|
+
`Hook ${at} matcher ${g.matcher} is not a valid regex — it never matches`,
|
|
282
|
+
"fix the regex matcher",
|
|
283
|
+
{ ref: at },
|
|
284
|
+
),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const cmds = g && Array.isArray(g.hooks) ? g.hooks : null;
|
|
289
|
+
if (!cmds) {
|
|
290
|
+
out.push(
|
|
291
|
+
finding(
|
|
292
|
+
"hook-malformed",
|
|
293
|
+
"hooks",
|
|
294
|
+
"warn",
|
|
295
|
+
`Hook ${at} has no hooks array`,
|
|
296
|
+
"each hook group needs a hooks:[] array",
|
|
297
|
+
{ ref: at },
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
cmds.forEach((h, hi) => {
|
|
303
|
+
const hAt = `${at}.hooks[${hi}]`;
|
|
304
|
+
if (!h || typeof h.command !== "string" || !h.command.trim()) {
|
|
305
|
+
out.push(
|
|
306
|
+
finding(
|
|
307
|
+
"hook-no-command",
|
|
308
|
+
"hooks",
|
|
309
|
+
"warn",
|
|
310
|
+
`Hook ${hAt} has no command — it does nothing`,
|
|
311
|
+
"add a command string or remove the entry",
|
|
312
|
+
{ ref: hAt },
|
|
313
|
+
),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
if (h && h.timeout != null) {
|
|
317
|
+
const t = Number(h.timeout);
|
|
318
|
+
if (!Number.isFinite(t) || t <= 0) {
|
|
319
|
+
out.push(
|
|
320
|
+
finding(
|
|
321
|
+
"hook-bad-timeout",
|
|
322
|
+
"hooks",
|
|
323
|
+
"info",
|
|
324
|
+
`Hook ${hAt} timeout ${h.timeout} is not a positive number`,
|
|
325
|
+
"timeout is in seconds; use a positive value",
|
|
326
|
+
{ ref: hAt },
|
|
327
|
+
),
|
|
328
|
+
);
|
|
329
|
+
} else if (t > maxTimeoutSec) {
|
|
330
|
+
out.push(
|
|
331
|
+
finding(
|
|
332
|
+
"hook-bad-timeout",
|
|
333
|
+
"hooks",
|
|
334
|
+
"info",
|
|
335
|
+
`Hook ${hAt} timeout ${t}s exceeds the ${maxTimeoutSec}s cap (clamped)`,
|
|
336
|
+
"lower the timeout",
|
|
337
|
+
{ ref: hAt },
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Sandbox real-capability / silent-degradation (P2 doctor "沙箱真实能力和静默
|
|
350
|
+
* 降级"). Given a probed snapshot, flag the dangerous case: a sandbox is
|
|
351
|
+
* CONFIGURED but its engine is unavailable AND `failIfUnavailable` is off — so
|
|
352
|
+
* tool subprocesses run with NO isolation and nothing warns (you think you're
|
|
353
|
+
* sandboxed but you're not). Strict-but-unavailable is a warn (fail-closed: the
|
|
354
|
+
* agent refuses to start, so it's not silent). Available → an info confirming
|
|
355
|
+
* the true isolation level.
|
|
356
|
+
*
|
|
357
|
+
* @param {object} snapshot { configured, engine, available, reason,
|
|
358
|
+
* failIfUnavailable, isolationLevel }
|
|
359
|
+
*/
|
|
360
|
+
export function checkSandbox(snapshot = {}) {
|
|
361
|
+
const out = [];
|
|
362
|
+
if (!snapshot || !snapshot.configured) return out;
|
|
363
|
+
const engine = snapshot.engine || "sandbox";
|
|
364
|
+
const why = snapshot.reason ? `: ${snapshot.reason}` : "";
|
|
365
|
+
if (snapshot.available) {
|
|
366
|
+
out.push(
|
|
367
|
+
finding(
|
|
368
|
+
"sandbox-active",
|
|
369
|
+
"sandbox",
|
|
370
|
+
"info",
|
|
371
|
+
`Sandbox ${engine} available (isolation: ${snapshot.isolationLevel || "unknown"})`,
|
|
372
|
+
"",
|
|
373
|
+
{ ref: engine },
|
|
374
|
+
),
|
|
375
|
+
);
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
if (snapshot.failIfUnavailable) {
|
|
379
|
+
out.push(
|
|
380
|
+
finding(
|
|
381
|
+
"sandbox-strict-unavailable",
|
|
382
|
+
"sandbox",
|
|
383
|
+
"warn",
|
|
384
|
+
`Sandbox ${engine} is configured (strict) but unavailable${why} — the agent refuses to start until it's installed`,
|
|
385
|
+
`install ${engine} or unset sandbox.failIfUnavailable`,
|
|
386
|
+
{ ref: engine },
|
|
387
|
+
),
|
|
388
|
+
);
|
|
389
|
+
} else {
|
|
390
|
+
out.push(
|
|
391
|
+
finding(
|
|
392
|
+
"sandbox-silent-degrade",
|
|
393
|
+
"sandbox",
|
|
394
|
+
"error",
|
|
395
|
+
`Sandbox ${engine} is configured but unavailable${why} — commands run WITHOUT isolation and won't warn`,
|
|
396
|
+
`install ${engine}, or set sandbox.failIfUnavailable:true to fail closed`,
|
|
397
|
+
{ ref: engine },
|
|
398
|
+
),
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Skill ids defined MORE THAN ONCE across (or within) layers (P2 doctor "重复
|
|
406
|
+
* Skill"). The loader's loadAll() silently keeps only the highest-priority copy
|
|
407
|
+
* per id (Map.set overwrite), so a user never learns their custom skill shadowed
|
|
408
|
+
* — or was shadowed by — another. Given the full, un-deduped per-layer entry
|
|
409
|
+
* list, flag every id with more than one definition.
|
|
410
|
+
*
|
|
411
|
+
* @param {Array<{id:string, layer:string}>} entries
|
|
412
|
+
*/
|
|
413
|
+
export function checkDuplicateSkills(entries = []) {
|
|
414
|
+
const byId = new Map(); // id → layer[]
|
|
415
|
+
for (const e of entries) {
|
|
416
|
+
if (!e || !e.id) continue;
|
|
417
|
+
const arr = byId.get(e.id) || [];
|
|
418
|
+
arr.push(e.layer || "?");
|
|
419
|
+
byId.set(e.id, arr);
|
|
420
|
+
}
|
|
421
|
+
const out = [];
|
|
422
|
+
for (const [id, layers] of byId) {
|
|
423
|
+
if (layers.length > 1) {
|
|
424
|
+
const uniq = [...new Set(layers)];
|
|
425
|
+
out.push(
|
|
426
|
+
finding(
|
|
427
|
+
"skill-duplicate",
|
|
428
|
+
"skills",
|
|
429
|
+
"warn",
|
|
430
|
+
`Skill "${id}" has ${layers.length} definitions (${uniq.join(", ")}) — only the highest-priority copy loads`,
|
|
431
|
+
"rename or remove the redundant skill copy",
|
|
432
|
+
{ ref: id, layers: uniq },
|
|
433
|
+
),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
|
|
219
440
|
/** Plugins / LSP servers reported unhealthy or dead. */
|
|
220
441
|
export function checkPluginsAndLsp(plugins = []) {
|
|
221
442
|
const out = [];
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recall-first secret scanner + a labeled corpus and recall / false-positive /
|
|
3
|
+
* latency benchmark (IDE gap §8.1 "凭据脱敏真实语料基准" of
|
|
4
|
+
* CLAUDE_CODE_IDE_INCREMENTAL_GAP_ANALYSIS_2026-07-13.md).
|
|
5
|
+
*
|
|
6
|
+
* [[ide-context-redaction.js]] `redactSecretsInText` is deliberately PRECISION-
|
|
7
|
+
* first: it guards editor context that rides into the model, where mangling
|
|
8
|
+
* everyday code teaches users to disable it. That posture is right there, but it
|
|
9
|
+
* is the wrong one for the EXPORT gate — a diagnostic bundle / artifact / trace
|
|
10
|
+
* that leaves the machine must fail toward NOT leaking, i.e. recall-first. §8.1
|
|
11
|
+
* asks to prove a redactor with a real corpus (provider tokens, JWT, Bearer,
|
|
12
|
+
* PEM, cloud creds, connection strings, cookies, `.env` variants) plus counter-
|
|
13
|
+
* examples (sample values, hashes, UUIDs, plain base64) and to report recall,
|
|
14
|
+
* false-positive rate, P95 latency and max input size — per surface.
|
|
15
|
+
*
|
|
16
|
+
* This module is that recall-first scanner (`scanSecrets` / `redactSecrets`),
|
|
17
|
+
* the labeled `SECRET_CORPUS`, and `runRedactionBenchmark` which measures ANY
|
|
18
|
+
* redactor against it. It does NOT touch the precision-first IDE redactor, so
|
|
19
|
+
* default IDE-context behavior is unchanged; this is the scanner the future
|
|
20
|
+
* "Secret Scan before export" gate (P1-9 诊断包) runs.
|
|
21
|
+
*
|
|
22
|
+
* Pure: no fs / RNG / clock. Latency is measured only when a `clock` is
|
|
23
|
+
* injected, keeping the module deterministic in tests.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { isSecretEnvName } from "./credential-guard.js";
|
|
27
|
+
|
|
28
|
+
export const REDACTED = "[REDACTED]";
|
|
29
|
+
|
|
30
|
+
/** The export surfaces §8.1 wants measured independently. */
|
|
31
|
+
export const SURFACES = Object.freeze([
|
|
32
|
+
"source_context",
|
|
33
|
+
"transcript",
|
|
34
|
+
"trace",
|
|
35
|
+
"artifact",
|
|
36
|
+
"diagnostic_bundle",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/** The secret categories the scanner covers. */
|
|
40
|
+
export const SECRET_CATEGORIES = Object.freeze([
|
|
41
|
+
"provider_token",
|
|
42
|
+
"jwt",
|
|
43
|
+
"bearer",
|
|
44
|
+
"pem_private_key",
|
|
45
|
+
"connection_string",
|
|
46
|
+
"cookie",
|
|
47
|
+
"env_assignment",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
// Each pattern is stored as a SOURCE string so `scanSecrets` compiles a fresh
|
|
51
|
+
// RegExp per call — a shared /g/ regex carries `lastIndex` state across calls
|
|
52
|
+
// and would skip matches. `replace` resets state itself, but scan/match do not.
|
|
53
|
+
const PATTERNS = [
|
|
54
|
+
{
|
|
55
|
+
category: "provider_token",
|
|
56
|
+
// OpenAI/Anthropic sk-, GitHub PAT/OAuth, GitLab PAT, Slack xox*, AWS keys.
|
|
57
|
+
source:
|
|
58
|
+
"\\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{16,}|xox[baprs]-[A-Za-z0-9-]{10,}|(?:AKIA|ASIA)[0-9A-Z]{16})\\b",
|
|
59
|
+
flags: "g",
|
|
60
|
+
replace: () => REDACTED,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
category: "jwt",
|
|
64
|
+
// Three base64url segments; the first two begin `eyJ` (base64 of `{"`).
|
|
65
|
+
source:
|
|
66
|
+
"\\beyJ[A-Za-z0-9_-]{6,}\\.eyJ[A-Za-z0-9_-]{6,}\\.[A-Za-z0-9_-]{6,}\\b",
|
|
67
|
+
flags: "g",
|
|
68
|
+
replace: () => REDACTED,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
category: "bearer",
|
|
72
|
+
source: "\\bBearer\\s+[A-Za-z0-9\\-._~+/=]{8,}",
|
|
73
|
+
flags: "g",
|
|
74
|
+
replace: () => `Bearer ${REDACTED}`,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
category: "pem_private_key",
|
|
78
|
+
// The bytes ARE the secret; a truncated END marker still redacts to the end.
|
|
79
|
+
source:
|
|
80
|
+
"-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\\s\\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)",
|
|
81
|
+
flags: "g",
|
|
82
|
+
replace: () => REDACTED,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
category: "connection_string",
|
|
86
|
+
// scheme://user:PASSWORD@host — the embedded password is the secret. Only
|
|
87
|
+
// the credential form (with `:pass@`) matches, so `https://example.com` is
|
|
88
|
+
// untouched. Keep the scheme + user, swallow the password + host tail.
|
|
89
|
+
source:
|
|
90
|
+
"\\b((?:postgres|postgresql|mysql|mongodb(?:\\+srv)?|redis|rediss|amqp|amqps|https?|ftp|ssh):\\/\\/[^:\\s/@]+:)[^@\\s/]+@\\S+",
|
|
91
|
+
flags: "gi",
|
|
92
|
+
replace: (_m, prefix) => `${prefix}${REDACTED}`,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
category: "cookie",
|
|
96
|
+
// A Cookie / Set-Cookie header value. Keep the header name, swallow the value.
|
|
97
|
+
source: "\\b(Set-Cookie|Cookie)(\\s*:\\s*)\\S[^\\n\\r]*",
|
|
98
|
+
flags: "gi",
|
|
99
|
+
replace: (_m, name, sep) => `${name}${sep}${REDACTED}`,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
category: "env_assignment",
|
|
103
|
+
// `API_KEY = "value"` / `token: value`. The key must contain a boundary-
|
|
104
|
+
// delimited secret word (isSecretEnvName), the value a bare token-like run.
|
|
105
|
+
// The lookahead rejects `token = getToken()` (code, not a secret).
|
|
106
|
+
source:
|
|
107
|
+
"\\b([A-Za-z_][A-Za-z0-9_-]*)([\"']?\\s*[=:]\\s*)([\"']?)([A-Za-z0-9+/=_.\\-]{8,})\\3(?![A-Za-z0-9+/=_.\\-(])",
|
|
108
|
+
flags: "g",
|
|
109
|
+
replace: (m, key, sep, quote) =>
|
|
110
|
+
isSecretEnvName(key.replace(/-/g, "_"))
|
|
111
|
+
? `${key}${sep}${quote}${REDACTED}${quote}`
|
|
112
|
+
: m, // key isn't secret-shaped → leave the assignment alone
|
|
113
|
+
// env_assignment only counts as a detection when the key gates true; a
|
|
114
|
+
// guard so scanSecrets doesn't report `count = 12345678` as a secret.
|
|
115
|
+
guard: (m, groups) => isSecretEnvName(String(groups[0]).replace(/-/g, "_")),
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Find every secret-shaped span in `text`. Recall-first: overlapping categories
|
|
121
|
+
* all report, so an assignment holding a JWT is caught by both.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} text
|
|
124
|
+
* @returns {Array<{category:string, index:number, length:number, match:string}>}
|
|
125
|
+
*/
|
|
126
|
+
export function scanSecrets(text) {
|
|
127
|
+
if (typeof text !== "string" || text.length === 0) return [];
|
|
128
|
+
const findings = [];
|
|
129
|
+
for (const p of PATTERNS) {
|
|
130
|
+
const re = new RegExp(p.source, p.flags);
|
|
131
|
+
for (const m of text.matchAll(re)) {
|
|
132
|
+
if (p.guard && !p.guard(m[0], m.slice(1))) continue;
|
|
133
|
+
findings.push({
|
|
134
|
+
category: p.category,
|
|
135
|
+
index: m.index,
|
|
136
|
+
length: m[0].length,
|
|
137
|
+
match: m[0],
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return findings.sort((a, b) => a.index - b.index || a.length - b.length);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Redact every secret-shaped span, keeping the surviving structure a human /
|
|
146
|
+
* agent still needs (the `Bearer` word, a header/key name, a connection scheme)
|
|
147
|
+
* while swallowing the secret value.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} text
|
|
150
|
+
* @returns {string}
|
|
151
|
+
*/
|
|
152
|
+
export function redactSecrets(text) {
|
|
153
|
+
if (typeof text !== "string" || text.length === 0) return text;
|
|
154
|
+
let out = text;
|
|
155
|
+
for (const p of PATTERNS) {
|
|
156
|
+
const re = new RegExp(p.source, p.flags);
|
|
157
|
+
out = out.replace(re, p.replace);
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Whether `text` contains any secret (the export gate's fail-closed check). */
|
|
163
|
+
export function containsSecret(text) {
|
|
164
|
+
return scanSecrets(text).length > 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Labeled corpus ───────────────────────────────────────────────────────────
|
|
168
|
+
//
|
|
169
|
+
// Positives carry the exact `secret` substring that MUST disappear after
|
|
170
|
+
// redaction; negatives (sample/placeholder values, hashes, UUIDs, plain base64,
|
|
171
|
+
// ordinary code) MUST pass through byte-for-byte. Items are spread across the
|
|
172
|
+
// export surfaces so the benchmark can report per-surface.
|
|
173
|
+
|
|
174
|
+
const POSITIVES = [
|
|
175
|
+
{
|
|
176
|
+
id: "openai",
|
|
177
|
+
category: "provider_token",
|
|
178
|
+
surface: "transcript",
|
|
179
|
+
secret: "sk-abcd1234efgh5678ijkl",
|
|
180
|
+
text: 'client = OpenAI(api_key="sk-abcd1234efgh5678ijkl")',
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: "github_pat",
|
|
184
|
+
category: "provider_token",
|
|
185
|
+
surface: "artifact",
|
|
186
|
+
secret: "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
|
|
187
|
+
text: "git remote set-url origin https://ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@github.com/x/y",
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: "aws",
|
|
191
|
+
category: "provider_token",
|
|
192
|
+
surface: "diagnostic_bundle",
|
|
193
|
+
secret: "AKIAIOSFODNN7EXAMPLE",
|
|
194
|
+
text: "aws_access_key_id AKIAIOSFODNN7EXAMPLE",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: "jwt",
|
|
198
|
+
category: "jwt",
|
|
199
|
+
surface: "trace",
|
|
200
|
+
secret:
|
|
201
|
+
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
|
|
202
|
+
text: "Authorization header carried eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: "bearer",
|
|
206
|
+
category: "bearer",
|
|
207
|
+
surface: "source_context",
|
|
208
|
+
secret: "aG9sZDpteS1zZWNyZXQtdG9rZW4",
|
|
209
|
+
text: "curl -H 'Authorization: Bearer aG9sZDpteS1zZWNyZXQtdG9rZW4' https://api",
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: "pem",
|
|
213
|
+
category: "pem_private_key",
|
|
214
|
+
surface: "artifact",
|
|
215
|
+
secret:
|
|
216
|
+
"-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBAKj34Gkx...\n-----END RSA PRIVATE KEY-----",
|
|
217
|
+
text: "key file:\n-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBAKj34Gkx...\n-----END RSA PRIVATE KEY-----\n",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: "postgres",
|
|
221
|
+
category: "connection_string",
|
|
222
|
+
surface: "diagnostic_bundle",
|
|
223
|
+
secret: "s3cr3tP@ss",
|
|
224
|
+
text: "DATABASE_URL=postgres://appuser:s3cr3tP@ss@db.internal:5432/prod",
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: "mongodb",
|
|
228
|
+
category: "connection_string",
|
|
229
|
+
surface: "trace",
|
|
230
|
+
secret: "hunter2hunter2",
|
|
231
|
+
text: "mongodb+srv://svc:hunter2hunter2@cluster0.mongodb.net/app",
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: "cookie",
|
|
235
|
+
category: "cookie",
|
|
236
|
+
surface: "transcript",
|
|
237
|
+
secret: "sessionid=9f8b7c6d5e4f3a2b1c0d",
|
|
238
|
+
text: "Set-Cookie: sessionid=9f8b7c6d5e4f3a2b1c0d; HttpOnly",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: "env_password",
|
|
242
|
+
category: "env_assignment",
|
|
243
|
+
surface: "diagnostic_bundle",
|
|
244
|
+
secret: "Sup3rSecret_Value-123",
|
|
245
|
+
text: 'DB_PASSWORD="Sup3rSecret_Value-123"',
|
|
246
|
+
},
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
const NEGATIVES = [
|
|
250
|
+
{
|
|
251
|
+
id: "uuid",
|
|
252
|
+
surface: "trace",
|
|
253
|
+
text: "requestId = 550e8400-e29b-41d4-a716-446655440000",
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: "git_sha",
|
|
257
|
+
surface: "transcript",
|
|
258
|
+
text: "commit beaa3f9ec7069258aad5e8587d32f779da5e5d20 landed",
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: "plain_base64",
|
|
262
|
+
surface: "artifact",
|
|
263
|
+
text: "payload blob aGVsbG8gd29ybGQgdGhpcyBpcyBub3QgYSBzZWNyZXQ=",
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: "prose",
|
|
267
|
+
surface: "source_context",
|
|
268
|
+
text: "The function returns the number of open editor tabs.",
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: "code_call",
|
|
272
|
+
surface: "source_context",
|
|
273
|
+
text: "const token = getToken(config)",
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
id: "nonsecret_assignment",
|
|
277
|
+
surface: "diagnostic_bundle",
|
|
278
|
+
text: "retryCount = 12345678",
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: "plain_url",
|
|
282
|
+
surface: "artifact",
|
|
283
|
+
text: "docs at https://example.com/guide/index.html",
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: "hex_digest",
|
|
287
|
+
surface: "diagnostic_bundle",
|
|
288
|
+
text: "sha256 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
|
|
289
|
+
},
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
export const SECRET_CORPUS = Object.freeze({
|
|
293
|
+
positives: Object.freeze(POSITIVES.map((p) => Object.freeze(p))),
|
|
294
|
+
negatives: Object.freeze(NEGATIVES.map((n) => Object.freeze(n))),
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
function percentile(sortedAsc, p) {
|
|
298
|
+
if (sortedAsc.length === 0) return null;
|
|
299
|
+
const idx = Math.min(
|
|
300
|
+
sortedAsc.length - 1,
|
|
301
|
+
Math.max(0, Math.ceil((p / 100) * sortedAsc.length) - 1),
|
|
302
|
+
);
|
|
303
|
+
return sortedAsc[idx];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Measure a redactor against a labeled corpus: recall (positives whose secret
|
|
308
|
+
* disappeared), false-positive rate (negatives that were altered), max input
|
|
309
|
+
* size, and — only when a `clock` is injected — P95 redaction latency. Reports
|
|
310
|
+
* overall, per category (recall) and per surface (recall + FPR).
|
|
311
|
+
*
|
|
312
|
+
* @param {object} [corpus=SECRET_CORPUS]
|
|
313
|
+
* @param {object} [opts]
|
|
314
|
+
* @param {(text:string)=>string} [opts.redactor=redactSecrets]
|
|
315
|
+
* @param {()=>number} [opts.clock] ms clock; enables latency metrics
|
|
316
|
+
* @returns {object} the benchmark report
|
|
317
|
+
*/
|
|
318
|
+
export function runRedactionBenchmark(corpus = SECRET_CORPUS, opts = {}) {
|
|
319
|
+
const redactor =
|
|
320
|
+
typeof opts.redactor === "function" ? opts.redactor : redactSecrets;
|
|
321
|
+
const clock = typeof opts.clock === "function" ? opts.clock : null;
|
|
322
|
+
const positives = Array.isArray(corpus?.positives) ? corpus.positives : [];
|
|
323
|
+
const negatives = Array.isArray(corpus?.negatives) ? corpus.negatives : [];
|
|
324
|
+
|
|
325
|
+
const latencies = [];
|
|
326
|
+
let maxInputBytes = 0;
|
|
327
|
+
const measure = (text) => {
|
|
328
|
+
maxInputBytes = Math.max(maxInputBytes, Buffer.byteLength(text, "utf8"));
|
|
329
|
+
if (!clock) return redactor(text);
|
|
330
|
+
const t0 = Number(clock());
|
|
331
|
+
const out = redactor(text);
|
|
332
|
+
const t1 = Number(clock());
|
|
333
|
+
if (Number.isFinite(t0) && Number.isFinite(t1)) latencies.push(t1 - t0);
|
|
334
|
+
return out;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const catRecall = {}; // category -> {detected, total}
|
|
338
|
+
const surface = {}; // surface -> {detected, posTotal, falsePos, negTotal}
|
|
339
|
+
const bumpSurface = (s) =>
|
|
340
|
+
(surface[s] = surface[s] || {
|
|
341
|
+
detected: 0,
|
|
342
|
+
posTotal: 0,
|
|
343
|
+
falsePos: 0,
|
|
344
|
+
negTotal: 0,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const missed = [];
|
|
348
|
+
let detected = 0;
|
|
349
|
+
for (const item of positives) {
|
|
350
|
+
const out = measure(item.text);
|
|
351
|
+
const ok = typeof item.secret === "string" && !out.includes(item.secret);
|
|
352
|
+
const cat = (catRecall[item.category] = catRecall[item.category] || {
|
|
353
|
+
detected: 0,
|
|
354
|
+
total: 0,
|
|
355
|
+
});
|
|
356
|
+
cat.total += 1;
|
|
357
|
+
const s = bumpSurface(item.surface);
|
|
358
|
+
s.posTotal += 1;
|
|
359
|
+
if (ok) {
|
|
360
|
+
detected += 1;
|
|
361
|
+
cat.detected += 1;
|
|
362
|
+
s.detected += 1;
|
|
363
|
+
} else {
|
|
364
|
+
missed.push(item.id);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const falsePositives = [];
|
|
369
|
+
let fp = 0;
|
|
370
|
+
for (const item of negatives) {
|
|
371
|
+
const out = measure(item.text);
|
|
372
|
+
const s = bumpSurface(item.surface);
|
|
373
|
+
s.negTotal += 1;
|
|
374
|
+
if (out !== item.text) {
|
|
375
|
+
fp += 1;
|
|
376
|
+
s.falsePos += 1;
|
|
377
|
+
falsePositives.push(item.id);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const byCategory = {};
|
|
382
|
+
for (const [c, v] of Object.entries(catRecall)) {
|
|
383
|
+
byCategory[c] = { recall: v.total ? v.detected / v.total : 1, ...v };
|
|
384
|
+
}
|
|
385
|
+
const bySurface = {};
|
|
386
|
+
for (const [sName, v] of Object.entries(surface)) {
|
|
387
|
+
bySurface[sName] = {
|
|
388
|
+
recall: v.posTotal ? v.detected / v.posTotal : 1,
|
|
389
|
+
falsePositiveRate: v.negTotal ? v.falsePos / v.negTotal : 0,
|
|
390
|
+
...v,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const sorted = latencies.slice().sort((a, b) => a - b);
|
|
395
|
+
return {
|
|
396
|
+
overall: {
|
|
397
|
+
recall: positives.length ? detected / positives.length : 1,
|
|
398
|
+
falsePositiveRate: negatives.length ? fp / negatives.length : 0,
|
|
399
|
+
positives: positives.length,
|
|
400
|
+
negatives: negatives.length,
|
|
401
|
+
maxInputBytes,
|
|
402
|
+
p95LatencyMs: clock ? percentile(sorted, 95) : null,
|
|
403
|
+
},
|
|
404
|
+
missed,
|
|
405
|
+
falsePositives,
|
|
406
|
+
byCategory,
|
|
407
|
+
bySurface,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
Binary file
|