chainlesschain 0.162.166 → 0.162.167
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-LbDx7jL6.js → AIOps-BZJrchIA.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DnurtK34.js → ActionButton-Bhlrylt7.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DXSsV1am.js → Analytics-pHtXVQYs.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-dqRpzNuD.js → AppLayout-ORpuri4R.js} +3 -3
- package/src/assets/web-panel/assets/{Artifacts-BwdmYepK.js → Artifacts-eC77yr1_.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-oSX0kOUG.js → Audit-B2dZIiqz.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-C2KxJahT.js → BackgroundAgents-COZiTiwP.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-B4TkxWkC.js → Backup-vY_Srl5d.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CX5-KxeU.js → BaseInput-Dili5BdM.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-O_HzD-PL.js → Chat-EzKBf0XL.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-Br8saHQA.js → ChatBubbleRenderer-D1CtRAyy.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-ChepDpC1.js → Checkbox-Beh6BmuW.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-D9z8kc7q.js → Codegen-C3fA6kBU.js} +1 -1
- package/src/assets/web-panel/assets/{Col-B-H5eoNw.js → Col-CefMqONH.js} +1 -1
- package/src/assets/web-panel/assets/{Community-Ccyp_Kk5.js → Community-CBAxFJtB.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BNvBznp_.js → Compact--flHc-sM.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-D1FjRd8n.js → Compliance-CPeNLvA6.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CV9B9u-8.js → Cowork-Bv5yv4BU.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-BowJ4VIx.js → Cron-C22yMEas.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BJc6jzRF.js → Crosschain-Dsb2bcpu.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BEp8NV6D.js → DID-DAxei4w0.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Dls0B3YB.js → Dashboard-DEUwuVZ8.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DQ7zXVtD.js → Dropdown-BVp4si2j.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-B9q79tOl.js → EmailListRenderer-CyFkybOG.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CD8Ie-88.js → FamilyGuardDashboard-BWmmsbVx.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-5o7XU5Vf.js → Federation-Cxw27A5d.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DeKbMvOs.js → FormItemContext-Q9XthzL1.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CZiQvz_z.js → GenericCardRenderer-DS5DIdsW.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CTvFxVV8.js → Git-BOliHfxP.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DwzFIjHt.js → Governance-CD4oxa-W.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CJBk59c-.js → Inference-CMlcivo8.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DfsaXN2z.js → KnowledgeGraph-BwhERcC4.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DVRYf18S.js → Logs-BjVcZR5P.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-BfRhXdyG.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-BuIwyE4t.js → Marketplace-DQLU3E2w.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-D3U0o78E.js → McpTools-CeK0XkpB.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-QYVmyb3b.js → Memory-DLFkgwaz.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BFgrj6eZ.js → MobileBridge-Bh6ymXyj.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-Bp5QB-46.js → MobileProjects-D950tUIe.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BZQu8TcH.js → Mtc-Bx5x9_2m.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-BnFfEkLo.js → MtcAudit-a1-Fexsq.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-iUnNS9Cm.js → Multisig-eaw0X8X1.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DNNz1y9Y.js → NLProgramming-BaNsPV4k.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CZezNYRU.js → Notes-C7pevN2X.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-DBvrplIR.js → NotificationSettings-HDqeoP0r.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-CTDtee49.js → OrderTableRenderer-DvzVl5T3.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-CCOUJX1R.js → Organization-D_xHDlGW.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-DcZghdk0.js → Overflow-B5sRnueL.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-jmddWX9q.js → P2P-C-IRjOPW.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-wfjMcczl.js → PdhVaultBrowser-BqgUPlcU.js} +4 -4
- package/src/assets/web-panel/assets/{Permissions-DU1GJjca.js → Permissions-Bm_T1uWw.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BzTMzDF9.js → PersonalDataHub-DujDllye.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-DPmjlpVc.js → Pipeline-DyNPOLIG.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-B6HRLpfK.js → Privacy-CYKTW3iK.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Cyy86AsN.js → ProjectInit-CGf03uZ0.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-Bl_WQxJ5.js → ProjectSettings-DIec4bTI.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-Co9yKThA.js → Projects-BY43-bmi.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DLa4NlIs.js → Providers-RrnUmgPk.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BvzM7q5f.js → QrScannerModal-BBWfqd4y.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BN8yyyUj.js → QuickAsk-BsICSrFp.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-QsVN4w0i.js → Recommend-B8y0b4ys.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-tkDXw-TY.js → RemoteSession-0QgdB1Oe.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-DAifkGsy.js → Reputation-DBJGM0YO.js} +1 -1
- package/src/assets/web-panel/assets/{Row-D7dZEyak.js → Row-DtWQ5otv.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-DB5sImTA.js → RssFeed-2zBxHd5H.js} +3 -3
- package/src/assets/web-panel/assets/{Search-3py3TO9V.js → Search-BofliSkZ.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DhNUfT_k.js → Security-ockRBBEL.js} +3 -3
- package/src/assets/web-panel/assets/{Services-DjJAA_5D.js → Services-QyvG7Vd0.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-u95JjK3n.js → Skeleton-DwbOndX3.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DPo3L0Tz.js → Skills-cHWa63sZ.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DWhkAZV1.js → Sla-BHnTEydQ.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-PUegMtqV.js → SpeechSettings-DE2hrrLb.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BMNYrnrU.js → SyncSettings-CZ9vnbJH.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-DWgoAHvM.js +1 -0
- package/src/assets/web-panel/assets/{Templates-Dw9Er5DN.js → Templates-DiR3ad4X.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-7P3QIBSo.js → Tenant-NqcW3frV.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-ClPrfWW7.js → Terminal-iYeqRQZ0.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-DbCd1bDH.js → TimelineRenderer-C5GOoxFz.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-EQofpA0t.js → Tokens-82dz0XdU.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-ChNLLypd.js → Trigger-B3JCNrOi.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-dlVGUKs5.js → Trust-BehFYsN_.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BXyBUhNF.js → UkeySign-BfOxJjVf.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BE8-68ge.js → VideoEditing-DdITCCPz.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-7ZIJOwNg.js → Wallet-B0EhkCmD.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DgNd2Amr.js → WebAuthn-DJ4i1f3-.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-Z-Un-9l5.js → WorkflowEditor-Dr9XTqIT.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CtVDEkUc.js → chat-Dc2nFyOj.js} +1 -1
- package/src/assets/web-panel/assets/{colors-PXtJ2f1B.js → colors-Drw9djBR.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-oN96nuBi.js → compact-item-BdLXbU52.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-okCJXQXa.js → createContext-BpQMZiWY.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CnfpDzMr.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-pcHm--4o.js → hasIn-CbYwawHf.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZKk4Lnm.js → index-B1ANzVc6.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQYes2mk.js → index-B3uRI8_y.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cc_MkksW.js → index-B5KboakZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQ1ZzJjN.js → index-B9_u4Qhe.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bht9n8Sm.js → index-BAq-4KqY.js} +1 -1
- package/src/assets/web-panel/assets/{index-_SaFHNsG.js → index-BBueKSTR.js} +1 -1
- package/src/assets/web-panel/assets/{index-jAY55MSx.js → index-BIcEW07n.js} +1 -1
- package/src/assets/web-panel/assets/{index-nMqLdwao.js → index-BWUoc9-s.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHJIfdCV.js → index-BZxzdAI0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAQktO-6.js → index-BiGDrEt4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BjVIZfdA.js → index-Blv7c80V.js} +1 -1
- package/src/assets/web-panel/assets/{index-BEtzqJw3.js → index-BmQlot_u.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0bfypsF.js → index-Bo_KwJTF.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0rfJBU3.js → index-Bu8Vy-mj.js} +1 -1
- package/src/assets/web-panel/assets/{index-BuME9k4Y.js → index-C1eo1Qkd.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1efSh1X.js → index-C3Alz2QD.js} +1 -1
- package/src/assets/web-panel/assets/{index-DBcKBb1E.js → index-CBwQwtsE.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4ravjdv.js → index-CJQiSQw7.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cs1fGk1Z.js → index-CKmz3Is-.js} +1 -1
- package/src/assets/web-panel/assets/{index-BUUJ2WfG.js → index-COoIIMbO.js} +1 -1
- package/src/assets/web-panel/assets/index-CYTEB0tb.js +1 -0
- package/src/assets/web-panel/assets/index-CgsEp5bm.js +1 -0
- package/src/assets/web-panel/assets/{index-DC0bqrqV.js → index-CpmlTfpv.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bl6N7ZyX.js → index-D7Ipfs4F.js} +1 -1
- package/src/assets/web-panel/assets/{index-vW0lwj0Y.js → index-DHMeEu-P.js} +1 -1
- package/src/assets/web-panel/assets/{index-mB1UV7wA.js → index-DKQlF7Pw.js} +1 -1
- package/src/assets/web-panel/assets/{index-BDR2mi1p.js → index-DO7LD_1t.js} +1 -1
- package/src/assets/web-panel/assets/{index-CK0H3pQW.js → index-DSmZjrFy.js} +1 -1
- package/src/assets/web-panel/assets/{index-DtsyPnF5.js → index-DTeqWdAS.js} +1 -1
- package/src/assets/web-panel/assets/{index-T3QE8LuV.js → index-DWUN8c01.js} +1 -1
- package/src/assets/web-panel/assets/{index-BUTyx-Lk.js → index-D_lkTnZ0.js} +1 -1
- package/src/assets/web-panel/assets/{index-5aO2V0V4.js → index-DyU21xVi.js} +1 -1
- package/src/assets/web-panel/assets/{index-CWUJlCvK.js → index-GG_MXjSL.js} +3 -3
- package/src/assets/web-panel/assets/{index-CopGaCI_.js → index-IG3__pGj.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnhHxZul.js → index-UX_Mfyfv.js} +1 -1
- package/src/assets/web-panel/assets/{index-CXhezK4d.js → index-VIyNlXPX.js} +1 -1
- package/src/assets/web-panel/assets/{index-Rk0X8lVz.js → index-XYe6kBmi.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfL10EcN.js → index-_vVucYqC.js} +1 -1
- package/src/assets/web-panel/assets/{index-BeAWCGEK.js → index-aHrUfrv1.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CG1jyiI7.js → initDefaultProps-DybC85E4.js} +1 -1
- package/src/assets/web-panel/assets/{motion-C8BYa98D.js → motion-BNNuyDg7.js} +1 -1
- package/src/assets/web-panel/assets/{move-D1hqwitW.js → move-Bm8gSjEN.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-C2lol8rp.js → mtc-parser-CuHQvzzr.js} +1 -1
- package/src/assets/web-panel/assets/{omit-Ct6Zghfi.js → omit-lv4NO7Ng.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CSYbYfdc.js → pickAttrs-BoSMfjHq.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-WCk8di0z.js → placementArrow-BOMDSvgz.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-CEC8CZ-3.js → responsiveObserve-C064Ubmu.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BsXyd7Lq.js → slide-CeqhbKoG.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-tDKldiUT.js → statusUtils-CunFoYww.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BIS2unhx.js → styleChecker-Cxz3wPL7.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-yFTVMDUv.js → useFlexGapSupport-DJsIaU2S.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CZYbJwkO.js → useFs-DnAygceJ.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-sbEHRLAM.js → usePersonalDataHub-CrEUY7ta.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BVjeSEQK.js → vnode-Cq3rN2eT.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-BZKVvEZ6.js → zoom-TMzcOx__.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +75 -0
- package/src/commands/session.js +132 -0
- package/src/data/changelog.json +7 -2
- package/src/harness/jsonl-session-store.js +9 -0
- package/src/harness/session-mirror.js +212 -6
- package/src/lib/async-hook-queue.cjs +288 -0
- package/src/lib/async-hook-supervisor.cjs +52 -0
- package/src/lib/doctor-checkup.js +184 -4
- package/src/lib/install-command-policy.js +333 -0
- package/src/lib/jsonl-session-store.js +1 -0
- package/src/lib/lsp/__tests__/lsp-server-registry.test.js +27 -0
- package/src/lib/lsp/lsp-server-registry.js +14 -0
- package/src/lib/runtime-checkup.js +33 -0
- package/src/lib/settings-hook-events.cjs +157 -0
- package/src/lib/settings-hooks.cjs +4 -0
- package/src/lib/shell-approval.js +47 -1
- package/src/repl/add-dir.js +59 -1
- package/src/repl/agent-repl.js +85 -3
- package/src/runtime/headless-runner.js +69 -1
- package/src/runtime/headless-stream.js +22 -0
- package/src/runtime/system-prompt.js +39 -12
- package/src/assets/web-panel/assets/MarkdownRenderer-BL_WsWZM.js +0 -1
- package/src/assets/web-panel/assets/Tasks-CYm_oTJC.js +0 -1
- package/src/assets/web-panel/assets/devWarning-BzV6dk1B.js +0 -1
- package/src/assets/web-panel/assets/index-BFrAwXHp.js +0 -1
- package/src/assets/web-panel/assets/index-DJQFP47u.js +0 -1
|
@@ -26,6 +26,11 @@ import {
|
|
|
26
26
|
import { execSync, spawnSync } from "node:child_process";
|
|
27
27
|
import { join, basename } from "node:path";
|
|
28
28
|
import { getHomeDir, getConfigPath } from "./paths.js";
|
|
29
|
+
import {
|
|
30
|
+
languageIdForFile,
|
|
31
|
+
resolveServer,
|
|
32
|
+
describeServer,
|
|
33
|
+
} from "./lsp/lsp-server-registry.js";
|
|
29
34
|
import {
|
|
30
35
|
checkAgenda,
|
|
31
36
|
checkInstructionFiles,
|
|
@@ -33,6 +38,8 @@ import {
|
|
|
33
38
|
checkHooks,
|
|
34
39
|
checkSandbox,
|
|
35
40
|
checkDuplicateSkills,
|
|
41
|
+
checkLspReadiness,
|
|
42
|
+
checkOrphanProcesses,
|
|
36
43
|
DEFAULT_CHECKUP_THRESHOLDS,
|
|
37
44
|
} from "./runtime-checkup.js";
|
|
38
45
|
|
|
@@ -604,16 +611,84 @@ async function worktreeSection(opts, deps) {
|
|
|
604
611
|
|
|
605
612
|
// ── runtime checkup: agenda expiry + verbose instruction files ──────────────
|
|
606
613
|
// Only the gaps the OTHER sections don't already cover. Stale sessions,
|
|
607
|
-
// worktrees
|
|
608
|
-
// transcriptSection / worktreeSection / backgroundSection — surfacing
|
|
609
|
-
// too would double-report
|
|
610
|
-
//
|
|
614
|
+
// worktrees and lost background agents (worker STATUS) are handled by
|
|
615
|
+
// transcriptSection / worktreeSection / backgroundSection — surfacing those
|
|
616
|
+
// here too would double-report. This section covers the agenda schedule store,
|
|
617
|
+
// instruction files (cc.md / AGENTS.md / CLAUDE.md), and — distinct from
|
|
618
|
+
// backgroundSection's worker-status check — leaked agent CHILD subprocesses
|
|
619
|
+
// (a per-turn agentPid still alive after its worker died), probed via a static
|
|
620
|
+
// one-shot PID-liveness check and the shared checkOrphanProcesses evaluator.
|
|
611
621
|
const SEVERITY_TO_LEVEL = {
|
|
612
622
|
error: CHECK_LEVELS.ERR,
|
|
613
623
|
warn: CHECK_LEVELS.WARN,
|
|
614
624
|
info: CHECK_LEVELS.INFO,
|
|
615
625
|
};
|
|
616
626
|
|
|
627
|
+
// Heavy / generated directories a project-language scan must never descend into
|
|
628
|
+
// (they'd dominate the budget and aren't the user's source).
|
|
629
|
+
const LSP_SCAN_SKIP_DIRS = new Set([
|
|
630
|
+
"node_modules",
|
|
631
|
+
"dist",
|
|
632
|
+
"build",
|
|
633
|
+
"out",
|
|
634
|
+
"target",
|
|
635
|
+
"coverage",
|
|
636
|
+
"vendor",
|
|
637
|
+
"venv",
|
|
638
|
+
"__pycache__",
|
|
639
|
+
]);
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Bounded shallow walk of a project root, counting source files per LSP
|
|
643
|
+
* languageId (via `languageIdForFile`). Deliberately capped by total entries
|
|
644
|
+
* visited AND depth so `cc doctor` stays fast on a huge monorepo; heavy build /
|
|
645
|
+
* vendor dirs and dot-dirs are skipped. fs access goes through injected `deps`
|
|
646
|
+
* (readdirSync/statSync) so it's unit-testable. Returns a Map languageId→count.
|
|
647
|
+
*/
|
|
648
|
+
function scanProjectLanguages(
|
|
649
|
+
root,
|
|
650
|
+
deps,
|
|
651
|
+
{ maxEntries = 20000, maxDepth = 8 } = {},
|
|
652
|
+
) {
|
|
653
|
+
const counts = new Map();
|
|
654
|
+
let visited = 0;
|
|
655
|
+
const stack = [{ dir: root, depth: 0 }];
|
|
656
|
+
while (stack.length && visited < maxEntries) {
|
|
657
|
+
const { dir, depth } = stack.pop();
|
|
658
|
+
let names;
|
|
659
|
+
try {
|
|
660
|
+
names = deps.readdirSync(dir);
|
|
661
|
+
} catch {
|
|
662
|
+
continue; // unreadable dir — skip, never crash the doctor
|
|
663
|
+
}
|
|
664
|
+
for (const name of names) {
|
|
665
|
+
if (visited >= maxEntries) break;
|
|
666
|
+
visited += 1;
|
|
667
|
+
const lang = languageIdForFile(name);
|
|
668
|
+
if (lang) {
|
|
669
|
+
counts.set(lang, (counts.get(lang) || 0) + 1);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
// Not a source file — maybe a directory to descend (bounded). Skip heavy
|
|
673
|
+
// build/vendor trees and dot-dirs (rarely hold the user's own source).
|
|
674
|
+
if (
|
|
675
|
+
depth < maxDepth &&
|
|
676
|
+
!LSP_SCAN_SKIP_DIRS.has(name) &&
|
|
677
|
+
!String(name).startsWith(".")
|
|
678
|
+
) {
|
|
679
|
+
let isDir = false;
|
|
680
|
+
try {
|
|
681
|
+
isDir = deps.statSync(join(dir, name)).isDirectory();
|
|
682
|
+
} catch {
|
|
683
|
+
isDir = false;
|
|
684
|
+
}
|
|
685
|
+
if (isDir) stack.push({ dir: join(dir, name), depth: depth + 1 });
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return counts;
|
|
690
|
+
}
|
|
691
|
+
|
|
617
692
|
async function runtimeSection(opts, deps) {
|
|
618
693
|
const checks = [];
|
|
619
694
|
const now = deps.now();
|
|
@@ -789,6 +864,111 @@ async function runtimeSection(opts, deps) {
|
|
|
789
864
|
checks.push(failedCheck("sandbox", "sandbox availability", err));
|
|
790
865
|
}
|
|
791
866
|
|
|
867
|
+
// LSP readiness — languages the project actually contains but for which no
|
|
868
|
+
// language server is installed: the silent reason `code_intelligence` /
|
|
869
|
+
// `cc code-intel` degrades to plain text search. A STATIC install-gap check
|
|
870
|
+
// (no server is started); the project scan is bounded so it stays fast.
|
|
871
|
+
try {
|
|
872
|
+
const root = opts.cwd || process.cwd();
|
|
873
|
+
const langCounts = scanProjectLanguages(root, deps);
|
|
874
|
+
if (langCounts.size > 0) {
|
|
875
|
+
const languages = [];
|
|
876
|
+
for (const [languageId, fileCount] of langCounts) {
|
|
877
|
+
const resolved = resolveServer(languageId, root);
|
|
878
|
+
const desc = describeServer(languageId);
|
|
879
|
+
languages.push({
|
|
880
|
+
languageId,
|
|
881
|
+
fileCount,
|
|
882
|
+
available: !!resolved,
|
|
883
|
+
serverId: (resolved && resolved.id) || (desc && desc.id) || null,
|
|
884
|
+
exampleBin: desc && desc.bins ? desc.bins[0] : null,
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
for (const f of checkLspReadiness(languages)) {
|
|
888
|
+
checks.push(
|
|
889
|
+
check(
|
|
890
|
+
f.id,
|
|
891
|
+
`lsp ${f.ref}`,
|
|
892
|
+
SEVERITY_TO_LEVEL[f.severity] || CHECK_LEVELS.INFO,
|
|
893
|
+
`${f.message} — ${f.remediation}`,
|
|
894
|
+
),
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
} catch (err) {
|
|
899
|
+
checks.push(failedCheck("lsp-readiness", "lsp server readiness", err));
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Orphaned agent subprocesses — a background worker died but its per-turn
|
|
903
|
+
// agent CHILD (the LLM/tool subprocess recorded as `agentPid`) is still
|
|
904
|
+
// alive: a leaked process nothing reaped. backgroundSection reports the
|
|
905
|
+
// worker's STATUS; this probes the actual child PID's liveness (static,
|
|
906
|
+
// one-shot, anchored on the process start time to defeat PID reuse) and
|
|
907
|
+
// feeds the shared checkOrphanProcesses evaluator. Only PIDs we recorded are
|
|
908
|
+
// probed → no false positives, no OS-wide process-table scan. Tests inject
|
|
909
|
+
// opts.backgroundProcesses / opts.activeSessionIds (mirrors
|
|
910
|
+
// opts.skillLayerEntries below).
|
|
911
|
+
try {
|
|
912
|
+
let processes = opts.backgroundProcesses;
|
|
913
|
+
let activeSessionIds = opts.activeSessionIds;
|
|
914
|
+
if (!processes) {
|
|
915
|
+
const bgMod = await import("./background-agent-supervisor.js");
|
|
916
|
+
const agents = bgMod.listBackgroundAgents({ all: true }) || [];
|
|
917
|
+
activeSessionIds = agents
|
|
918
|
+
.filter((a) => a.status === "running")
|
|
919
|
+
.map((a) => a.sessionId)
|
|
920
|
+
.filter(Boolean);
|
|
921
|
+
processes = [];
|
|
922
|
+
for (const a of agents) {
|
|
923
|
+
let state;
|
|
924
|
+
try {
|
|
925
|
+
state = bgMod.readBackgroundAgentState(a.id);
|
|
926
|
+
} catch {
|
|
927
|
+
continue; // unreadable/raced state file — skip
|
|
928
|
+
}
|
|
929
|
+
const agentPid = Number(state?.agentPid);
|
|
930
|
+
if (!Number.isInteger(agentPid) || agentPid <= 0) continue;
|
|
931
|
+
if (agentPid === Number(state?.pid)) continue; // that's the worker itself
|
|
932
|
+
// Alive AND still the same process we launched (not a reused PID).
|
|
933
|
+
const anchor = state.agentStartedAt || state.startedAt;
|
|
934
|
+
if (!bgMod.isSameProcess(agentPid, anchor)) continue; // dead → nothing to reap
|
|
935
|
+
const parentAlive = bgMod.isSameProcess(
|
|
936
|
+
Number(state.pid),
|
|
937
|
+
state.startedAt,
|
|
938
|
+
);
|
|
939
|
+
processes.push({
|
|
940
|
+
pid: agentPid,
|
|
941
|
+
alive: true,
|
|
942
|
+
parentAlive,
|
|
943
|
+
sessionId: state.sessionId || null,
|
|
944
|
+
kind: "agent-child",
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
for (const f of checkOrphanProcesses(processes, activeSessionIds || [])) {
|
|
949
|
+
const killCmd =
|
|
950
|
+
process.platform === "win32"
|
|
951
|
+
? `taskkill /PID ${f.ref} /T /F`
|
|
952
|
+
: `kill ${f.ref}`;
|
|
953
|
+
checks.push(
|
|
954
|
+
check(
|
|
955
|
+
f.id,
|
|
956
|
+
`process pid=${f.ref}`,
|
|
957
|
+
SEVERITY_TO_LEVEL[f.severity] || CHECK_LEVELS.WARN,
|
|
958
|
+
`${f.message} — ${f.remediation}`,
|
|
959
|
+
{
|
|
960
|
+
id: `reap-orphan-${f.ref}`,
|
|
961
|
+
safe: false,
|
|
962
|
+
description: `terminate leaked agent subprocess pid=${f.ref}`,
|
|
963
|
+
command: killCmd,
|
|
964
|
+
},
|
|
965
|
+
),
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
} catch (err) {
|
|
969
|
+
checks.push(failedCheck("orphan-process", "orphan agent processes", err));
|
|
970
|
+
}
|
|
971
|
+
|
|
792
972
|
// Duplicate skills — an id defined in more than one layer silently shadows
|
|
793
973
|
// (loadAll keeps only the winner), so a user's custom skill can override, or
|
|
794
974
|
// be overridden by, another without any signal. opts.skillLayerEntries lets a
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install-command-policy — a UNIFIED classifier + audit for package-install
|
|
3
|
+
* shell commands across managers (npm / pnpm / yarn / bun / pip / pipx / poetry /
|
|
4
|
+
* conda / brew / winget / choco / apt / dnf / yum / apk / snap / gem / cargo /
|
|
5
|
+
* go / composer / dotnet).
|
|
6
|
+
*
|
|
7
|
+
* The gap (docs/…INCREMENTAL_GAP_ANALYSIS P0 "跨平台沙箱"): "npm、pip、winget、brew
|
|
8
|
+
* 等安装入口需要统一权限类型与审计事件". Today a `run_shell` install command is just
|
|
9
|
+
* another shell command — there is no single place that recognizes "this fetches
|
|
10
|
+
* and runs third-party code" regardless of which tool does it, and no unified
|
|
11
|
+
* audit trail. This module supplies both, as PURE detection (no I/O) plus a
|
|
12
|
+
* best-effort injected-IO audit sink, so it can be reused by the shell approval
|
|
13
|
+
* path today and other entry points (run_code, hooks) later.
|
|
14
|
+
*
|
|
15
|
+
* Design: a per-manager table maps a binary to its install subcommand(s), a
|
|
16
|
+
* global-scope signal, and package extraction. `classifyInstallCommand` splits a
|
|
17
|
+
* compound command into segments (reusing the shell-policy splitter) so
|
|
18
|
+
* `npm i foo && pip install bar` reports BOTH installs. Nothing here changes a
|
|
19
|
+
* permission decision — callers decide whether to raise a risk floor; the
|
|
20
|
+
* classification is advisory + auditable.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import os from "node:os";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
import shellPolicy from "../runtime/coding-agent-shell-policy.cjs";
|
|
27
|
+
|
|
28
|
+
const { splitCommandSegments, tokenizeShellCommand } = shellPolicy;
|
|
29
|
+
|
|
30
|
+
// Leading tokens that wrap the real command and must be stripped before the
|
|
31
|
+
// binary is read: privilege elevation, env-var assignments, and `env`/`command`.
|
|
32
|
+
const WRAPPER_TOKENS = new Set([
|
|
33
|
+
"sudo",
|
|
34
|
+
"doas",
|
|
35
|
+
"env",
|
|
36
|
+
"command",
|
|
37
|
+
"exec",
|
|
38
|
+
"nice",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Per-manager install detection. `sub` = subcommand keywords that mean "install".
|
|
43
|
+
* `system` = the install always affects the whole machine/user (no project
|
|
44
|
+
* scope), so it is treated as global regardless of flags. `globalFlags`/
|
|
45
|
+
* `globalWords` mark a per-tool global install. `packageWord` (dotnet) requires a
|
|
46
|
+
* keyword before the package name.
|
|
47
|
+
*/
|
|
48
|
+
const MANAGERS = [
|
|
49
|
+
{
|
|
50
|
+
name: "npm",
|
|
51
|
+
bin: /^npm$/,
|
|
52
|
+
sub: ["install", "i", "add", "ci"],
|
|
53
|
+
globalFlags: ["-g", "--global"],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "pnpm",
|
|
57
|
+
bin: /^pnpm$/,
|
|
58
|
+
sub: ["install", "i", "add"],
|
|
59
|
+
globalFlags: ["-g", "--global"],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "yarn",
|
|
63
|
+
bin: /^yarn$/,
|
|
64
|
+
sub: ["add", "install"],
|
|
65
|
+
globalWords: ["global"],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "bun",
|
|
69
|
+
bin: /^bun$/,
|
|
70
|
+
sub: ["install", "i", "add"],
|
|
71
|
+
globalFlags: ["-g", "--global"],
|
|
72
|
+
},
|
|
73
|
+
{ name: "pip", bin: /^pip[0-9.]*$/, sub: ["install"], globalFlags: [] },
|
|
74
|
+
{ name: "pipx", bin: /^pipx$/, sub: ["install"], system: true },
|
|
75
|
+
{ name: "poetry", bin: /^poetry$/, sub: ["add"], globalFlags: [] },
|
|
76
|
+
{ name: "conda", bin: /^conda$/, sub: ["install"], globalFlags: [] },
|
|
77
|
+
{ name: "brew", bin: /^brew$/, sub: ["install"], system: true },
|
|
78
|
+
{ name: "winget", bin: /^winget$/, sub: ["install"], system: true },
|
|
79
|
+
{ name: "choco", bin: /^choco(latey)?$/, sub: ["install"], system: true },
|
|
80
|
+
{ name: "apt", bin: /^apt(-get)?$/, sub: ["install"], system: true },
|
|
81
|
+
{ name: "dnf", bin: /^(dnf|yum)$/, sub: ["install"], system: true },
|
|
82
|
+
{ name: "apk", bin: /^apk$/, sub: ["add"], system: true },
|
|
83
|
+
{ name: "snap", bin: /^snap$/, sub: ["install"], system: true },
|
|
84
|
+
{ name: "gem", bin: /^gem$/, sub: ["install"], system: true },
|
|
85
|
+
{ name: "cargo", bin: /^cargo$/, sub: ["install"], system: true },
|
|
86
|
+
{ name: "go", bin: /^go$/, sub: ["install", "get"], system: true },
|
|
87
|
+
{
|
|
88
|
+
name: "composer",
|
|
89
|
+
bin: /^composer$/,
|
|
90
|
+
sub: ["require", "install"],
|
|
91
|
+
globalWords: ["global"],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "dotnet",
|
|
95
|
+
bin: /^dotnet$/,
|
|
96
|
+
sub: ["add"],
|
|
97
|
+
packageWord: "package",
|
|
98
|
+
system: false,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
/** Basename of a binary path, minus a Windows .exe/.cmd/.bat/.ps1 suffix. */
|
|
103
|
+
function normalizeBin(token) {
|
|
104
|
+
const base = String(token || "")
|
|
105
|
+
.replace(/\\/g, "/")
|
|
106
|
+
.split("/")
|
|
107
|
+
.pop();
|
|
108
|
+
return base.replace(/\.(exe|cmd|bat|ps1)$/i, "").toLowerCase();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Strip leading wrapper tokens (sudo/env/VAR=val) → the real argv, or null. */
|
|
112
|
+
function unwrapArgv(tokens) {
|
|
113
|
+
let i = 0;
|
|
114
|
+
while (i < tokens.length) {
|
|
115
|
+
const t = tokens[i];
|
|
116
|
+
if (WRAPPER_TOKENS.has(t) || /^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) {
|
|
117
|
+
i += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
return tokens.slice(i);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Classify ONE already-split command segment. Returns an install descriptor or
|
|
127
|
+
* null. Pure.
|
|
128
|
+
* @returns {{manager:string, subcommand:string, packages:string[], global:boolean}|null}
|
|
129
|
+
*/
|
|
130
|
+
export function classifyInstallSegment(segment) {
|
|
131
|
+
const argv = unwrapArgv(tokenizeShellCommand(segment));
|
|
132
|
+
if (argv.length === 0) return null;
|
|
133
|
+
const bin = normalizeBin(argv[0]);
|
|
134
|
+
const mgr = MANAGERS.find((m) => m.bin.test(bin));
|
|
135
|
+
if (!mgr) return null;
|
|
136
|
+
|
|
137
|
+
const rest = argv.slice(1);
|
|
138
|
+
// Find the install subcommand token (first non-flag token that matches). For
|
|
139
|
+
// dotnet, the trigger is `add … package`, so also require the package word.
|
|
140
|
+
let subIdx = -1;
|
|
141
|
+
let subcommand = null;
|
|
142
|
+
for (let i = 0; i < rest.length; i++) {
|
|
143
|
+
const tok = rest[i];
|
|
144
|
+
if (mgr.sub.includes(tok)) {
|
|
145
|
+
subIdx = i;
|
|
146
|
+
subcommand = tok;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (subIdx === -1) return null;
|
|
151
|
+
if (mgr.packageWord && !rest.slice(subIdx + 1).includes(mgr.packageWord)) {
|
|
152
|
+
return null; // e.g. `dotnet add reference` is not a package install
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Everything after the subcommand (and, for dotnet, after `package`) that is
|
|
156
|
+
// not a flag = the requested packages (best-effort; enough for audit).
|
|
157
|
+
let argsAfter = rest.slice(subIdx + 1);
|
|
158
|
+
if (mgr.packageWord) {
|
|
159
|
+
const pw = argsAfter.indexOf(mgr.packageWord);
|
|
160
|
+
argsAfter = pw >= 0 ? argsAfter.slice(pw + 1) : argsAfter;
|
|
161
|
+
}
|
|
162
|
+
const packages = argsAfter.filter((t) => !t.startsWith("-"));
|
|
163
|
+
|
|
164
|
+
const global =
|
|
165
|
+
mgr.system === true ||
|
|
166
|
+
(mgr.globalFlags || []).some((f) => rest.includes(f)) ||
|
|
167
|
+
(mgr.globalWords || []).some((w) => rest.includes(w));
|
|
168
|
+
|
|
169
|
+
return { manager: mgr.name, subcommand, packages, global };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Classify a full (possibly compound) shell command. Splits on `&&`/`||`/`;`/`|`
|
|
174
|
+
* so `npm i a && pip install b` reports BOTH. Returns `{ isInstall, installs }`.
|
|
175
|
+
* Pure.
|
|
176
|
+
*/
|
|
177
|
+
export function classifyInstallCommand(command) {
|
|
178
|
+
const segments = splitCommandSegments(String(command || ""));
|
|
179
|
+
const installs = [];
|
|
180
|
+
for (const seg of segments) {
|
|
181
|
+
const hit = classifyInstallSegment(seg);
|
|
182
|
+
if (hit) installs.push(hit);
|
|
183
|
+
}
|
|
184
|
+
return { isInstall: installs.length > 0, installs };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** True if any classified install is global/system-scoped (higher blast radius). */
|
|
188
|
+
export function hasGlobalInstall(classification) {
|
|
189
|
+
return !!(
|
|
190
|
+
classification &&
|
|
191
|
+
Array.isArray(classification.installs) &&
|
|
192
|
+
classification.installs.some((i) => i.global)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── remote-script execution ("curl … | sh") ────────────────────────────────
|
|
197
|
+
// The most dangerous "fetch and run third-party code" vector isn't a package
|
|
198
|
+
// manager at all — it's piping a download straight into a shell, or running a
|
|
199
|
+
// shell over a command/process substitution that downloads. It runs UNPINNED,
|
|
200
|
+
// UNVERIFIED, attacker-mutable code with the agent's privileges. These patterns
|
|
201
|
+
// must be recognized on the RAW command (the pipe/substitution structure is lost
|
|
202
|
+
// once split into segments), so they use anchored regexes rather than the
|
|
203
|
+
// per-segment classifier.
|
|
204
|
+
const REMOTE_FETCHERS = "curl|wget|fetch|iwr|invoke-webrequest|http|httpie";
|
|
205
|
+
const SHELL_INTERPRETERS =
|
|
206
|
+
"sh|bash|zsh|ksh|dash|fish|python[0-9.]*|ruby|perl|node|deno|pwsh|powershell|eval|source";
|
|
207
|
+
|
|
208
|
+
// `curl URL | sh`, `wget -O- URL | sudo bash`, `curl URL | tee f | sh`. After
|
|
209
|
+
// the pipe, skip inline wrappers (`sudo `/`env `, space-separated) AND piped
|
|
210
|
+
// pass-throughs (`tee f |`, `cat |`) before the interpreter.
|
|
211
|
+
const PIPE_TO_SHELL_RE = new RegExp(
|
|
212
|
+
`\\b(${REMOTE_FETCHERS})\\b[^|]*\\|\\s*(?:(?:sudo|env|command|nice)\\s+|(?:tee|cat)\\b[^|]*\\|\\s*)*(${SHELL_INTERPRETERS})\\b`,
|
|
213
|
+
"i",
|
|
214
|
+
);
|
|
215
|
+
// `bash -c "$(curl URL)"`, `sh <(curl URL)`, `eval "$(wget -O- URL)"`
|
|
216
|
+
const SUBST_TO_SHELL_RE = new RegExp(
|
|
217
|
+
`\\b(${SHELL_INTERPRETERS})\\b[^\\n]*[$<]\\(\\s*(?:sudo\\s+)?(${REMOTE_FETCHERS})\\b`,
|
|
218
|
+
"i",
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
function firstUrl(s) {
|
|
222
|
+
const m = String(s).match(/\bhttps?:\/\/[^\s'"|)>]+/i);
|
|
223
|
+
return m ? m[0] : null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Detect remote-script execution in a raw shell command: a network fetcher piped
|
|
228
|
+
* into (or command/process-substituted into) a shell interpreter. Returns
|
|
229
|
+
* `{ isRemoteExec, matches:[{pattern, fetcher, interpreter, url}] }`. Pure.
|
|
230
|
+
*/
|
|
231
|
+
export function classifyRemoteExecCommand(command) {
|
|
232
|
+
const s = String(command || "");
|
|
233
|
+
const matches = [];
|
|
234
|
+
const pipe = PIPE_TO_SHELL_RE.exec(s);
|
|
235
|
+
if (pipe) {
|
|
236
|
+
matches.push({
|
|
237
|
+
pattern: "pipe-to-shell",
|
|
238
|
+
fetcher: pipe[1].toLowerCase(),
|
|
239
|
+
interpreter: pipe[2].toLowerCase(),
|
|
240
|
+
url: firstUrl(s),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
const subst = SUBST_TO_SHELL_RE.exec(s);
|
|
244
|
+
if (subst) {
|
|
245
|
+
matches.push({
|
|
246
|
+
pattern: "cmdsubst-to-shell",
|
|
247
|
+
interpreter: subst[1].toLowerCase(),
|
|
248
|
+
fetcher: subst[2].toLowerCase(),
|
|
249
|
+
url: firstUrl(s),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return { isRemoteExec: matches.length > 0, matches };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Combined "untrusted code acquisition" classification: package installs AND
|
|
257
|
+
* remote-script execution. `flagged` is true if EITHER fires — the single signal
|
|
258
|
+
* a caller gates/audits on. Pure.
|
|
259
|
+
*/
|
|
260
|
+
export function classifyCodeAcquisition(command) {
|
|
261
|
+
const install = classifyInstallCommand(command);
|
|
262
|
+
const remote = classifyRemoteExecCommand(command);
|
|
263
|
+
return {
|
|
264
|
+
flagged: install.isInstall || remote.isRemoteExec,
|
|
265
|
+
isInstall: install.isInstall,
|
|
266
|
+
installs: install.installs,
|
|
267
|
+
isRemoteExec: remote.isRemoteExec,
|
|
268
|
+
remoteExec: remote.matches,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Resolve the install-command policy from env/settings (opt-in, so the default
|
|
274
|
+
* shell path is byte-unchanged). `audit` enables the audit trail; `riskFloor`
|
|
275
|
+
* ("low"|"medium"|"high") is the MINIMUM risk an install command is treated as
|
|
276
|
+
* (never lowers an already-higher risk).
|
|
277
|
+
*/
|
|
278
|
+
export function resolveInstallPolicy({
|
|
279
|
+
env = process.env,
|
|
280
|
+
settings = null,
|
|
281
|
+
} = {}) {
|
|
282
|
+
const s = (settings && settings.installPolicy) || {};
|
|
283
|
+
const envAudit = env.CC_INSTALL_AUDIT;
|
|
284
|
+
const audit =
|
|
285
|
+
envAudit === "1" ? true : envAudit === "0" ? false : s.audit === true;
|
|
286
|
+
const riskFloor =
|
|
287
|
+
env.CC_INSTALL_RISK_FLOOR ||
|
|
288
|
+
(typeof s.riskFloor === "string" ? s.riskFloor : null) ||
|
|
289
|
+
null;
|
|
290
|
+
return {
|
|
291
|
+
audit,
|
|
292
|
+
riskFloor: riskFloor ? String(riskFloor).toLowerCase() : null,
|
|
293
|
+
enabled: audit || !!riskFloor,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const RISK_RANK = { low: 0, medium: 1, high: 2 };
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Apply a risk floor: return whichever of `currentRisk` / `floor` is HIGHER
|
|
301
|
+
* (never downgrades). Unknown values pass `currentRisk` through unchanged.
|
|
302
|
+
*/
|
|
303
|
+
export function applyRiskFloor(currentRisk, floor) {
|
|
304
|
+
if (!floor || !(floor in RISK_RANK)) return currentRisk;
|
|
305
|
+
const cur = RISK_RANK[currentRisk];
|
|
306
|
+
if (cur == null) return currentRisk;
|
|
307
|
+
return RISK_RANK[floor] > cur ? floor : currentRisk;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Best-effort append of one install-command audit record. Never throws. */
|
|
311
|
+
export function recordInstallCommandAudit(entry, opts = {}) {
|
|
312
|
+
try {
|
|
313
|
+
const baseDir =
|
|
314
|
+
opts.baseDir ||
|
|
315
|
+
(opts.env || process.env).CC_AUDIT_DIR ||
|
|
316
|
+
path.join(os.homedir(), ".chainlesschain", "audit");
|
|
317
|
+
const _fs = opts.fs || fs;
|
|
318
|
+
_fs.mkdirSync(baseDir, { recursive: true });
|
|
319
|
+
const line = JSON.stringify({
|
|
320
|
+
ts: new Date(opts.now ? opts.now() : Date.now()).toISOString(),
|
|
321
|
+
kind: "install-command",
|
|
322
|
+
...entry,
|
|
323
|
+
});
|
|
324
|
+
_fs.appendFileSync(
|
|
325
|
+
path.join(baseDir, "install-commands.jsonl"),
|
|
326
|
+
line + "\n",
|
|
327
|
+
"utf-8",
|
|
328
|
+
);
|
|
329
|
+
return true;
|
|
330
|
+
} catch {
|
|
331
|
+
return false; // audit is best-effort — never affect the run
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
resolveServer,
|
|
12
12
|
resolveBin,
|
|
13
13
|
probeServers,
|
|
14
|
+
describeServer,
|
|
14
15
|
registerLanguageServer,
|
|
15
16
|
_resetPluginServers,
|
|
16
17
|
_deps,
|
|
@@ -126,3 +127,29 @@ describe("probeServers", () => {
|
|
|
126
127
|
expect(ids).toContain("typescript-language-server");
|
|
127
128
|
});
|
|
128
129
|
});
|
|
130
|
+
|
|
131
|
+
describe("describeServer", () => {
|
|
132
|
+
it("names the builtin server for a language even when not installed", () => {
|
|
133
|
+
existsFor([]); // nothing on PATH
|
|
134
|
+
const ts = describeServer("typescript");
|
|
135
|
+
expect(ts.id).toBe("typescript-language-server");
|
|
136
|
+
expect(ts.bins).toContain("typescript-language-server");
|
|
137
|
+
// Independent of install state — it describes the EXPECTED server.
|
|
138
|
+
expect(describeServer("python").id).toBe("pyright");
|
|
139
|
+
expect(describeServer("go").id).toBe("gopls");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("returns null for an unsupported language", () => {
|
|
143
|
+
expect(describeServer("cobol")).toBe(null);
|
|
144
|
+
expect(describeServer(undefined)).toBe(null);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("prefers a registered plugin server over the builtin", () => {
|
|
148
|
+
registerLanguageServer({
|
|
149
|
+
languageId: "typescript",
|
|
150
|
+
command: "my-ts-lsp",
|
|
151
|
+
id: "plugin-ts",
|
|
152
|
+
});
|
|
153
|
+
expect(describeServer("typescript").id).toBe("plugin-ts");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -232,3 +232,17 @@ export function probeServers(projectRoot) {
|
|
|
232
232
|
export function supportedExtensions() {
|
|
233
233
|
return Object.keys(EXTENSION_LANGUAGE).sort();
|
|
234
234
|
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Describe the server that WOULD serve a languageId, whether or not its binary
|
|
238
|
+
* is installed — the first plugin candidate, else the builtin. Unlike
|
|
239
|
+
* `resolveServer` (which returns null when nothing is on PATH), this names the
|
|
240
|
+
* expected server so a doctor / status view can say *what to install*. Returns
|
|
241
|
+
* `{ id, bins }` or null for an unsupported language.
|
|
242
|
+
*/
|
|
243
|
+
export function describeServer(languageId) {
|
|
244
|
+
const candidates = [...(pluginServers.get(languageId) || [])];
|
|
245
|
+
if (BUILTIN_SERVERS[languageId]) candidates.push(BUILTIN_SERVERS[languageId]);
|
|
246
|
+
const def = candidates[0];
|
|
247
|
+
return def ? { id: def.id, bins: [...def.bins] } : null;
|
|
248
|
+
}
|
|
@@ -437,6 +437,39 @@ export function checkDuplicateSkills(entries = []) {
|
|
|
437
437
|
return out;
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
+
/**
|
|
441
|
+
* LSP READINESS: languages the project actually contains but for which no
|
|
442
|
+
* language server is installed — the silent reason `code_intelligence` /
|
|
443
|
+
* `cc code-intel` quietly degrades to plain text search. Unlike
|
|
444
|
+
* `checkPluginsAndLsp` (live server health) this is a STATIC install-gap check a
|
|
445
|
+
* separate `cc doctor` process can compute without any server running: it
|
|
446
|
+
* correlates source files present with `resolveServer` availability. Only
|
|
447
|
+
* present-but-missing languages warn; installed languages produce nothing (no
|
|
448
|
+
* noise). Each `languages` entry: `{ languageId, fileCount, available, serverId,
|
|
449
|
+
* exampleBin }`.
|
|
450
|
+
*/
|
|
451
|
+
export function checkLspReadiness(languages = []) {
|
|
452
|
+
const out = [];
|
|
453
|
+
for (const l of languages) {
|
|
454
|
+
if (!l || !l.languageId) continue;
|
|
455
|
+
const fileCount = Number(l.fileCount) || 0;
|
|
456
|
+
if (fileCount <= 0 || l.available) continue;
|
|
457
|
+
const server = l.serverId || `a ${l.languageId} language server`;
|
|
458
|
+
const install = l.exampleBin ? ` (e.g. install ${l.exampleBin})` : "";
|
|
459
|
+
out.push(
|
|
460
|
+
finding(
|
|
461
|
+
"lsp-server-missing",
|
|
462
|
+
"lsp",
|
|
463
|
+
"warn",
|
|
464
|
+
`${fileCount} ${l.languageId} file(s) present but no language server installed${install}`,
|
|
465
|
+
`install ${server} for real code-intelligence, or ignore to keep text-search fallback`,
|
|
466
|
+
{ ref: l.languageId, fileCount, serverId: l.serverId || null },
|
|
467
|
+
),
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
return out;
|
|
471
|
+
}
|
|
472
|
+
|
|
440
473
|
/** Plugins / LSP servers reported unhealthy or dead. */
|
|
441
474
|
export function checkPluginsAndLsp(plugins = []) {
|
|
442
475
|
const out = [];
|