chainlesschain 0.162.150 → 0.162.152
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/README.md +1 -1
- package/package.json +3 -2
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-CBLvrjeG.js → AIOps-CVLVJS-Q.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DYv2GtE0.js → ActionButton-CJ2_GJsI.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CL6PZ-Ww.js → Analytics-CZsmlTF8.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-xuyHt4uA.js → AppLayout-DREvMZ62.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-DOvSVycq.js → Audit-B4NR42w_.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DMhfTswr.js → Backup-CJFmf3In.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CQxDdm63.js → BaseInput-Bh8_iUZY.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BEeNhK2a.js → Chat-DEwKjIxV.js} +5 -5
- package/src/assets/web-panel/assets/ChatBubbleRenderer-BOqHkxK_.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-CL04O-ER.js → Checkbox-DYWCqYI9.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-C50o-n7x.js → Codegen-BlXrfZAJ.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CH-bDkzs.js → Col-DLPM22eX.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DqAIMvSe.js → Community-PFyzqijs.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-C43fo_my.js → Compact-7DIfY5Wp.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BERnMrdP.js → Compliance-DF4CinPU.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Baw3w1gp.js → Cowork-B1J80Vcj.js} +4 -4
- package/src/assets/web-panel/assets/{Cron-BEqAHF1-.js → Cron-B7sx2OXX.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CvnmKwK4.js → Crosschain-omLkxmu-.js} +1 -1
- package/src/assets/web-panel/assets/{DID-iMPxgVdt.js → DID-B_7r7b5F.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard--4SJX3YR.js → Dashboard-Cp7k3GZg.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BgLV6xgz.js → Dropdown-BCKO89Y8.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Bvb8oUD8.js → EmailListRenderer-CNgTSsFZ.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DpXPjJB5.js → FamilyGuardDashboard-DobM83JP.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BbFfJ1Xi.js → Federation-XVAYQIeu.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DeGq1B-q.js → FormItemContext-DV5ka1KQ.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CNW7s9zM.js → GenericCardRenderer-Dg5q-KpC.js} +1 -1
- package/src/assets/web-panel/assets/{Git-L1PjW-lT.js → Git-Dsn3Xhxf.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-PK-X58to.js → Governance-D06S4Qow.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BePWEH-d.js → Inference-nAvwxUXy.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-LF_sBvdL.js → KnowledgeGraph-CtqRP_rF.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BKkfk9hy.js → Logs-J5aGAyfY.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-BNMqUDla.js → Marketplace-CaeY23Kj.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-w4WPiyz2.js → McpTools-C2HHnwnE.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-DUKMVGNi.js → Memory-CWBrdKlR.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BWhiEyTJ.js → MobileBridge-CBLQ8uCY.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-Ocf5JEkX.js → MobileProjects-Q_Cy_W4_.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-D2B6MMhU.js → Mtc-BXur-euo.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-DdYjCq1O.js → MtcAudit-CpQSlOQC.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-Cb1hfuUZ.js → Multisig-DlFwdEOA.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CTeImGp7.js → NLProgramming-DSh7fevc.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BjYNc7eQ.js → Notes-Bv4NOVxg.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-Dn_NtRXQ.js → NotificationSettings-CzHNnIfa.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DVYYIizh.js → OrderTableRenderer-D7pHfKK4.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-Be2Kmr5j.js → Organization-COzV01ed.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Cu_-qRlR.js → Overflow-CYRZJb2_.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DD2HoGzE.js → P2P-DFm49FE0.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BRI7DVth.js → PdhVaultBrowser-Ctr-6k5B.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-C9Srcs2M.js → Permissions-DXVrhFyv.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-CUqcMgo1.js → PersonalDataHub-BO5utpLK.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-HBrQGLXt.js → Pipeline-C9irYCg7.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-D-B0vjwh.js → Privacy-r13hrvoF.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-eTeLIu9e.js → ProjectInit-PRKNPZZC.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DrRqCsC6.js → ProjectSettings-QvxM1ZDO.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-1G9DNOhI.js → Projects-DfgmsN3j.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-RkTZzpxP.js → Providers-DxbrccZ7.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BSCUgsgC.js → QrScannerModal-BMHVRxBX.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CHgyM3Bz.js → QuickAsk-Co3_ndAH.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-0lry4OZq.js → Recommend-Cv3UWR6s.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-Cn45UroZ.js → RemoteSession-CSDsO2zA.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-Q-Zjb707.js → Reputation-Bdj59vzM.js} +1 -1
- package/src/assets/web-panel/assets/{Row-BdM70oda.js → Row-CejfUjmo.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-DmOAKKap.js → RssFeed-C83yKkvU.js} +2 -2
- package/src/assets/web-panel/assets/{Search-Dwavbm07.js → Search-Ck4l_VI3.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DukXFCnk.js → Security-DGIIEx6K.js} +3 -3
- package/src/assets/web-panel/assets/{Services-BIlRnITu.js → Services-Cj2aNAy3.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-YYVQdhhQ.js → Skeleton-Dlyy8akb.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-B8_iYHz2.js → Skills-C4aDa9Zh.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-BdVrhT31.js → Sla-tHd_Bauo.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-CD3ggRx7.js → SpeechSettings-DI7R0Qgd.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-Bp02VZFH.js → SyncSettings-OOxiPdsE.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-DEVmCEsr.js → Tasks-Nro_Ltg5.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-B6czWc4N.js → Templates-Bm7eSMWH.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BCJLv17r.js → Tenant-B8F8eFvD.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-CD132d2Y.js → Terminal-2oWhR7QF.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-ZB-CvkZl.js → TimelineRenderer-GLnecmpd.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BnSY0S-s.js → Tokens-CtpFyati.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-VZw9Oy2w.js → Trigger-DGkXZzbN.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-pZq7Hjd2.js → Trust-ClYZMkXN.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DnQaqk4q.js → UkeySign-C1AuEW5-.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BEUfmvJV.js → VideoEditing-DHa9ekSK.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CoZKMXJ2.js → Wallet-CyQv8ZAN.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CBOGVx1I.js → WebAuthn-DPxEEb5Y.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-huSKn7TT.js → WorkflowEditor-C-fCM8Si.js} +1 -1
- package/src/assets/web-panel/assets/{chat-A_3w6FBO.js → chat-BMu1Pk8B.js} +1 -1
- package/src/assets/web-panel/assets/{colors-D1Bem9Oy.js → colors-DYPOXyWQ.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CJYJj0oO.js → compact-item-C5r_plPk.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BkbFiKT4.js → createContext-9fWrWmuJ.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DmRz5Pet.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CKvUafVY.js → hasIn-CjZM2-qE.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHdailyo.js → index-0DVMoDzF.js} +1 -1
- package/src/assets/web-panel/assets/{index-C-WIY-FQ.js → index-B1iUe3Ba.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOoAQjJz.js → index-B56CI20h.js} +1 -1
- package/src/assets/web-panel/assets/{index-AjrSPnvv.js → index-BBVCXmJV.js} +1 -1
- package/src/assets/web-panel/assets/{index-CD62EEGo.js → index-BPucc_ES.js} +1 -1
- package/src/assets/web-panel/assets/{index-tgO8iza6.js → index-BVFtY8Y4.js} +1 -1
- package/src/assets/web-panel/assets/{index-F--HuPG1.js → index-BW-x3mUK.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTs_D1OL.js → index-B_bTdeou.js} +1 -1
- package/src/assets/web-panel/assets/{index-BiDWxzbn.js → index-Bglw_kmu.js} +1 -1
- package/src/assets/web-panel/assets/{index-CBaosWRO.js → index-BsHmE__1.js} +1 -1
- package/src/assets/web-panel/assets/{index-B8i0lViZ.js → index-Bti4lpw5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BJwlEnYo.js → index-ByVDaARk.js} +1 -1
- package/src/assets/web-panel/assets/{index-CVS8Ymuq.js → index-C3E7xVl1.js} +1 -1
- package/src/assets/web-panel/assets/{index-CBlBVJ_m.js → index-CDaA4A0b.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfgCdFrN.js → index-CGqrVLWe.js} +27 -27
- package/src/assets/web-panel/assets/{index-Crp8CbRg.js → index-CJJx0kbR.js} +1 -1
- package/src/assets/web-panel/assets/{index-JEAGMii9.js → index-CTnaRErh.js} +1 -1
- package/src/assets/web-panel/assets/{index-Co3OFL0t.js → index-CYoS2cwK.js} +1 -1
- package/src/assets/web-panel/assets/{index-b-sbVdjQ.js → index-CcDGi9U1.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjYrecNb.js → index-Cd9_KCPs.js} +1 -1
- package/src/assets/web-panel/assets/{index-PF-mpv8K.js → index-Co9kBFxY.js} +1 -1
- package/src/assets/web-panel/assets/index-CrsL7YBW.js +1 -0
- package/src/assets/web-panel/assets/{index-Fuk6t_3K.js → index-CvUR6l7C.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHdHSX06.js → index-CyUtBxnC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CUj-l7GM.js → index-D0rFX-xN.js} +1 -1
- package/src/assets/web-panel/assets/{index-C9bWmTcO.js → index-D6zyUpD6.js} +1 -1
- package/src/assets/web-panel/assets/{index-ChkXUj3f.js → index-DJk9cPrs.js} +1 -1
- package/src/assets/web-panel/assets/{index-pyvMVX8r.js → index-DLCoKst9.js} +1 -1
- package/src/assets/web-panel/assets/{index-ttDQVhZy.js → index-DNxR1CHs.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9Svn0zc.js → index-DWwI0Jjd.js} +1 -1
- package/src/assets/web-panel/assets/{index-BdutMsne.js → index-DjTB4wPn.js} +1 -1
- package/src/assets/web-panel/assets/{index-By5a0T_W.js → index-Du-w2i6G.js} +1 -1
- package/src/assets/web-panel/assets/{index-D22zNXiS.js → index-HJdfPSMz.js} +1 -1
- package/src/assets/web-panel/assets/{index-BSgWxAOG.js → index-I3Qdrfcf.js} +1 -1
- package/src/assets/web-panel/assets/{index-qNTtOVi_.js → index-Mipa7XgU.js} +1 -1
- package/src/assets/web-panel/assets/{index-DpW9cf3e.js → index-jO4lIpFE.js} +1 -1
- package/src/assets/web-panel/assets/{index-C37f6iey.js → index-oerotzoq.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cj0OZ-37.js → index-phHPSXlV.js} +1 -1
- package/src/assets/web-panel/assets/index-zW69WSDA.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-DtrnfsZH.js → initDefaultProps-B7tpABc_.js} +1 -1
- package/src/assets/web-panel/assets/{motion-D4zqSaX3.js → motion-DmvaRoVv.js} +1 -1
- package/src/assets/web-panel/assets/{move-CGFGOUQj.js → move-DF4X29Wf.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DH2HvkJl.js → mtc-parser-C0cbg4PO.js} +1 -1
- package/src/assets/web-panel/assets/{omit-jJ2at5HX.js → omit-DZzcYy0l.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CRyMT1Fv.js → pickAttrs-DaZWfzuH.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DZ_CX0Pt.js → placementArrow-CBxOEyvn.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-wruHk-h5.js → responsiveObserve-CGl8NIlO.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BGgx8q67.js → slide-Bv330MSZ.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-Do5M7gKm.js → statusUtils-DJVy5oEM.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CRa-GZpX.js → styleChecker-BiY1H5ST.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-BxPcq6pj.js → useFlexGapSupport-D1sN2OlQ.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-v5O2ZcJm.js → useFs-DxxHawzW.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub--z2FU0Px.js → usePersonalDataHub-C1HblLrD.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-1emidVKd.js → vnode-Dex0jy4T.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CcJaRyHs.js → zoom-Bk1MScAD.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-manifest.json +8 -1
- package/src/commands/agent.js +8 -1
- package/src/commands/changelog.js +148 -0
- package/src/commands/eval.js +88 -23
- package/src/commands/plugin.js +10 -0
- package/src/commands/team.js +26 -3
- package/src/data/changelog.json +199 -0
- package/src/gateways/ws/remote-session-protocol.js +9 -2
- package/src/harness/remote-command-ledger.js +60 -10
- package/src/index.js +2 -0
- package/src/lib/agent-sandbox.js +11 -5
- package/src/lib/agent-team/task-lease.js +28 -0
- package/src/lib/agent-team/team-runner.js +25 -0
- package/src/lib/changelog.js +252 -0
- package/src/lib/eval/tasks.js +62 -41
- package/src/lib/eval/trend.js +7 -1
- package/src/lib/lsp/__tests__/lsp-client.test.js +12 -6
- package/src/lib/lsp/code-intelligence.js +55 -17
- package/src/lib/lsp/lsp-client.js +7 -1
- package/src/lib/lsp/lsp-server-registry.js +15 -4
- package/src/lib/plugin-runtime/hooks.js +20 -4
- package/src/lib/plugin-runtime/install.js +20 -1
- package/src/lib/plugin-runtime/monitors.js +20 -4
- package/src/lib/plugin-runtime/policy.js +13 -0
- package/src/lib/plugin-runtime/remote-source.js +34 -0
- package/src/lib/plugin-runtime/signature.js +95 -22
- package/src/lib/plugin-security.js +13 -14
- package/src/lib/sandbox-egress-proxy.js +38 -2
- package/src/lib/sandbox-network-policy.js +26 -4
- package/src/lib/telemetry/span-recorder.js +26 -4
- package/src/repl/agent-repl.js +12 -8
- package/src/runtime/__tests__/auto-pin.test.js +27 -4
- package/src/runtime/auto-pin.js +19 -0
- package/src/runtime/headless-runner.js +23 -5
- package/src/assets/web-panel/assets/ChatBubbleRenderer-8nul8eTq.js +0 -1
- package/src/assets/web-panel/assets/devWarning-WDr3_M_N.js +0 -1
- package/src/assets/web-panel/assets/index-Dwix3p4g.js +0 -1
- package/src/assets/web-panel/assets/index-SHaoZ0CA.js +0 -1
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
discoverPlugins,
|
|
30
30
|
} from "./scopes.js";
|
|
31
31
|
import { verifyPluginManifest } from "../plugin-security.js";
|
|
32
|
-
import { writePluginLock } from "./signature.js";
|
|
32
|
+
import { writePluginLock, LOCK_FILENAME } from "./signature.js";
|
|
33
33
|
|
|
34
34
|
export const _deps = {
|
|
35
35
|
existsSync: fs.existsSync,
|
|
@@ -97,6 +97,12 @@ export function installFromDirectory(srcDir, opts = {}) {
|
|
|
97
97
|
_deps.mkdirSync(dest, { recursive: true });
|
|
98
98
|
copyDirGuarded(src, dest, dest);
|
|
99
99
|
|
|
100
|
+
// A lock may ONLY exist if THIS installer wrote it. The source is untrusted
|
|
101
|
+
// and may ship its own `.plugin-lock.json` (copied verbatim above) — on an
|
|
102
|
+
// unsigned install that forged lock would otherwise survive into the
|
|
103
|
+
// "immutable" version dir and defeat load-time requireSignedPlugins.
|
|
104
|
+
_deps.rmSync(path.join(dest, LOCK_FILENAME), { force: true });
|
|
105
|
+
|
|
100
106
|
// Record the verified signature into the installed (immutable) version dir so
|
|
101
107
|
// load-time enforcement can re-check it. The manifest was copied verbatim, so
|
|
102
108
|
// its bytes/sha in `dest` match what we verified in `src`.
|
|
@@ -110,6 +116,8 @@ export function installFromDirectory(srcDir, opts = {}) {
|
|
|
110
116
|
sha256: verification.sha256,
|
|
111
117
|
publicKeySha256: verification.publicKeySha256,
|
|
112
118
|
signatureVerified: verification.signatureVerified === true,
|
|
119
|
+
signatureBase64: verification.signatureBase64,
|
|
120
|
+
publicKeyPem: verification.publicKeyPem,
|
|
113
121
|
});
|
|
114
122
|
}
|
|
115
123
|
|
|
@@ -245,6 +253,17 @@ export function parseGitSource(raw) {
|
|
|
245
253
|
* command line — no injection). Caller removes the temp dir's parent.
|
|
246
254
|
*/
|
|
247
255
|
export function fetchGitRepo(url, ref) {
|
|
256
|
+
// git argv-injection guard: a value starting with "-" is parsed by git as an
|
|
257
|
+
// OPTION, not a URL/ref — e.g. a registry-supplied ref "-f" reaches
|
|
258
|
+
// `git checkout <ref>` on the full-clone retry path, and an option-looking
|
|
259
|
+
// url reaches `git clone`. Real git URLs/refs never start with "-"
|
|
260
|
+
// (check-ref-format forbids it), so reject instead of trying to escape.
|
|
261
|
+
if (String(url).startsWith("-")) {
|
|
262
|
+
throw new Error(`refusing git source that looks like an option: ${url}`);
|
|
263
|
+
}
|
|
264
|
+
if (ref != null && String(ref).startsWith("-")) {
|
|
265
|
+
throw new Error(`refusing git ref that looks like an option: ${ref}`);
|
|
266
|
+
}
|
|
248
267
|
const base = _deps.mkdtempSync(path.join(os.tmpdir(), "cc-plugin-git-"));
|
|
249
268
|
const dir = path.join(base, "repo");
|
|
250
269
|
const run = (args) =>
|
|
@@ -79,11 +79,27 @@ export function collectPluginMonitors(opts = {}) {
|
|
|
79
79
|
for (const p of trusted) {
|
|
80
80
|
if (!p.manifest || p.manifest.ok !== true) continue;
|
|
81
81
|
const m = p.manifest.components?.monitors;
|
|
82
|
-
if (!m
|
|
82
|
+
if (!m) continue;
|
|
83
83
|
let parsed;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
if (m.absPath) {
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(_deps.readFileSync(m.absPath, "utf8"));
|
|
87
|
+
} catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
} else if (m.inline) {
|
|
91
|
+
// Monitors declared inline in plugin.json — the normalized component keeps
|
|
92
|
+
// only counts, so re-read the raw manifest for the actual entries (same
|
|
93
|
+
// approach as the MCP collector). Without this, inline monitors never spawn.
|
|
94
|
+
try {
|
|
95
|
+
const raw = JSON.parse(
|
|
96
|
+
_deps.readFileSync(p.manifest.manifestPath, "utf8"),
|
|
97
|
+
);
|
|
98
|
+
parsed = raw && typeof raw === "object" ? raw.monitors : null;
|
|
99
|
+
} catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
87
103
|
continue;
|
|
88
104
|
}
|
|
89
105
|
const list = Array.isArray(parsed?.monitors)
|
|
@@ -115,6 +115,19 @@ export function filterByManagedPolicy(plugins, managed) {
|
|
|
115
115
|
continue;
|
|
116
116
|
}
|
|
117
117
|
if (requireSigned) {
|
|
118
|
+
// Fail closed when no signing keys are pinned: a lock's signature can be
|
|
119
|
+
// SELF-signed by whoever authored the plugin (the lock lives in a
|
|
120
|
+
// writable dir), so "signed by anyone" is not a guarantee. Without
|
|
121
|
+
// trustedPluginKeySha256 the gate would accept any self-signed drop-in.
|
|
122
|
+
if (!trustedKeys || trustedKeys.size === 0) {
|
|
123
|
+
dropped.push({
|
|
124
|
+
name: p.name,
|
|
125
|
+
reason:
|
|
126
|
+
"requireSignedPlugins: no trustedPluginKeySha256 fingerprints " +
|
|
127
|
+
"configured (fail-closed — pin the org's signing keys)",
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
118
131
|
const v = verifyInstalledSignature(p, { trustedKeys });
|
|
119
132
|
if (!v.signed) {
|
|
120
133
|
dropped.push({
|
|
@@ -46,6 +46,39 @@ export function isRemoteSource(raw) {
|
|
|
46
46
|
return /^https?:\/\//i.test(s) && /\.json(\?.*)?(#.*)?$/i.test(s);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Enforce HTTPS for registry URLs. The registry supplies BOTH the git source
|
|
51
|
+
* and its claimed sha256 — over plain HTTP a network MITM controls the two
|
|
52
|
+
* together, so the integrity check verifies nothing. Loopback is exempt (a
|
|
53
|
+
* local dev registry isn't MITM-able off-host); anything else needs the
|
|
54
|
+
* explicit opt-in (opts.allowInsecure / CC_PLUGIN_REGISTRY_ALLOW_HTTP=1).
|
|
55
|
+
*/
|
|
56
|
+
export function assertRegistryUrlSafe(url, { allowInsecure = false } = {}) {
|
|
57
|
+
let u;
|
|
58
|
+
try {
|
|
59
|
+
u = new URL(String(url));
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error(`invalid registry URL: ${url}`);
|
|
62
|
+
}
|
|
63
|
+
if (u.protocol === "https:") return;
|
|
64
|
+
if (u.protocol !== "http:") {
|
|
65
|
+
throw new Error(`registry URL must be http(s): ${url}`);
|
|
66
|
+
}
|
|
67
|
+
// WHATWG URL keeps the brackets on IPv6 hostnames — strip before comparing.
|
|
68
|
+
const host = u.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
69
|
+
const loopback =
|
|
70
|
+
host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
71
|
+
const optIn =
|
|
72
|
+
allowInsecure === true || process.env.CC_PLUGIN_REGISTRY_ALLOW_HTTP === "1";
|
|
73
|
+
if (loopback || optIn) return;
|
|
74
|
+
throw new Error(
|
|
75
|
+
`plain-HTTP registry rejected: ${url} — a network MITM controls both the ` +
|
|
76
|
+
`source and its sha256, so the integrity check verifies nothing. Use ` +
|
|
77
|
+
`https, or opt in with --allow-insecure-registry / ` +
|
|
78
|
+
`CC_PLUGIN_REGISTRY_ALLOW_HTTP=1 on a trusted network.`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
49
82
|
/** Where a fetched registry is cached (content-addressed by its URL). */
|
|
50
83
|
export function registryCachePath(url, cacheDir) {
|
|
51
84
|
const dir =
|
|
@@ -93,6 +126,7 @@ export async function fetchRegistry(url, opts = {}) {
|
|
|
93
126
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
94
127
|
allowCache = true,
|
|
95
128
|
} = opts;
|
|
129
|
+
assertRegistryUrlSafe(url, { allowInsecure: opts.allowInsecure });
|
|
96
130
|
const cachePath = registryCachePath(url, cacheDir);
|
|
97
131
|
const headers = { Accept: "application/json" };
|
|
98
132
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
@@ -7,20 +7,24 @@
|
|
|
7
7
|
* install records the verification result in a `.plugin-lock.json` inside the
|
|
8
8
|
* immutable version dir; discovery then re-checks it fail-closed.
|
|
9
9
|
*
|
|
10
|
-
* The lock
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* The lock persists the DETACHED SIGNATURE + PUBLIC KEY themselves (lockVersion
|
|
11
|
+
* 2), and load-time verification re-runs the actual Ed25519 verify over the
|
|
12
|
+
* on-disk manifest bytes. The lock file lives in a writable plugin dir, so
|
|
13
|
+
* nothing in it can be TRUSTED — a recorded boolean or fingerprint could simply
|
|
14
|
+
* be forged by whoever authored the plugin. What a forger CANNOT do is produce a
|
|
15
|
+
* signature that verifies under a key the org has pinned via
|
|
16
|
+
* trustedPluginKeySha256 — which is why the key fingerprint used for the trust
|
|
17
|
+
* check is COMPUTED from the embedded public key, never read from the lock.
|
|
18
|
+
* The signature covers only the manifest file, not every component file — a
|
|
14
19
|
* known limitation (documented), an integrity anchor rather than a full
|
|
15
|
-
* supply-chain seal; the
|
|
16
|
-
* tampering, which is where the component wiring is declared.
|
|
20
|
+
* supply-chain seal; the manifest is where the component wiring is declared.
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
|
-
import { createHash } from "node:crypto";
|
|
23
|
+
import { createHash, createPublicKey, verify } from "node:crypto";
|
|
20
24
|
import fs from "fs";
|
|
21
25
|
import path from "path";
|
|
22
26
|
|
|
23
|
-
const
|
|
27
|
+
export const LOCK_FILENAME = ".plugin-lock.json";
|
|
24
28
|
|
|
25
29
|
export const _deps = {
|
|
26
30
|
readFileSync: fs.readFileSync,
|
|
@@ -31,18 +35,29 @@ export const _deps = {
|
|
|
31
35
|
/** Record a verified signature into the version dir. */
|
|
32
36
|
export function writePluginLock(
|
|
33
37
|
versionDir,
|
|
34
|
-
{
|
|
38
|
+
{
|
|
39
|
+
manifestFile,
|
|
40
|
+
sha256,
|
|
41
|
+
publicKeySha256,
|
|
42
|
+
signatureVerified,
|
|
43
|
+
signatureBase64,
|
|
44
|
+
publicKeyPem,
|
|
45
|
+
},
|
|
35
46
|
) {
|
|
36
47
|
const rel = path.relative(versionDir, manifestFile).replace(/\\/g, "/");
|
|
37
48
|
const lock = {
|
|
38
|
-
lockVersion:
|
|
49
|
+
lockVersion: 2,
|
|
39
50
|
manifest: rel || "plugin.json",
|
|
40
51
|
sha256,
|
|
41
52
|
publicKeySha256: publicKeySha256 || null,
|
|
42
53
|
signatureVerified: signatureVerified === true,
|
|
54
|
+
// The material load-time enforcement actually re-verifies. Without these a
|
|
55
|
+
// lock is just self-asserted JSON and counts as unsigned.
|
|
56
|
+
signatureBase64: signatureBase64 || null,
|
|
57
|
+
publicKeyPem: publicKeyPem || null,
|
|
43
58
|
};
|
|
44
59
|
_deps.writeFileSync(
|
|
45
|
-
path.join(versionDir,
|
|
60
|
+
path.join(versionDir, LOCK_FILENAME),
|
|
46
61
|
JSON.stringify(lock, null, 2),
|
|
47
62
|
"utf8",
|
|
48
63
|
);
|
|
@@ -51,7 +66,7 @@ export function writePluginLock(
|
|
|
51
66
|
|
|
52
67
|
/** Read a version dir's signature lock, or null when absent/unreadable. */
|
|
53
68
|
export function readPluginLock(versionDir) {
|
|
54
|
-
const p = path.join(versionDir,
|
|
69
|
+
const p = path.join(versionDir, LOCK_FILENAME);
|
|
55
70
|
if (!_deps.existsSync(p)) return null;
|
|
56
71
|
try {
|
|
57
72
|
return JSON.parse(_deps.readFileSync(p, "utf8"));
|
|
@@ -62,9 +77,11 @@ export function readPluginLock(versionDir) {
|
|
|
62
77
|
|
|
63
78
|
/**
|
|
64
79
|
* Verify an installed plugin's recorded signature against its on-disk manifest.
|
|
65
|
-
* `signed:true` ONLY when a lock exists,
|
|
66
|
-
*
|
|
67
|
-
* is a non-empty Set — the signing key fingerprint
|
|
80
|
+
* `signed:true` ONLY when a lock exists, carries real signature material, the
|
|
81
|
+
* embedded Ed25519 signature VERIFIES over the on-disk manifest bytes, and —
|
|
82
|
+
* when trustedKeys is a non-empty Set — the signing key's COMPUTED fingerprint
|
|
83
|
+
* is among them. Every field of the lock is treated as attacker-writable; the
|
|
84
|
+
* only trust anchors are the cryptographic verify and the caller's trustedKeys.
|
|
68
85
|
*
|
|
69
86
|
* @param {{root:string}} plugin
|
|
70
87
|
* @param {object} [opts] { trustedKeys?: Set<string> }
|
|
@@ -77,31 +94,87 @@ export function verifyInstalledSignature(plugin, opts = {}) {
|
|
|
77
94
|
return { signed: false, reason: "lock is not signature-verified" };
|
|
78
95
|
}
|
|
79
96
|
const manifestFile = path.join(plugin.root, lock.manifest || "plugin.json");
|
|
97
|
+
// `lock.manifest` is untrusted — it lives in the (writable) plugin dir. A lock
|
|
98
|
+
// pointing OUTSIDE the plugin root (`manifest: "../../secret"`) would make us
|
|
99
|
+
// re-hash an arbitrary file and, with a matching locked sha256, forge
|
|
100
|
+
// signed:true — which gates load-time requireSignedPlugins. A plugin's manifest
|
|
101
|
+
// is ALWAYS inside its own root, so reject any path that escapes it (boundary
|
|
102
|
+
// via path.relative, not startsWith — the sandbox-fs-policy pattern).
|
|
103
|
+
const rel = path.relative(
|
|
104
|
+
path.resolve(plugin.root),
|
|
105
|
+
path.resolve(manifestFile),
|
|
106
|
+
);
|
|
107
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
108
|
+
return {
|
|
109
|
+
signed: false,
|
|
110
|
+
reason: "lock manifest path escapes the plugin root",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
80
113
|
if (!_deps.existsSync(manifestFile)) {
|
|
81
114
|
return { signed: false, reason: "locked manifest file is missing" };
|
|
82
115
|
}
|
|
83
|
-
let
|
|
116
|
+
let bytes;
|
|
84
117
|
try {
|
|
85
|
-
|
|
86
|
-
.update(_deps.readFileSync(manifestFile))
|
|
87
|
-
.digest("hex");
|
|
118
|
+
bytes = _deps.readFileSync(manifestFile);
|
|
88
119
|
} catch {
|
|
89
120
|
return { signed: false, reason: "manifest unreadable" };
|
|
90
121
|
}
|
|
122
|
+
const sha = createHash("sha256").update(bytes).digest("hex");
|
|
91
123
|
if (sha.toLowerCase() !== String(lock.sha256 || "").toLowerCase()) {
|
|
92
124
|
return {
|
|
93
125
|
signed: false,
|
|
94
126
|
reason: "manifest tampered since install (sha256 mismatch)",
|
|
95
127
|
};
|
|
96
128
|
}
|
|
129
|
+
// A lock without real signature material is unverifiable, and therefore
|
|
130
|
+
// worthless as a signing gate: `signatureVerified:true` is just a JSON field
|
|
131
|
+
// anyone can write. Legacy (lockVersion 1) locks land here too — reinstalling
|
|
132
|
+
// the plugin with --signature re-records a verifiable lock.
|
|
133
|
+
if (!lock.signatureBase64 || !lock.publicKeyPem) {
|
|
134
|
+
return {
|
|
135
|
+
signed: false,
|
|
136
|
+
reason:
|
|
137
|
+
"lock lacks signature material (legacy or hand-written lock — " +
|
|
138
|
+
"reinstall the plugin with --signature to re-record it)",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
let keyObject;
|
|
142
|
+
let publicKeySha256;
|
|
143
|
+
try {
|
|
144
|
+
keyObject = createPublicKey(lock.publicKeyPem);
|
|
145
|
+
// Fingerprint is COMPUTED from the embedded key — the lock's own
|
|
146
|
+
// publicKeySha256 field is attacker-writable and never used for trust.
|
|
147
|
+
publicKeySha256 = createHash("sha256")
|
|
148
|
+
.update(keyObject.export({ type: "spki", format: "der" }))
|
|
149
|
+
.digest("hex");
|
|
150
|
+
} catch {
|
|
151
|
+
return { signed: false, reason: "lock public key is not a valid key" };
|
|
152
|
+
}
|
|
153
|
+
let sigOk = false;
|
|
154
|
+
try {
|
|
155
|
+
sigOk = verify(
|
|
156
|
+
null,
|
|
157
|
+
bytes,
|
|
158
|
+
keyObject,
|
|
159
|
+
Buffer.from(String(lock.signatureBase64), "base64"),
|
|
160
|
+
);
|
|
161
|
+
} catch {
|
|
162
|
+
sigOk = false;
|
|
163
|
+
}
|
|
164
|
+
if (!sigOk) {
|
|
165
|
+
return {
|
|
166
|
+
signed: false,
|
|
167
|
+
reason: "recorded signature does not verify over the on-disk manifest",
|
|
168
|
+
};
|
|
169
|
+
}
|
|
97
170
|
const trustedKeys = opts.trustedKeys;
|
|
98
171
|
if (trustedKeys && trustedKeys.size > 0) {
|
|
99
|
-
if (!
|
|
172
|
+
if (!trustedKeys.has(publicKeySha256)) {
|
|
100
173
|
return {
|
|
101
174
|
signed: false,
|
|
102
|
-
reason: `signing key not trusted (${
|
|
175
|
+
reason: `signing key not trusted (${publicKeySha256})`,
|
|
103
176
|
};
|
|
104
177
|
}
|
|
105
178
|
}
|
|
106
|
-
return { signed: true, publicKeySha256
|
|
179
|
+
return { signed: true, publicKeySha256 };
|
|
107
180
|
}
|
|
@@ -80,16 +80,19 @@ export function verifyPluginManifest({
|
|
|
80
80
|
|
|
81
81
|
const wantsSignature = requireSignature || signatureFile || publicKeyFile;
|
|
82
82
|
let signatureVerified = false;
|
|
83
|
+
let signature = null;
|
|
84
|
+
let publicKeyPem = null;
|
|
85
|
+
let publicKeySha256 = null;
|
|
83
86
|
if (wantsSignature) {
|
|
84
87
|
if (!signatureFile || !publicKeyFile) {
|
|
85
88
|
throw new Error(
|
|
86
89
|
"plugin signature verification requires --signature and --public-key",
|
|
87
90
|
);
|
|
88
91
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const keyObject = createPublicKey(
|
|
92
|
-
|
|
92
|
+
signature = readFileSync(signatureFile);
|
|
93
|
+
publicKeyPem = readFileSync(publicKeyFile, "utf8");
|
|
94
|
+
const keyObject = createPublicKey(publicKeyPem);
|
|
95
|
+
publicKeySha256 = createHash("sha256")
|
|
93
96
|
.update(keyObject.export({ type: "spki", format: "der" }))
|
|
94
97
|
.digest("hex");
|
|
95
98
|
const trusted = stringSet(trustedKeySha256);
|
|
@@ -110,16 +113,12 @@ export function verifyPluginManifest({
|
|
|
110
113
|
bytes,
|
|
111
114
|
sha256,
|
|
112
115
|
signatureVerified,
|
|
113
|
-
publicKeySha256: signatureVerified
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}),
|
|
120
|
-
)
|
|
121
|
-
.digest("hex")
|
|
122
|
-
: null,
|
|
116
|
+
publicKeySha256: signatureVerified ? publicKeySha256 : null,
|
|
117
|
+
// The raw signature material, so the installer can persist it and load-time
|
|
118
|
+
// enforcement can CRYPTOGRAPHICALLY re-verify (not merely trust a recorded
|
|
119
|
+
// boolean, which a hand-written lock file could forge).
|
|
120
|
+
signatureBase64: signatureVerified ? signature.toString("base64") : null,
|
|
121
|
+
publicKeyPem: signatureVerified ? publicKeyPem : null,
|
|
123
122
|
};
|
|
124
123
|
}
|
|
125
124
|
|
|
@@ -72,6 +72,42 @@ async function guardResolvedTarget(host, policy, lookup) {
|
|
|
72
72
|
return { ok: true, ip: typeof first === "string" ? first : first.address };
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Parse a CONNECT request target ("host:port") into `{ host, port }`. A naive
|
|
77
|
+
* `split(":")` mangles IPv6 literals — `[2001:db8::1]:8443` would yield host
|
|
78
|
+
* `"[2001"` and a NaN port that silently defaults to 443, so even an IP-pinned
|
|
79
|
+
* tunnel connects to the WRONG port (and, when unpinned, the wrong host). Handle
|
|
80
|
+
* the bracketed-IPv6, bare-IPv6, and `name[:port]` forms explicitly. Port
|
|
81
|
+
* defaults to 443 (the CONNECT/HTTPS default).
|
|
82
|
+
*/
|
|
83
|
+
export function parseConnectTarget(target) {
|
|
84
|
+
const s = String(target == null ? "" : target).trim();
|
|
85
|
+
const toPort = (v) => {
|
|
86
|
+
const n = parseInt(v, 10);
|
|
87
|
+
return Number.isInteger(n) && n > 0 && n <= 65535 ? n : 443;
|
|
88
|
+
};
|
|
89
|
+
// Bracketed IPv6: "[::1]" or "[::1]:8443"
|
|
90
|
+
if (s.startsWith("[")) {
|
|
91
|
+
const end = s.indexOf("]");
|
|
92
|
+
if (end > 0) {
|
|
93
|
+
const host = s.slice(1, end);
|
|
94
|
+
const rest = s.slice(end + 1); // "" or ":8443"
|
|
95
|
+
return { host, port: rest.startsWith(":") ? toPort(rest.slice(1)) : 443 };
|
|
96
|
+
}
|
|
97
|
+
// malformed bracket — fall through to the generic handling
|
|
98
|
+
}
|
|
99
|
+
// Bare IPv6 literal (2+ colons, no brackets) → no port component to split off.
|
|
100
|
+
if ((s.match(/:/g) || []).length > 1) {
|
|
101
|
+
return { host: s, port: 443 };
|
|
102
|
+
}
|
|
103
|
+
// "name:port" (rsplit on the single colon) or bare "name".
|
|
104
|
+
const idx = s.lastIndexOf(":");
|
|
105
|
+
if (idx > 0) {
|
|
106
|
+
return { host: s.slice(0, idx), port: toPort(s.slice(idx + 1)) };
|
|
107
|
+
}
|
|
108
|
+
return { host: s, port: 443 };
|
|
109
|
+
}
|
|
110
|
+
|
|
75
111
|
/**
|
|
76
112
|
* Build the proxy env vars a child process needs to route through the proxy.
|
|
77
113
|
* `NO_PROXY` is intentionally empty so nothing bypasses the filter.
|
|
@@ -204,8 +240,8 @@ export function createEgressProxy(policy = {}, opts = {}) {
|
|
|
204
240
|
clientSocket.end();
|
|
205
241
|
return;
|
|
206
242
|
}
|
|
207
|
-
|
|
208
|
-
const port =
|
|
243
|
+
// Parse host:port properly (IPv6 literals break a naive split).
|
|
244
|
+
const { host, port } = parseConnectTarget(req.url);
|
|
209
245
|
// Connect to the pinned validated IP so a rebind at connect time can't
|
|
210
246
|
// swap in a private address between our check and the socket.
|
|
211
247
|
const upstream = net.connect(port, ip || host, () => {
|
|
@@ -60,6 +60,24 @@ function anyMatch(host, patterns) {
|
|
|
60
60
|
return (patterns || []).some((p) => matchesDomain(host, p));
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* If `h` is an IPv4-mapped IPv6 address, return the embedded IPv4 in dotted
|
|
65
|
+
* form; else null. Handles both the dotted tail ("::ffff:127.0.0.1", how a user
|
|
66
|
+
* might type it) and the compressed-hex tail ("::ffff:7f00:1", how Node's URL
|
|
67
|
+
* parser serializes it). The hex tail is two 16-bit groups = the 32-bit IPv4.
|
|
68
|
+
*/
|
|
69
|
+
function mappedIpv4Tail(h) {
|
|
70
|
+
const dotted = h.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
|
|
71
|
+
if (dotted) return dotted[1];
|
|
72
|
+
const hex = h.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
|
|
73
|
+
if (hex) {
|
|
74
|
+
const high = parseInt(hex[1], 16);
|
|
75
|
+
const low = parseInt(hex[2], 16);
|
|
76
|
+
return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
63
81
|
/** Private / loopback / link-local / cloud-metadata target? (SSRF guard) */
|
|
64
82
|
export function isPrivateHost(host) {
|
|
65
83
|
if (!host) return false;
|
|
@@ -70,10 +88,14 @@ export function isPrivateHost(host) {
|
|
|
70
88
|
// IPv6 loopback / unique-local / link-local.
|
|
71
89
|
if (h === "::1" || h === "::") return true;
|
|
72
90
|
if (/^f[cd][0-9a-f]{2}:/i.test(h)) return true; // fc00::/7 unique-local
|
|
73
|
-
if (/^
|
|
74
|
-
// IPv4-mapped IPv6
|
|
75
|
-
|
|
76
|
-
|
|
91
|
+
if (/^fe[89ab][0-9a-f]:/i.test(h)) return true; // fe80::/10 link-local (fe80–febf)
|
|
92
|
+
// IPv4-mapped IPv6 — check the embedded IPv4 tail. Node's WHATWG URL parser
|
|
93
|
+
// serializes "[::ffff:127.0.0.1]" as COMPRESSED HEX ("::ffff:7f00:1"), never
|
|
94
|
+
// dotted-decimal, so we must decode BOTH forms or the guard is dead code for
|
|
95
|
+
// any URL/bracketed input (loopback, private ranges, AND 169.254.169.254 all
|
|
96
|
+
// sail through — confirmed SSRF bypass). Bare non-URL hosts may still arrive
|
|
97
|
+
// dotted, so keep that form too.
|
|
98
|
+
const ipv4 = mappedIpv4Tail(h) || h;
|
|
77
99
|
const m = ipv4.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
78
100
|
if (m) {
|
|
79
101
|
const [a, b] = [Number(m[1]), Number(m[2])];
|
|
@@ -20,6 +20,23 @@ function spanId() {
|
|
|
20
20
|
return _seq.toString(16).padStart(16, "0");
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// One trace per recorder (a "run"). The OTLP/OTel spec treats an ALL-ZERO
|
|
24
|
+
// traceId as INVALID, so a strict collector would drop every span exported with
|
|
25
|
+
// `"0".repeat(32)` — defeating toOtlp()'s purpose. A bare counter is non-zero
|
|
26
|
+
// but resets every PROCESS, so successive CLI runs all exported trace
|
|
27
|
+
// `000…001` and a collector merged them into one giant trace. Prefix the
|
|
28
|
+
// counter with per-process entropy (start time + pid — RNG-free, so ids stay
|
|
29
|
+
// deterministic WITHIN a run) to make traces practically unique ACROSS runs.
|
|
30
|
+
const _traceBase = (
|
|
31
|
+
Date.now().toString(16).padStart(12, "0") +
|
|
32
|
+
(process.pid >>> 0).toString(16).padStart(8, "0")
|
|
33
|
+
).slice(0, 20);
|
|
34
|
+
let _traceSeq = 0;
|
|
35
|
+
function newTraceId() {
|
|
36
|
+
_traceSeq = (_traceSeq + 1) % Number.MAX_SAFE_INTEGER;
|
|
37
|
+
return _traceBase + _traceSeq.toString(16).padStart(12, "0"); // 32 hex, never all-zero
|
|
38
|
+
}
|
|
39
|
+
|
|
23
40
|
export class TelemetryRecorder {
|
|
24
41
|
constructor({
|
|
25
42
|
now = () => Date.now(),
|
|
@@ -27,6 +44,7 @@ export class TelemetryRecorder {
|
|
|
27
44
|
} = {}) {
|
|
28
45
|
this._now = typeof now === "function" ? now : () => now;
|
|
29
46
|
this.serviceName = serviceName;
|
|
47
|
+
this.traceId = newTraceId(); // one trace per recorder (valid non-zero id)
|
|
30
48
|
this._spans = []; // completed spans
|
|
31
49
|
this._counters = new Map(); // name → { total, byAttr }
|
|
32
50
|
this._failures = new Map(); // category → count
|
|
@@ -171,12 +189,16 @@ export class TelemetryRecorder {
|
|
|
171
189
|
{
|
|
172
190
|
scope: { name: "chainlesschain.cli" },
|
|
173
191
|
spans: this._spans.map((s) => ({
|
|
174
|
-
traceId:
|
|
192
|
+
traceId: this.traceId,
|
|
175
193
|
spanId: s.id,
|
|
176
194
|
parentSpanId: s.parentId || "",
|
|
177
195
|
name: s.name,
|
|
178
|
-
|
|
179
|
-
|
|
196
|
+
// proto3 JSON mapping: (s)fixed64/(u)int64 fields MUST be
|
|
197
|
+
// decimal STRINGS — ms×1e6 ≈ 1.8e18 exceeds 2^53, so a JSON
|
|
198
|
+
// number both violates the OTLP/JSON spec (strict collectors
|
|
199
|
+
// reject the span) and quantizes (~256 ns spacing).
|
|
200
|
+
startTimeUnixNano: String(Math.round(s.startTime * 1e6)),
|
|
201
|
+
endTimeUnixNano: String(Math.round(s.endTime * 1e6)),
|
|
180
202
|
status: { code: s.status === "error" ? 2 : 1 },
|
|
181
203
|
attributes: Object.entries(s.attributes).map(([k, v]) => ({
|
|
182
204
|
key: k,
|
|
@@ -189,7 +211,7 @@ export class TelemetryRecorder {
|
|
|
189
211
|
})),
|
|
190
212
|
events: s.events.map((e) => ({
|
|
191
213
|
name: e.name,
|
|
192
|
-
timeUnixNano: e.time * 1e6,
|
|
214
|
+
timeUnixNano: String(Math.round(e.time * 1e6)),
|
|
193
215
|
attributes: Object.entries(e.attributes).map(([k, v]) => ({
|
|
194
216
|
key: k,
|
|
195
217
|
value: { stringValue: String(v) },
|
package/src/repl/agent-repl.js
CHANGED
|
@@ -64,7 +64,10 @@ import {
|
|
|
64
64
|
PromptCompressor,
|
|
65
65
|
getContextWindow,
|
|
66
66
|
} from "../harness/prompt-compressor.js";
|
|
67
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
buildAutoPinPredicate,
|
|
69
|
+
resolveAutoPinOption,
|
|
70
|
+
} from "../runtime/auto-pin.js";
|
|
68
71
|
import { feature } from "../lib/feature-flags.js";
|
|
69
72
|
import { recordCompressionMetric } from "../lib/compression-telemetry.js";
|
|
70
73
|
import {
|
|
@@ -417,15 +420,16 @@ export async function startAgentRepl(options = {}) {
|
|
|
417
420
|
// Unset → agent-core's 180s default (matches cc chat/ask). Set to 0 to
|
|
418
421
|
// disable. Left undefined here means "use the default".
|
|
419
422
|
let _streamStallTimeoutMs;
|
|
420
|
-
// Auto-pin (
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
|
|
423
|
+
// Auto-pin (default ON since 2026-07-07): compaction pins the original task
|
|
424
|
+
// (first user turn) so it survives context compression. Resolution order:
|
|
425
|
+
// CC_AUTO_PIN env ("1"/"0") > config `context.autoPin` (true/false/object) >
|
|
426
|
+
// default on. See resolveAutoPinOption.
|
|
427
|
+
let _autoPinCfgValue;
|
|
424
428
|
try {
|
|
425
429
|
const { loadConfig } = await import("../lib/config-manager.js");
|
|
426
430
|
const _cfg = loadConfig();
|
|
427
431
|
_visionModel = _cfg?.llm?.visionModel || undefined;
|
|
428
|
-
|
|
432
|
+
_autoPinCfgValue = _cfg?.context?.autoPin;
|
|
429
433
|
const raw = _cfg?.llm?.streamStallTimeoutMs;
|
|
430
434
|
const t = Number(raw);
|
|
431
435
|
// Accept 0 (explicit disable) — only ignore absent/invalid values.
|
|
@@ -483,8 +487,8 @@ export async function startAgentRepl(options = {}) {
|
|
|
483
487
|
if (feature("PROMPT_COMPRESSOR")) {
|
|
484
488
|
_compressor = new PromptCompressor({ model, provider });
|
|
485
489
|
}
|
|
486
|
-
|
|
487
|
-
// Compaction options shared by /compact + auto-compact. Adds the
|
|
490
|
+
const _autoPinOpt = resolveAutoPinOption({ config: _autoPinCfgValue });
|
|
491
|
+
// Compaction options shared by /compact + auto-compact. Adds the
|
|
488
492
|
// pin predicate only when auto-pin is enabled; otherwise byte-identical.
|
|
489
493
|
const _compactOpts = (msgs) => {
|
|
490
494
|
const base = { preserveToolPairs: true };
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* auto-pin policy tests
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* auto-pin policy tests. The PREDICATE layer stays opt-valued (null when the
|
|
3
|
+
* resolved option is falsy → byte-identical compaction); the EFFECTIVE default
|
|
4
|
+
* is decided by resolveAutoPinOption — ON since 2026-07-07, opt out via
|
|
5
|
+
* CC_AUTO_PIN=0 or config context.autoPin=false.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, it, expect } from "vitest";
|
|
9
9
|
import {
|
|
10
10
|
resolveAutoPinConfig,
|
|
11
|
+
resolveAutoPinOption,
|
|
11
12
|
buildAutoPinPredicate,
|
|
12
13
|
describeAutoPin,
|
|
13
14
|
} from "../auto-pin.js";
|
|
@@ -102,3 +103,25 @@ describe("describeAutoPin", () => {
|
|
|
102
103
|
);
|
|
103
104
|
});
|
|
104
105
|
});
|
|
106
|
+
|
|
107
|
+
describe("resolveAutoPinOption (effective default — ON since 2026-07-07)", () => {
|
|
108
|
+
it("defaults ON when nothing is set", () => {
|
|
109
|
+
expect(resolveAutoPinOption({ env: undefined, config: undefined })).toBe(
|
|
110
|
+
true,
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
it("CC_AUTO_PIN=0 opts out; =1 forces on over a config false", () => {
|
|
114
|
+
expect(resolveAutoPinOption({ env: "0", config: true })).toBe(false);
|
|
115
|
+
expect(resolveAutoPinOption({ env: "1", config: false })).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
it("config false opts out; a config object passes through", () => {
|
|
118
|
+
expect(resolveAutoPinOption({ env: undefined, config: false })).toBe(false);
|
|
119
|
+
const cfg = { maxPinTokens: 99 };
|
|
120
|
+
expect(resolveAutoPinOption({ env: undefined, config: cfg })).toBe(cfg);
|
|
121
|
+
});
|
|
122
|
+
it("an explicit --auto-pin flag wins over everything", () => {
|
|
123
|
+
expect(resolveAutoPinOption({ flag: true, env: "0", config: false })).toBe(
|
|
124
|
+
true,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
});
|
package/src/runtime/auto-pin.js
CHANGED
|
@@ -23,6 +23,25 @@
|
|
|
23
23
|
|
|
24
24
|
const DEFAULT_MAX_PIN_TOKENS = 2000;
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the EFFECTIVE auto-pin setting from (highest precedence first):
|
|
28
|
+
* 1. an explicit CLI flag (`--auto-pin` → force on),
|
|
29
|
+
* 2. the CC_AUTO_PIN env var ("1" on / "0" off),
|
|
30
|
+
* 3. the config value (`context.autoPin`: true | false | {…}),
|
|
31
|
+
* 4. the default — **ON** (product decision 2026-07-07): pinning the
|
|
32
|
+
* original task through compaction costs a bounded slice of context
|
|
33
|
+
* (maxPinTokens, default 2000) but prevents goal drift in long sessions.
|
|
34
|
+
* Opt out with CC_AUTO_PIN=0 or `context.autoPin: false` in config.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveAutoPinOption({ flag, env, config } = {}) {
|
|
37
|
+
if (flag === true) return true;
|
|
38
|
+
const e = env !== undefined ? env : process.env.CC_AUTO_PIN;
|
|
39
|
+
if (e === "0") return false;
|
|
40
|
+
if (e === "1") return true;
|
|
41
|
+
if (config !== undefined && config !== null) return config;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
26
45
|
/**
|
|
27
46
|
* Normalize the opt-in value into a config object, or null when auto-pin is off.
|
|
28
47
|
* Accepts: falsy (off), `true` (defaults on), or a config object
|