chainlesschain 0.162.165 → 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 +3 -1
- package/src/assets/web-panel/assets/{AIOps-DgGZr3PR.js → AIOps-BZJrchIA.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BAOgBIc_.js → ActionButton-Bhlrylt7.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-Ds4GAvNF.js → Analytics-pHtXVQYs.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-CoJvCkoF.js → AppLayout-ORpuri4R.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-6EgUaE1T.js → Artifacts-eC77yr1_.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-BYie_i74.js → Audit-B2dZIiqz.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-D5gxOhjd.js → BackgroundAgents-COZiTiwP.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CIsVK2pT.js → Backup-vY_Srl5d.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-ClorP55M.js → BaseInput-Dili5BdM.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CnhTPGGA.js → Chat-EzKBf0XL.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-Bl1VXgwp.js → ChatBubbleRenderer-D1CtRAyy.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BRavl7TP.js → Checkbox-Beh6BmuW.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BXg_ErNT.js → Codegen-C3fA6kBU.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CfDPzFPq.js → Col-CefMqONH.js} +1 -1
- package/src/assets/web-panel/assets/{Community-Cgo0httP.js → Community-CBAxFJtB.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DU5JcEQM.js → Compact--flHc-sM.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DCH_TVBl.js → Compliance-CPeNLvA6.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-IGb5O2IH.js → Cowork-Bv5yv4BU.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-CPp_Xp9F.js → Cron-C22yMEas.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-Cwrqklzw.js → Crosschain-Dsb2bcpu.js} +1 -1
- package/src/assets/web-panel/assets/{DID-D55sBQwM.js → DID-DAxei4w0.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CedXhGmP.js → Dashboard-DEUwuVZ8.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-Dm6_v3V4.js → Dropdown-BVp4si2j.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CWjnuE3G.js → EmailListRenderer-CyFkybOG.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DubTh7y-.js → FamilyGuardDashboard-BWmmsbVx.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DlSM4uJv.js → Federation-Cxw27A5d.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BYhg0vtG.js → FormItemContext-Q9XthzL1.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-Bkh7qjfK.js → GenericCardRenderer-DS5DIdsW.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DSLWPKvn.js → Git-BOliHfxP.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DIj9O8BB.js → Governance-CD4oxa-W.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Dhep3e5Y.js → Inference-CMlcivo8.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Cj8vzxB_.js → KnowledgeGraph-BwhERcC4.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-Ba4SsPja.js → Logs-BjVcZR5P.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-BfRhXdyG.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-CGs9IIbf.js → Marketplace-DQLU3E2w.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BTPAoX4K.js → McpTools-CeK0XkpB.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-BJ6ISf2c.js → Memory-DLFkgwaz.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-Ca6n3Dcn.js → MobileBridge-Bh6ymXyj.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-lvilHuLt.js → MobileProjects-D950tUIe.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-CMo0acBV.js → Mtc-Bx5x9_2m.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-Hz5wBvFX.js → MtcAudit-a1-Fexsq.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-BK2aKWE6.js → Multisig-eaw0X8X1.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CExpnYMI.js → NLProgramming-BaNsPV4k.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-B7IFbuBo.js → Notes-C7pevN2X.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-VrGchtb0.js → NotificationSettings-HDqeoP0r.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-CyrNTxkU.js → OrderTableRenderer-DvzVl5T3.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-I0C17GBP.js → Organization-D_xHDlGW.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-CXcWvERe.js → Overflow-B5sRnueL.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-FFSCXrZq.js → P2P-C-IRjOPW.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-1PB-p8Zk.js → PdhVaultBrowser-BqgUPlcU.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-C1YHejsb.js → Permissions-Bm_T1uWw.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-CEMPpgf8.js → PersonalDataHub-DujDllye.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-FoIScdLr.js → Pipeline-DyNPOLIG.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DDfbEzEj.js → Privacy-CYKTW3iK.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Dso4kB0O.js → ProjectInit-CGf03uZ0.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-WvDoydDc.js → ProjectSettings-DIec4bTI.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-NJp0kzkd.js → Projects-BY43-bmi.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BuJXnOLI.js → Providers-RrnUmgPk.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-Bb3lMvGG.js → QrScannerModal-BBWfqd4y.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-Ddxra3fK.js → QuickAsk-BsICSrFp.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BW-dR9qw.js → Recommend-B8y0b4ys.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-Hnm7-Qkm.js → RemoteSession-0QgdB1Oe.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-DKz6meHE.js → Reputation-DBJGM0YO.js} +1 -1
- package/src/assets/web-panel/assets/{Row-BuAsoNTS.js → Row-DtWQ5otv.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-_do9DMIH.js → RssFeed-2zBxHd5H.js} +3 -3
- package/src/assets/web-panel/assets/{Search-CMlcw7ND.js → Search-BofliSkZ.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DhAkfSpM.js → Security-ockRBBEL.js} +3 -3
- package/src/assets/web-panel/assets/{Services-5zKLhy46.js → Services-QyvG7Vd0.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DCMZ3wfv.js → Skeleton-DwbOndX3.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DxQgYCC_.js → Skills-cHWa63sZ.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CJmE4cSN.js → Sla-BHnTEydQ.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BgpRTPQp.js → SpeechSettings-DE2hrrLb.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DkogtYbB.js → SyncSettings-CZ9vnbJH.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-DWgoAHvM.js +1 -0
- package/src/assets/web-panel/assets/{Templates-DAZIu5nN.js → Templates-DiR3ad4X.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BRJMI-su.js → Tenant-NqcW3frV.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-iYeqRQZ0.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-D4z9aSGD.js → TimelineRenderer-C5GOoxFz.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-zgYA4UOI.js → Tokens-82dz0XdU.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DMOgHX4m.js → Trigger-B3JCNrOi.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CEtoNwcT.js → Trust-BehFYsN_.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DFx2pQfO.js → UkeySign-BfOxJjVf.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DLssy-7B.js → VideoEditing-DdITCCPz.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DQHc6gTl.js → Wallet-B0EhkCmD.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DgKcODkn.js → WebAuthn-DJ4i1f3-.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-cPEWfMOf.js → WorkflowEditor-Dr9XTqIT.js} +1 -1
- package/src/assets/web-panel/assets/{chat-Coj7rSO1.js → chat-Dc2nFyOj.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DiyGfr-6.js → colors-Drw9djBR.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DB63aMXh.js → compact-item-BdLXbU52.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CENpsYHg.js → createContext-BpQMZiWY.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CnfpDzMr.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-B9NOmk0K.js → hasIn-CbYwawHf.js} +1 -1
- package/src/assets/web-panel/assets/{index-BvQq6gX2.js → index-B1ANzVc6.js} +1 -1
- package/src/assets/web-panel/assets/{index-mTK_ZHhE.js → index-B3uRI8_y.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_rZCxxI.js → index-B5KboakZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DAMKhjtT.js → index-B9_u4Qhe.js} +1 -1
- package/src/assets/web-panel/assets/{index-7IMjHwGU.js → index-BAq-4KqY.js} +1 -1
- package/src/assets/web-panel/assets/{index-DqLC0iE4.js → index-BBueKSTR.js} +1 -1
- package/src/assets/web-panel/assets/{index-Kp2AETEw.js → index-BIcEW07n.js} +1 -1
- package/src/assets/web-panel/assets/{index-KDCuTzdx.js → index-BWUoc9-s.js} +1 -1
- package/src/assets/web-panel/assets/{index-nio98wGC.js → index-BZxzdAI0.js} +1 -1
- package/src/assets/web-panel/assets/{index-BxvNtndl.js → index-BiGDrEt4.js} +1 -1
- package/src/assets/web-panel/assets/{index-B45osqnZ.js → index-Blv7c80V.js} +1 -1
- package/src/assets/web-panel/assets/{index-BpmTl8QC.js → index-BmQlot_u.js} +1 -1
- package/src/assets/web-panel/assets/{index-DANoIqXP.js → index-Bo_KwJTF.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dg0v-CXZ.js → index-Bu8Vy-mj.js} +1 -1
- package/src/assets/web-panel/assets/{index-U_snupDl.js → index-C1eo1Qkd.js} +1 -1
- package/src/assets/web-panel/assets/{index-ClEy1v1o.js → index-C3Alz2QD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bvj07Ab1.js → index-CBwQwtsE.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKNt2vfs.js → index-CJQiSQw7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CXRknr9W.js → index-CKmz3Is-.js} +1 -1
- package/src/assets/web-panel/assets/{index-CRhEJ8JO.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-CxVfbuj9.js → index-CpmlTfpv.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmNCDN5t.js → index-D7Ipfs4F.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0C6zl0y.js → index-DHMeEu-P.js} +1 -1
- package/src/assets/web-panel/assets/{index-DsPDEHOg.js → index-DKQlF7Pw.js} +1 -1
- package/src/assets/web-panel/assets/{index-Yb9EfUnY.js → index-DO7LD_1t.js} +1 -1
- package/src/assets/web-panel/assets/{index-CvTJ-x6b.js → index-DSmZjrFy.js} +1 -1
- package/src/assets/web-panel/assets/{index-C9d8hdgY.js → index-DTeqWdAS.js} +1 -1
- package/src/assets/web-panel/assets/{index-Y_UKQpsV.js → index-DWUN8c01.js} +1 -1
- package/src/assets/web-panel/assets/{index-t6hXF2Cw.js → index-D_lkTnZ0.js} +1 -1
- package/src/assets/web-panel/assets/{index-VDFbduzG.js → index-DyU21xVi.js} +1 -1
- package/src/assets/web-panel/assets/{index-76GMjcbq.js → index-GG_MXjSL.js} +3 -3
- package/src/assets/web-panel/assets/{index-BramLq29.js → index-IG3__pGj.js} +1 -1
- package/src/assets/web-panel/assets/{index-BXzMKVLp.js → index-UX_Mfyfv.js} +1 -1
- package/src/assets/web-panel/assets/{index-CwkK9Kl2.js → index-VIyNlXPX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAXouogg.js → index-XYe6kBmi.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTuX_MU0.js → index-_vVucYqC.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkoqtJzV.js → index-aHrUfrv1.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-GTwJVIOz.js → initDefaultProps-DybC85E4.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BJQLsPx8.js → motion-BNNuyDg7.js} +1 -1
- package/src/assets/web-panel/assets/{move-DVQM0bdG.js → move-Bm8gSjEN.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-CMXI1I2y.js → mtc-parser-CuHQvzzr.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CAc8MDw9.js → omit-lv4NO7Ng.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BgqzoyP7.js → pickAttrs-BoSMfjHq.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-B9ebCdRy.js → placementArrow-BOMDSvgz.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Dy8D0D3n.js → responsiveObserve-C064Ubmu.js} +1 -1
- package/src/assets/web-panel/assets/{slide-Bcrc8J39.js → slide-CeqhbKoG.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CDW6FOlc.js → statusUtils-CunFoYww.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CWhJrDON.js → styleChecker-Cxz3wPL7.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-C82c33XI.js → useFlexGapSupport-DJsIaU2S.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-A6m--mih.js → useFs-DnAygceJ.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-m-1laJY5.js → usePersonalDataHub-CrEUY7ta.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CL9oKKRa.js → vnode-Cq3rN2eT.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DrOyaof3.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 +12 -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/background-session-transport.js +24 -2
- package/src/lib/doctor-checkup.js +184 -4
- package/src/lib/governance-coverage.js +179 -0
- 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/packer/smoke-runner.js +85 -15
- package/src/lib/protocol-replay.js +153 -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/agent-core.js +35 -0
- 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-DUjyuVB3.js +0 -1
- package/src/assets/web-panel/assets/Tasks-D5OrwAl9.js +0 -1
- package/src/assets/web-panel/assets/Terminal-CMXLqQKF.js +0 -3
- package/src/assets/web-panel/assets/devWarning-XvRaIiyt.js +0 -1
- package/src/assets/web-panel/assets/index-CbO1zSXV.js +0 -1
- package/src/assets/web-panel/assets/index-aCu2gbUb.js +0 -1
|
@@ -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
|
+
}
|
|
@@ -85,6 +85,15 @@ export async function smokeTestExe(ctx) {
|
|
|
85
85
|
stdio: ["ignore", "pipe", "pipe"],
|
|
86
86
|
windowsHide: true,
|
|
87
87
|
shell: needsShell,
|
|
88
|
+
// POSIX: own process group so teardown can signal the WHOLE tree via the
|
|
89
|
+
// negative pid. Critical when shell:true wraps the launch in `/bin/sh -c`
|
|
90
|
+
// (the `.sh` test shim, and any real exe that re-execs): a plain
|
|
91
|
+
// child.kill() then hits only the wrapper shell and orphans the real
|
|
92
|
+
// `node` server grandchild (ppid→1), which kept serving AND — via the
|
|
93
|
+
// stdio pipe fds it inherited from the worker — pinned the vitest forks
|
|
94
|
+
// pool past its terminate deadline ("Worker exited unexpectedly" flake).
|
|
95
|
+
// No-op on Windows, where killChild uses taskkill /T to walk the tree.
|
|
96
|
+
detached: process.platform !== "win32",
|
|
88
97
|
});
|
|
89
98
|
|
|
90
99
|
let stdoutBuf = "";
|
|
@@ -104,9 +113,26 @@ export async function smokeTestExe(ctx) {
|
|
|
104
113
|
// is enough in practice, but /T protects us if that ever changes.
|
|
105
114
|
spawn("taskkill", ["/F", "/T", "/PID", String(child.pid)], {
|
|
106
115
|
stdio: "ignore",
|
|
107
|
-
});
|
|
116
|
+
}).unref();
|
|
117
|
+
} else if (child.pid) {
|
|
118
|
+
// Negative pid → the whole process group from the detached spawn above,
|
|
119
|
+
// so a shell:true wrapper's exec'd/forked `node` grandchild dies too
|
|
120
|
+
// (a plain child.kill would signal only the wrapper and orphan the
|
|
121
|
+
// server). SIGKILL, not SIGTERM: a smoke teardown has no reason to wait
|
|
122
|
+
// for a graceful close and not every packed/fake server installs a
|
|
123
|
+
// SIGTERM handler. Fall back to a direct kill if the group is already
|
|
124
|
+
// gone (ESRCH).
|
|
125
|
+
try {
|
|
126
|
+
process.kill(-child.pid, "SIGKILL");
|
|
127
|
+
} catch {
|
|
128
|
+
try {
|
|
129
|
+
child.kill("SIGKILL");
|
|
130
|
+
} catch {
|
|
131
|
+
/* already gone */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
108
134
|
} else {
|
|
109
|
-
child.kill("
|
|
135
|
+
child.kill("SIGKILL");
|
|
110
136
|
}
|
|
111
137
|
} catch {
|
|
112
138
|
/* best effort */
|
|
@@ -115,9 +141,10 @@ export async function smokeTestExe(ctx) {
|
|
|
115
141
|
|
|
116
142
|
// Early-death watchdog: if the child exits before we see a listening
|
|
117
143
|
// port, report the stdout/stderr so the user sees the real failure
|
|
118
|
-
// instead of a generic timeout.
|
|
144
|
+
// instead of a generic timeout. Named so cleanup() can detach it.
|
|
145
|
+
let onExit;
|
|
119
146
|
const deathPromise = new Promise((_, reject) => {
|
|
120
|
-
|
|
147
|
+
onExit = (code, signal) => {
|
|
121
148
|
reject(
|
|
122
149
|
new PackError(
|
|
123
150
|
`Smoke-test process exited (code=${code}, signal=${signal}) before ports came up.\n` +
|
|
@@ -126,16 +153,52 @@ export async function smokeTestExe(ctx) {
|
|
|
126
153
|
EXIT.SMOKE,
|
|
127
154
|
),
|
|
128
155
|
);
|
|
129
|
-
}
|
|
156
|
+
};
|
|
157
|
+
child.on("exit", onExit);
|
|
130
158
|
});
|
|
131
159
|
|
|
160
|
+
// Full teardown: killing the child is not enough — the parent still holds
|
|
161
|
+
// the child's stdout/stderr PIPE handles and the ChildProcess object, all of
|
|
162
|
+
// which keep the event loop alive (killed-but-unreaped on Windows, or
|
|
163
|
+
// still-open pipes on POSIX). Left dangling across the ~8 smokeTestExe calls
|
|
164
|
+
// this test makes, they pinned the vitest forks-pool worker so it could not
|
|
165
|
+
// terminate → the "Worker exited unexpectedly" / "Timeout terminating forks
|
|
166
|
+
// worker" flake blamed on whatever file ran next on the reused worker. Detach
|
|
167
|
+
// the exit watcher, destroy the pipes, kill, and unref so nothing survives.
|
|
168
|
+
let cleaned = false;
|
|
169
|
+
const cleanup = () => {
|
|
170
|
+
if (cleaned) return;
|
|
171
|
+
cleaned = true;
|
|
172
|
+
try {
|
|
173
|
+
child.removeListener("exit", onExit);
|
|
174
|
+
} catch {
|
|
175
|
+
/* noop */
|
|
176
|
+
}
|
|
177
|
+
killChild();
|
|
178
|
+
try {
|
|
179
|
+
child.stdout?.destroy();
|
|
180
|
+
} catch {
|
|
181
|
+
/* noop */
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
child.stderr?.destroy();
|
|
185
|
+
} catch {
|
|
186
|
+
/* noop */
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
child.unref();
|
|
190
|
+
} catch {
|
|
191
|
+
/* noop */
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
132
195
|
try {
|
|
133
196
|
await Promise.race([
|
|
134
197
|
waitForPorts({ uiPort, wsPort, timeoutMs: bootTimeoutMs }),
|
|
135
198
|
deathPromise,
|
|
136
199
|
]);
|
|
137
200
|
} catch (e) {
|
|
138
|
-
|
|
201
|
+
cleanup();
|
|
139
202
|
throw e;
|
|
140
203
|
}
|
|
141
204
|
|
|
@@ -148,7 +211,7 @@ export async function smokeTestExe(ctx) {
|
|
|
148
211
|
timeoutMs: probeTimeoutMs,
|
|
149
212
|
});
|
|
150
213
|
} catch (e) {
|
|
151
|
-
|
|
214
|
+
cleanup();
|
|
152
215
|
throw new PackError(
|
|
153
216
|
`Smoke-test HTTP probe failed: ${e.message}`,
|
|
154
217
|
EXIT.SMOKE,
|
|
@@ -156,7 +219,7 @@ export async function smokeTestExe(ctx) {
|
|
|
156
219
|
}
|
|
157
220
|
|
|
158
221
|
if (uiStatus < 200 || uiStatus >= 300) {
|
|
159
|
-
|
|
222
|
+
cleanup();
|
|
160
223
|
throw new PackError(
|
|
161
224
|
`Smoke-test HTTP probe: http://127.0.0.1:${uiPort}/ returned ${uiStatus} (expected 2xx)`,
|
|
162
225
|
EXIT.SMOKE,
|
|
@@ -191,7 +254,7 @@ export async function smokeTestExe(ctx) {
|
|
|
191
254
|
` skills check: /api/skills returned 404 — skipping (older pack predating Phase 2b)`,
|
|
192
255
|
);
|
|
193
256
|
skillsCheck = { ok: true, skipped: "endpoint-404" };
|
|
194
|
-
|
|
257
|
+
cleanup();
|
|
195
258
|
return {
|
|
196
259
|
ok: true,
|
|
197
260
|
uiStatus,
|
|
@@ -200,7 +263,7 @@ export async function smokeTestExe(ctx) {
|
|
|
200
263
|
skillsCheck,
|
|
201
264
|
};
|
|
202
265
|
}
|
|
203
|
-
|
|
266
|
+
cleanup();
|
|
204
267
|
throw new PackError(
|
|
205
268
|
`Smoke-test /api/skills probe failed: ${e.message}`,
|
|
206
269
|
EXIT.SMOKE,
|
|
@@ -216,7 +279,7 @@ export async function smokeTestExe(ctx) {
|
|
|
216
279
|
);
|
|
217
280
|
const missing = bundledSkillNames.filter((n) => !registeredNames.has(n));
|
|
218
281
|
if (missing.length > 0) {
|
|
219
|
-
|
|
282
|
+
cleanup();
|
|
220
283
|
throw new PackError(
|
|
221
284
|
`Smoke-test skills check: bundled skill(s) not registered: ${missing.join(", ")}`,
|
|
222
285
|
EXIT.SMOKE,
|
|
@@ -228,7 +291,7 @@ export async function smokeTestExe(ctx) {
|
|
|
228
291
|
skillsCheck = { ok: true, checked: bundledSkillNames.length };
|
|
229
292
|
}
|
|
230
293
|
|
|
231
|
-
|
|
294
|
+
cleanup();
|
|
232
295
|
|
|
233
296
|
return {
|
|
234
297
|
ok: true,
|
|
@@ -280,12 +343,17 @@ function tryConnect(host, port, timeoutMs) {
|
|
|
280
343
|
function probeHttp({ host, port, path: urlPath, timeoutMs }) {
|
|
281
344
|
return new Promise((resolve, reject) => {
|
|
282
345
|
const req = http.get(
|
|
283
|
-
|
|
346
|
+
// agent:false → a one-off connection that closes after the response
|
|
347
|
+
// instead of lingering in the global keep-alive pool (leak-free probe).
|
|
348
|
+
{ host, port, path: urlPath, timeout: timeoutMs, agent: false },
|
|
284
349
|
(res) => {
|
|
285
350
|
const status = res.statusCode || 0;
|
|
286
351
|
// Drain body so the socket can close cleanly.
|
|
287
352
|
res.on("data", () => {});
|
|
288
|
-
res.on("end", () =>
|
|
353
|
+
res.on("end", () => {
|
|
354
|
+
req.destroy(); // release the socket now, don't wait for GC
|
|
355
|
+
resolve(status);
|
|
356
|
+
});
|
|
289
357
|
res.on("error", reject);
|
|
290
358
|
},
|
|
291
359
|
);
|
|
@@ -299,10 +367,11 @@ function probeHttp({ host, port, path: urlPath, timeoutMs }) {
|
|
|
299
367
|
function probeHttpJson({ host, port, path: urlPath, timeoutMs }) {
|
|
300
368
|
return new Promise((resolve, reject) => {
|
|
301
369
|
const req = http.get(
|
|
302
|
-
{ host, port, path: urlPath, timeout: timeoutMs },
|
|
370
|
+
{ host, port, path: urlPath, timeout: timeoutMs, agent: false },
|
|
303
371
|
(res) => {
|
|
304
372
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
305
373
|
res.resume();
|
|
374
|
+
req.destroy();
|
|
306
375
|
reject(new Error(`HTTP ${res.statusCode} from ${urlPath}`));
|
|
307
376
|
return;
|
|
308
377
|
}
|
|
@@ -311,6 +380,7 @@ function probeHttpJson({ host, port, path: urlPath, timeoutMs }) {
|
|
|
311
380
|
body += d.toString("utf8");
|
|
312
381
|
});
|
|
313
382
|
res.on("end", () => {
|
|
383
|
+
req.destroy(); // release the socket now, don't wait for GC
|
|
314
384
|
try {
|
|
315
385
|
resolve(JSON.parse(body));
|
|
316
386
|
} catch (e) {
|