chainlesschain 0.166.78 → 0.166.80
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 +8 -16
- package/package.json +1 -1
- package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
- package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-DSyZEY2G.js → AIOps-C5NtVVBN.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DhNyZ577.js → ActionButton-CE9-HsoR.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-HMMpxmyi.js → Analytics-DRO6sI0Z.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CwKMM5Ie.js → AppLayout-CuVJiyC_.js} +3 -3
- package/src/assets/web-panel/assets/{Artifacts-CNzDmph7.js → Artifacts-GNahHsyZ.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DxWpUW3V.js → Audit-DWvTomfm.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-C1R2ST-P.js → BackgroundAgents-k4TuA3ec.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-B6biTaEV.js → Backup-BxqN-65D.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CVnkD2Z5.js → BaseInput-CYYLvwwD.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-Dbewl4Je.js → Chat-CoDmZ9yb.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-DVZ6SwC5.js → ChatBubbleRenderer-ChTIMWch.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BqqiF1JE.js → Checkbox-CRVlfbkN.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BVMkGumc.js → Codegen-BttMy7rW.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DmiRHy96.js → Col-DsdlE4QC.js} +1 -1
- package/src/assets/web-panel/assets/{Community-BDlpygqA.js → Community-BulheyO-.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-B2NME_m-.js → Compact-BC_7wMxW.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BlzDLE-R.js → Compliance-BA3PDNLK.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-C8raPybB.js → Cowork-B6Y3L-GP.js} +4 -4
- package/src/assets/web-panel/assets/{Cron-DA8Py0zP.js → Cron-DLk_CG77.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DU2XyHqB.js → Crosschain-CvsKV-90.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Df098fT_.js → DID-CD8K9G3F.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-D1rT702x.js → Dashboard-CXNE434f.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BweXGo1O.js → Dropdown-C_P5mttB.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-B9ShbgJH.js → EmailListRenderer-MxZvItNA.js} +1 -1
- package/src/assets/web-panel/assets/EvolutionSettings-DRoJQrDb.js +1 -0
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DrznZFVs.js → FamilyGuardDashboard-CBrz9T6T.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DQZ2adIA.js → Federation-DKlGgjNs.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DN30Dowh.js → FormItemContext-CteicPS5.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DRdt0j1e.js → GenericCardRenderer-Bs10mDna.js} +1 -1
- package/src/assets/web-panel/assets/{Git-wwRgStzk.js → Git-CXwzFBIh.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CI4Uw0Nh.js → Governance-CYzcwfwf.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-5TuE2AJI.js → Inference-pYpCF1ws.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-oXyvmFSR.js → KnowledgeGraph-B0YJnOpj.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-ewudSWuv.js → Logs-D-hJWfq-.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-Cz5nyufm.js → MarkdownRenderer-BPQlDtB7.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-DcZeWFrs.js → Marketplace-D5ZmI-si.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BcV9p1RS.js → McpTools-DByD6jY1.js} +2 -2
- package/src/assets/web-panel/assets/{Memory-CMhYvugt.js → Memory--lsjJd7Y.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-5RP2uwvT.js → MobileBridge-GQ3vGZO9.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BIuM9Ze4.js → MobileProjects-Dh2wq-NU.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-DmBszTDe.js → Mtc-uQFoRe_Z.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-Czdp5ZGt.js → MtcAudit-BUBYbvuz.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-DRWUuNr-.js → Multisig-D790SLqM.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-IRQQN0-c.js → NLProgramming-DcV9gSqn.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CGhD1lW9.js → Notes-sVl7Y_vt.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BtyPgQAh.js → NotificationSettings-BHRPqBZ3.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-yUaqwCty.js → OrderTableRenderer-B029pIMF.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-C3kTVn5i.js → Organization-BKrF-dtp.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-C2-0iZhq.js → Overflow-DOAGxhhi.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CISoEUxA.js → P2P-7uQ4F7lh.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-NfTVtvKQ.js → PdhVaultBrowser-BegpATva.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-CfK0jJhm.js → Permissions-C0qK0tAi.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-OsVxxh_j.js → PersonalDataHub-DCnfXKyb.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-DEmK_Ch2.js → Pipeline-D8wdVvao.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-D61s73b0.js → Privacy-C-dGmb9y.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-DkLF5gHb.js → ProjectInit-ChKvrsfc.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BA7odTrI.js → ProjectSettings-CaWnzNBi.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-CdI1GcAG.js → Projects-2uNBqS05.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-D9BYyP1r.js → Providers-DTufbN-S.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CXMItUDD.js → QrScannerModal-CGv6QJMC.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-8-tF9ffZ.js → QuickAsk-BfNcUCfG.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-C2QGWOSi.js → Recommend-B0s5YT2K.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-B1M_v1lx.js → RemoteSession-YCs5wa8P.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-D0WZHgv1.js → Reputation-DBC__Ylq.js} +1 -1
- package/src/assets/web-panel/assets/{Row-D6nf9-c-.js → Row-D_NT1ROe.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-ClZ6M-mN.js → RssFeed-Dd9ktNRw.js} +3 -3
- package/src/assets/web-panel/assets/{Search-ZWkt8vuO.js → Search-BsmlT4PY.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BUlQut2m.js → Security-DN-_e3oM.js} +3 -3
- package/src/assets/web-panel/assets/{Services-Cd_3xJOc.js → Services-Bm_KqSDz.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-C8e2-cHW.js → Skeleton-BYldeYI0.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DucVRJp1.js → Skills-h7XAXSls.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-Csr5pUwI.js → Sla-4ArxAaMJ.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-CXvyaRAT.js → SpeechSettings-B639ejZr.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-KdsIcV5h.js → SyncSettings-DR6RRo1O.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-B-eHmJm9.js → Tasks-DysrrJVE.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BglVeaLK.js → Templates-DyyB9L9f.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BBbQ8qan.js → Tenant-XbmWbUl1.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-Ca_pCVBi.js → Terminal-C5GeciJG.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-YD6XG_vZ.js → TimelineRenderer-Db-NjeVy.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-2lRoeVli.js → Tokens-Btm9pYvJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-C-3XNAU9.js → Trigger-BhJ35b0V.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-k-1PxdLG.js → Trust-C5T4GJky.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-FG-qW646.js → UkeySign-CqWxh5U0.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BNOe1cmd.js → VideoEditing-DKyI9zg1.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-D5C3i2Kw.js → Wallet-kgvyQOtT.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-BOpztqd3.js → WebAuthn-1wWucdJt.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-BpVrpUEv.js → WorkflowEditor-rHVIbLME.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DIuMHb38.js → chat-DV8ZtfWQ.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CTFJPyzQ.js → colors-zznFfEpj.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CONfmJcW.js → compact-item-CWk10Qsu.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-RkW5CRrz.js → createContext-CtoMW-kY.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-_Wlo1cZJ.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CW4XOjDC.js → hasIn-ieyHKAZZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Akmg1WdH.js → index--6zQrvYc.js} +1 -1
- package/src/assets/web-panel/assets/{index-BMg2JFmW.js → index-0GSB8LOT.js} +1 -1
- package/src/assets/web-panel/assets/{index-YaeGscSw.js → index-14ADEHo7.js} +1 -1
- package/src/assets/web-panel/assets/{index-7wMSBCU7.js → index-2TTLREp7.js} +1 -1
- package/src/assets/web-panel/assets/{index-asWsB34i.js → index-9fgmdiB_.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bf19fKef.js → index-B2WI4Jse.js} +1 -1
- package/src/assets/web-panel/assets/{index-BFs1Tznc.js → index-B9w6Zr_O.js} +1 -1
- package/src/assets/web-panel/assets/{index-B54qoVff.js → index-BG9KlVJl.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEZWQthY.js → index-BGYCtfL-.js} +1 -1
- package/src/assets/web-panel/assets/{index-YlMnFNOi.js → index-Bf9069uM.js} +1 -1
- package/src/assets/web-panel/assets/{index-DidrMvOx.js → index-BrpyZMUe.js} +1 -1
- package/src/assets/web-panel/assets/{index-CBjb2A5R.js → index-CBS8OtL2.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9XoBoty.js → index-CGZGQTF5.js} +1 -1
- package/src/assets/web-panel/assets/{index-DWY7YwDu.js → index-CLeX8Qp7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DP5pPedy.js → index-CdSnkhrR.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSlXL6Ju.js → index-CewbVIM8.js} +1 -1
- package/src/assets/web-panel/assets/index-Cj8Mcdok.js +1 -0
- package/src/assets/web-panel/assets/{index-gkjiFjQd.js → index-CjkiJdIX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFGLf88d.js → index-Cn71LASJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-D-lpTPS8.js → index-Cqpfzkch.js} +1 -1
- package/src/assets/web-panel/assets/{index-DAESljiJ.js → index-CrBful_u.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cvmc3f25.js → index-CxSgg3Xc.js} +1 -1
- package/src/assets/web-panel/assets/{index-CixqfdRk.js → index-D7yq9nAq.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRZsPxNn.js → index-D9jmNLfO.js} +1 -1
- package/src/assets/web-panel/assets/{index-DWCJnn2O.js → index-DNlvUg0O.js} +1 -1
- package/src/assets/web-panel/assets/{index-D59GbWCN.js → index-DPwOEcUt.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKgaRT0b.js → index-DR1Uw5as.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dj3K5kHs.js → index-DRuSuXJh.js} +1 -1
- package/src/assets/web-panel/assets/{index-CM2X25Er.js → index-DSnQWQ4L.js} +1 -1
- package/src/assets/web-panel/assets/{index-BM7bOUvu.js → index-DdMho2Wr.js} +1 -1
- package/src/assets/web-panel/assets/{index-CLUa68JH.js → index-DjNjO-VL.js} +3 -3
- package/src/assets/web-panel/assets/{index-CkRojEip.js → index-DmtyRKy8.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9aS2VLu.js → index-Dz1TqS34.js} +1 -1
- package/src/assets/web-panel/assets/{index-B8r2t0Ch.js → index-G9bqTRu6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkAbfBF-.js → index-McW2cdbg.js} +1 -1
- package/src/assets/web-panel/assets/{index-CX4T2uKN.js → index-esCpkxIZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DDHUx4Yz.js → index-fmwnODwg.js} +1 -1
- package/src/assets/web-panel/assets/index-vira2aS_.js +1 -0
- package/src/assets/web-panel/assets/{index-DZTwP364.js → index-zY56IXHj.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BOr4igh3.js → initDefaultProps-CVk4LKJK.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CK469n2j.js → motion-IAABGXc_.js} +1 -1
- package/src/assets/web-panel/assets/{move-DzjudlYQ.js → move-B9HSYeZc.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BQaKUxC6.js → mtc-parser-CJfDtM89.js} +1 -1
- package/src/assets/web-panel/assets/{omit-MciovCFD.js → omit-D5dYnom7.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DDPJ4zAz.js → pickAttrs-xbUkaZdY.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-Cznyb-jI.js → placementArrow-CIUU5NKN.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-AJK3SLzJ.js → responsiveObserve-BZpgHE8P.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CPQ28wu7.js → slide-x_8rsSKk.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CFjzvqf_.js → statusUtils-BZyDnVJs.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BPyFfTuR.js → styleChecker-DusfWp6J.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-Dq7N6R05.js → useFlexGapSupport-022JzYAL.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-C3GS0uTc.js → useFs-DRDxX7-3.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BhgIVzP_.js → usePersonalDataHub-DFrCp7m_.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-4ZD9-MDr.js → vnode-bCuSFOmr.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-Bx4MRVNW.js → zoom-sCpZ0sw7.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/data/changelog.json +26 -0
- package/src/lib/agent-sandbox.js +319 -2
- package/src/lib/lsp/code-intelligence.js +7 -1
- package/src/lib/sandbox-docker-egress.js +507 -0
- package/src/lib/sandbox-egress-authority-monitor.js +137 -0
- package/src/lib/sandbox-egress-proxy.js +167 -10
- package/src/lib/sandbox-egress-worker-thread.js +38 -0
- package/src/lib/sandbox-egress-worker.js +196 -0
- package/src/runtime/agent-core.js +211 -25
- package/src/assets/web-panel/assets/EvolutionSettings-D75_iBoc.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DaEBln_q.js +0 -1
- package/src/assets/web-panel/assets/index-DG1mcYTa.js +0 -1
- package/src/assets/web-panel/assets/index-DRs5Pe8u.js +0 -1
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
import http from "node:http";
|
|
19
19
|
import net from "node:net";
|
|
20
20
|
import dns from "node:dns";
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
21
23
|
import {
|
|
22
24
|
evaluateNetworkAccess,
|
|
23
25
|
isPrivateHost,
|
|
@@ -132,15 +134,83 @@ export function proxyEnv(port, host = "127.0.0.1") {
|
|
|
132
134
|
/**
|
|
133
135
|
* Create (but do not yet listen on) an egress-filtering proxy for a policy.
|
|
134
136
|
* @param {object} policy { allowedDomains?, deniedDomains?, allowPrivate? }
|
|
135
|
-
* @param {object} [opts] { onDecision?(info), bindHost? }
|
|
136
|
-
* @returns {{ listen():Promise<{port
|
|
137
|
-
*
|
|
137
|
+
* @param {object} [opts] { onDecision?(info), bindHost?, socketPath? }
|
|
138
|
+
* @returns {{ listen():Promise<{port?,socketPath?,env?,server}>, close():Promise<void>,
|
|
139
|
+
* updatePolicy(nextPolicy:object, expectedRevision:number):number,
|
|
140
|
+
* server:import('http').Server, blocked:number, allowed:number,
|
|
141
|
+
* revision:number }}
|
|
138
142
|
*/
|
|
139
143
|
export function createEgressProxy(policy = {}, opts = {}) {
|
|
140
144
|
const bindHost = opts.bindHost || "127.0.0.1";
|
|
145
|
+
const socketPath = opts.socketPath || null;
|
|
146
|
+
if (socketPath) {
|
|
147
|
+
if (
|
|
148
|
+
process.platform !== "linux" ||
|
|
149
|
+
!path.isAbsolute(socketPath) ||
|
|
150
|
+
Buffer.byteLength(socketPath) > 107
|
|
151
|
+
) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
"A short Linux absolute path is required for the egress broker socket",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const parent = path.dirname(socketPath);
|
|
157
|
+
const stat = fs.lstatSync(parent);
|
|
158
|
+
if (
|
|
159
|
+
!stat.isDirectory() ||
|
|
160
|
+
stat.mode & 0o077 ||
|
|
161
|
+
stat.uid !== process.getuid()
|
|
162
|
+
) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
"The egress broker socket requires an owner-only directory",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
141
168
|
const state = { blocked: 0, allowed: 0 };
|
|
169
|
+
const snapshot = (value) => {
|
|
170
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
171
|
+
throw new TypeError("Egress policy must be an object");
|
|
172
|
+
}
|
|
173
|
+
for (const key of ["allowedDomains", "deniedDomains"]) {
|
|
174
|
+
if (
|
|
175
|
+
value[key] !== undefined &&
|
|
176
|
+
(!Array.isArray(value[key]) ||
|
|
177
|
+
value[key].some((item) => typeof item !== "string"))
|
|
178
|
+
) {
|
|
179
|
+
throw new TypeError(`${key} must be an array of strings`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (
|
|
183
|
+
value.allowPrivate !== undefined &&
|
|
184
|
+
typeof value.allowPrivate !== "boolean"
|
|
185
|
+
) {
|
|
186
|
+
throw new TypeError("allowPrivate must be a boolean");
|
|
187
|
+
}
|
|
188
|
+
return Object.freeze({
|
|
189
|
+
allowedDomains: Object.freeze([...(value.allowedDomains || [])]),
|
|
190
|
+
deniedDomains: Object.freeze([...(value.deniedDomains || [])]),
|
|
191
|
+
allowPrivate: value.allowPrivate === true,
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
let currentPolicy = snapshot(policy);
|
|
195
|
+
let revision = 0;
|
|
196
|
+
let closed = false;
|
|
197
|
+
const active = new Set();
|
|
198
|
+
const connections = new Set();
|
|
199
|
+
const register = (client) => {
|
|
200
|
+
const lease = { revision, client, upstream: null, cancelled: false };
|
|
201
|
+
active.add(lease);
|
|
202
|
+
return lease;
|
|
203
|
+
};
|
|
204
|
+
const revoke = (lease) => {
|
|
205
|
+
lease.cancelled = true;
|
|
206
|
+
lease.upstream?.destroy();
|
|
207
|
+
lease.client.destroy();
|
|
208
|
+
active.delete(lease);
|
|
209
|
+
};
|
|
210
|
+
const live = (lease) =>
|
|
211
|
+
!closed && !lease.cancelled && lease.revision === revision;
|
|
142
212
|
const decide = (target, kind) => {
|
|
143
|
-
const verdict = evaluateNetworkAccess(target,
|
|
213
|
+
const verdict = evaluateNetworkAccess(target, currentPolicy);
|
|
144
214
|
if (verdict.allowed) state.allowed += 1;
|
|
145
215
|
else state.blocked += 1;
|
|
146
216
|
if (typeof opts.onDecision === "function") {
|
|
@@ -162,11 +232,11 @@ export function createEgressProxy(policy = {}, opts = {}) {
|
|
|
162
232
|
// exact address to pin the connection to (null → connect by the original
|
|
163
233
|
// name). When the resolved IP is private the name-based allow is overturned:
|
|
164
234
|
// fix the counters and re-notify observers with the block reason.
|
|
165
|
-
const guard = async (verdict, kind) => {
|
|
235
|
+
const guard = async (verdict, kind, policySnapshot) => {
|
|
166
236
|
if (!verdict.allowed || verdict.specific || !rebindGuard) {
|
|
167
237
|
return { verdict, ip: null };
|
|
168
238
|
}
|
|
169
|
-
const res = await guardResolvedTarget(verdict.host,
|
|
239
|
+
const res = await guardResolvedTarget(verdict.host, policySnapshot, lookup);
|
|
170
240
|
if (res.ok) return { verdict, ip: res.ip };
|
|
171
241
|
state.allowed -= 1;
|
|
172
242
|
state.blocked += 1;
|
|
@@ -182,9 +252,20 @@ export function createEgressProxy(policy = {}, opts = {}) {
|
|
|
182
252
|
};
|
|
183
253
|
|
|
184
254
|
const server = http.createServer(async (req, res) => {
|
|
255
|
+
const lease = register(res);
|
|
256
|
+
res.once("close", () => {
|
|
257
|
+
active.delete(lease);
|
|
258
|
+
if (!res.writableEnded) lease.upstream?.destroy();
|
|
259
|
+
});
|
|
185
260
|
try {
|
|
186
261
|
// A forward-proxy request carries the absolute URL in the request line.
|
|
187
|
-
const
|
|
262
|
+
const policySnapshot = currentPolicy;
|
|
263
|
+
const { verdict, ip } = await guard(
|
|
264
|
+
decide(req.url, "http"),
|
|
265
|
+
"http",
|
|
266
|
+
policySnapshot,
|
|
267
|
+
);
|
|
268
|
+
if (!live(lease)) return;
|
|
188
269
|
if (!verdict.allowed) {
|
|
189
270
|
res.writeHead(403, { "content-type": "text/plain" });
|
|
190
271
|
res.end(`egress blocked: ${verdict.host} — ${verdict.reason}\n`);
|
|
@@ -198,26 +279,40 @@ export function createEgressProxy(policy = {}, opts = {}) {
|
|
|
198
279
|
res.end("bad request target\n");
|
|
199
280
|
return;
|
|
200
281
|
}
|
|
282
|
+
if (u.protocol !== "http:" || u.username || u.password) {
|
|
283
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
284
|
+
res.end("unsupported proxy target\n");
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
201
287
|
const upstream = http.request(
|
|
202
288
|
{
|
|
203
289
|
hostname: ip || u.hostname, // pin to the validated IP when re-checked
|
|
204
290
|
port: u.port || 80,
|
|
205
291
|
path: `${u.pathname}${u.search}`,
|
|
206
292
|
method: req.method,
|
|
207
|
-
|
|
293
|
+
// The URL is the policy authority. Do not forward a conflicting
|
|
294
|
+
// client Host header to a different virtual host on the allowed IP.
|
|
295
|
+
headers: { ...req.headers, host: u.host },
|
|
208
296
|
},
|
|
209
297
|
(upRes) => {
|
|
298
|
+
if (!live(lease)) {
|
|
299
|
+
upRes.destroy();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
210
302
|
res.writeHead(upRes.statusCode || 502, upRes.headers);
|
|
211
303
|
upRes.pipe(res);
|
|
212
304
|
},
|
|
213
305
|
);
|
|
306
|
+
lease.upstream = upstream;
|
|
214
307
|
upstream.on("error", () => {
|
|
308
|
+
if (!live(lease)) return;
|
|
215
309
|
if (!res.headersSent)
|
|
216
310
|
res.writeHead(502, { "content-type": "text/plain" });
|
|
217
311
|
res.end("upstream error\n");
|
|
218
312
|
});
|
|
219
313
|
req.pipe(upstream);
|
|
220
314
|
} catch {
|
|
315
|
+
if (!live(lease)) return;
|
|
221
316
|
if (!res.headersSent)
|
|
222
317
|
res.writeHead(502, { "content-type": "text/plain" });
|
|
223
318
|
res.end("proxy error\n");
|
|
@@ -226,11 +321,19 @@ export function createEgressProxy(policy = {}, opts = {}) {
|
|
|
226
321
|
|
|
227
322
|
// HTTPS (and any TLS) goes through CONNECT — tunnel only allowed hosts.
|
|
228
323
|
server.on("connect", async (req, clientSocket, head) => {
|
|
324
|
+
const lease = register(clientSocket);
|
|
325
|
+
clientSocket.once("close", () => {
|
|
326
|
+
active.delete(lease);
|
|
327
|
+
lease.upstream?.destroy();
|
|
328
|
+
});
|
|
229
329
|
try {
|
|
330
|
+
const policySnapshot = currentPolicy;
|
|
230
331
|
const { verdict, ip } = await guard(
|
|
231
332
|
decide(req.url, "connect"),
|
|
232
333
|
"connect",
|
|
334
|
+
policySnapshot,
|
|
233
335
|
); // req.url = "host:port"
|
|
336
|
+
if (!live(lease)) return;
|
|
234
337
|
if (!verdict.allowed) {
|
|
235
338
|
clientSocket.write(
|
|
236
339
|
"HTTP/1.1 403 Forbidden\r\n" +
|
|
@@ -245,11 +348,16 @@ export function createEgressProxy(policy = {}, opts = {}) {
|
|
|
245
348
|
// Connect to the pinned validated IP so a rebind at connect time can't
|
|
246
349
|
// swap in a private address between our check and the socket.
|
|
247
350
|
const upstream = net.connect(port, ip || host, () => {
|
|
351
|
+
if (!live(lease)) {
|
|
352
|
+
upstream.destroy();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
248
355
|
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
249
356
|
if (head && head.length) upstream.write(head);
|
|
250
357
|
upstream.pipe(clientSocket);
|
|
251
358
|
clientSocket.pipe(upstream);
|
|
252
359
|
});
|
|
360
|
+
lease.upstream = upstream;
|
|
253
361
|
upstream.on("error", () => {
|
|
254
362
|
try {
|
|
255
363
|
clientSocket.end();
|
|
@@ -265,6 +373,7 @@ export function createEgressProxy(policy = {}, opts = {}) {
|
|
|
265
373
|
}
|
|
266
374
|
});
|
|
267
375
|
} catch {
|
|
376
|
+
if (!live(lease)) return;
|
|
268
377
|
try {
|
|
269
378
|
clientSocket.end();
|
|
270
379
|
} catch {
|
|
@@ -273,6 +382,16 @@ export function createEgressProxy(policy = {}, opts = {}) {
|
|
|
273
382
|
}
|
|
274
383
|
});
|
|
275
384
|
|
|
385
|
+
// Raw HTTP Upgrade is not a governed tunnel. WebSocket clients may use a
|
|
386
|
+
// CONNECT tunnel, which remains registered above for revision revocation.
|
|
387
|
+
server.on("upgrade", (_req, socket) => {
|
|
388
|
+
socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
389
|
+
});
|
|
390
|
+
server.on("connection", (socket) => {
|
|
391
|
+
connections.add(socket);
|
|
392
|
+
socket.once("close", () => connections.delete(socket));
|
|
393
|
+
});
|
|
394
|
+
|
|
276
395
|
return {
|
|
277
396
|
server,
|
|
278
397
|
get blocked() {
|
|
@@ -281,17 +400,55 @@ export function createEgressProxy(policy = {}, opts = {}) {
|
|
|
281
400
|
get allowed() {
|
|
282
401
|
return state.allowed;
|
|
283
402
|
},
|
|
403
|
+
get revision() {
|
|
404
|
+
return revision;
|
|
405
|
+
},
|
|
406
|
+
updatePolicy(nextPolicy, expectedRevision) {
|
|
407
|
+
if (closed || expectedRevision !== revision) {
|
|
408
|
+
const error = new Error("Stale or closed egress policy revision");
|
|
409
|
+
error.code = "ERR_EGRESS_POLICY_REVISION";
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
const next = snapshot(nextPolicy);
|
|
413
|
+
currentPolicy = next;
|
|
414
|
+
revision += 1;
|
|
415
|
+
// A new revision has no inherited connection authority. Closing every
|
|
416
|
+
// existing lease also covers in-flight DNS and HTTP connect callbacks.
|
|
417
|
+
for (const lease of active) revoke(lease);
|
|
418
|
+
return revision;
|
|
419
|
+
},
|
|
284
420
|
listen() {
|
|
285
421
|
return new Promise((resolve, reject) => {
|
|
422
|
+
if (closed) {
|
|
423
|
+
reject(new Error("Egress proxy is closed"));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
286
426
|
server.once("error", reject);
|
|
287
|
-
|
|
427
|
+
const onListening = () => {
|
|
288
428
|
server.removeListener("error", reject);
|
|
429
|
+
if (socketPath) {
|
|
430
|
+
try {
|
|
431
|
+
fs.chmodSync(socketPath, 0o600);
|
|
432
|
+
} catch (error) {
|
|
433
|
+
server.close();
|
|
434
|
+
reject(error);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
resolve({ socketPath, server });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
289
440
|
const port = server.address().port;
|
|
290
441
|
resolve({ port, env: proxyEnv(port), server });
|
|
291
|
-
}
|
|
442
|
+
};
|
|
443
|
+
if (socketPath) server.listen(socketPath, onListening);
|
|
444
|
+
else server.listen(0, bindHost, onListening);
|
|
292
445
|
});
|
|
293
446
|
},
|
|
294
447
|
close() {
|
|
448
|
+
closed = true;
|
|
449
|
+
for (const lease of active) revoke(lease);
|
|
450
|
+
for (const socket of connections) socket.destroy();
|
|
451
|
+
if (!server.listening) return Promise.resolve();
|
|
295
452
|
return new Promise((resolve) => server.close(() => resolve()));
|
|
296
453
|
},
|
|
297
454
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
2
|
+
import { createEgressProxy } from "./sandbox-egress-proxy.js";
|
|
3
|
+
|
|
4
|
+
let proxy;
|
|
5
|
+
try {
|
|
6
|
+
proxy = createEgressProxy(workerData.policy, {
|
|
7
|
+
bindHost: workerData.bindHost,
|
|
8
|
+
socketPath: workerData.socketPath,
|
|
9
|
+
});
|
|
10
|
+
const { port, socketPath } = await proxy.listen();
|
|
11
|
+
parentPort.postMessage({ type: "ready", port, socketPath });
|
|
12
|
+
parentPort.on("message", async (message) => {
|
|
13
|
+
if (!Number.isSafeInteger(message?.id) || message.id < 1) return;
|
|
14
|
+
try {
|
|
15
|
+
if (message.type === "update") {
|
|
16
|
+
const revision = proxy.updatePolicy(
|
|
17
|
+
message.policy,
|
|
18
|
+
message.expectedRevision,
|
|
19
|
+
);
|
|
20
|
+
parentPort.postMessage({ id: message.id, revision });
|
|
21
|
+
} else if (message.type === "close") {
|
|
22
|
+
await proxy.close();
|
|
23
|
+
parentPort.postMessage({ id: message.id, closed: true });
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
parentPort.postMessage({
|
|
27
|
+
id: message.id,
|
|
28
|
+
errorCode:
|
|
29
|
+
error?.code === "ERR_EGRESS_POLICY_REVISION"
|
|
30
|
+
? error.code
|
|
31
|
+
: "ERR_EGRESS_WORKER_REQUEST",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
} catch {
|
|
36
|
+
parentPort.postMessage({ type: "startup-failed" });
|
|
37
|
+
parentPort.close();
|
|
38
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/** Run the policy proxy on a separate event loop so a synchronous shell wait
|
|
2
|
+
* cannot stall HTTP/CONNECT decisions. This is a host-side building block;
|
|
3
|
+
* callers still need a kernel-enforced network route before allowing a
|
|
4
|
+
* domain-restricted sandbox command to start.
|
|
5
|
+
*/
|
|
6
|
+
import { Worker } from "node:worker_threads";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
const RESPONSE_TIMEOUT_MS = 10_000;
|
|
10
|
+
|
|
11
|
+
function workerError(code) {
|
|
12
|
+
const error = new Error(code);
|
|
13
|
+
error.code = code;
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function startEgressProxyWorker(policy, options = {}) {
|
|
18
|
+
const bindHost = options.bindHost || "127.0.0.1";
|
|
19
|
+
const socketPath = options.socketPath || null;
|
|
20
|
+
if (socketPath) {
|
|
21
|
+
if (process.platform !== "linux" || !path.isAbsolute(socketPath)) {
|
|
22
|
+
throw workerError("ERR_EGRESS_WORKER_SOCKET_PATH");
|
|
23
|
+
}
|
|
24
|
+
} else if (bindHost !== "127.0.0.1" && bindHost !== "::1") {
|
|
25
|
+
throw workerError("ERR_EGRESS_WORKER_BIND_HOST");
|
|
26
|
+
}
|
|
27
|
+
const timeoutMs = options.timeoutMs || RESPONSE_TIMEOUT_MS;
|
|
28
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
|
|
29
|
+
throw workerError("ERR_EGRESS_WORKER_TIMEOUT");
|
|
30
|
+
}
|
|
31
|
+
const worker = new Worker(
|
|
32
|
+
new URL("./sandbox-egress-worker-thread.js", import.meta.url),
|
|
33
|
+
{ workerData: { policy, bindHost, socketPath } },
|
|
34
|
+
);
|
|
35
|
+
const pending = new Map();
|
|
36
|
+
let nextId = 1;
|
|
37
|
+
let revision = 0;
|
|
38
|
+
let closed = false;
|
|
39
|
+
let terminalError = null;
|
|
40
|
+
let expectedTermination = false;
|
|
41
|
+
let terminationPromise = null;
|
|
42
|
+
let shutdownPromise = null;
|
|
43
|
+
let resolveReady;
|
|
44
|
+
let rejectReady;
|
|
45
|
+
const ready = new Promise((resolve, reject) => {
|
|
46
|
+
resolveReady = resolve;
|
|
47
|
+
rejectReady = reject;
|
|
48
|
+
});
|
|
49
|
+
const startupTimer = setTimeout(() => {
|
|
50
|
+
fail(workerError("ERR_EGRESS_WORKER_START_TIMEOUT"));
|
|
51
|
+
void terminateWorker();
|
|
52
|
+
}, timeoutMs);
|
|
53
|
+
|
|
54
|
+
function terminateWorker() {
|
|
55
|
+
if (!terminationPromise) {
|
|
56
|
+
expectedTermination = true;
|
|
57
|
+
terminationPromise = worker.terminate();
|
|
58
|
+
}
|
|
59
|
+
return terminationPromise;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fail(error) {
|
|
63
|
+
if (terminalError) return;
|
|
64
|
+
terminalError = error;
|
|
65
|
+
clearTimeout(startupTimer);
|
|
66
|
+
rejectReady(error);
|
|
67
|
+
for (const entry of pending.values()) {
|
|
68
|
+
clearTimeout(entry.timer);
|
|
69
|
+
entry.reject(error);
|
|
70
|
+
}
|
|
71
|
+
pending.clear();
|
|
72
|
+
if (!closed) {
|
|
73
|
+
try {
|
|
74
|
+
options.onFailure?.(error);
|
|
75
|
+
} catch {
|
|
76
|
+
// A diagnostic observer cannot keep a failed proxy alive.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
worker.on("message", (message) => {
|
|
82
|
+
if (message?.type === "startup-failed") {
|
|
83
|
+
fail(workerError("ERR_EGRESS_WORKER_START_FAILED"));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (message?.type === "ready") {
|
|
87
|
+
const validEndpoint = socketPath
|
|
88
|
+
? message.socketPath === socketPath && message.port === undefined
|
|
89
|
+
: Number.isSafeInteger(message.port) &&
|
|
90
|
+
message.port >= 1 &&
|
|
91
|
+
message.port <= 65535;
|
|
92
|
+
if (!validEndpoint) {
|
|
93
|
+
fail(workerError("ERR_EGRESS_WORKER_PROTOCOL"));
|
|
94
|
+
void terminateWorker();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
clearTimeout(startupTimer);
|
|
98
|
+
resolveReady(socketPath ? socketPath : message.port);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const entry = pending.get(message?.id);
|
|
102
|
+
if (!entry) return;
|
|
103
|
+
pending.delete(message.id);
|
|
104
|
+
clearTimeout(entry.timer);
|
|
105
|
+
if (message.errorCode) {
|
|
106
|
+
entry.reject(
|
|
107
|
+
workerError(
|
|
108
|
+
message.errorCode === "ERR_EGRESS_POLICY_REVISION"
|
|
109
|
+
? message.errorCode
|
|
110
|
+
: "ERR_EGRESS_WORKER_REQUEST",
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
entry.resolve(message);
|
|
116
|
+
});
|
|
117
|
+
worker.on("error", () => fail(workerError("ERR_EGRESS_WORKER_FAILURE")));
|
|
118
|
+
worker.on("exit", () => {
|
|
119
|
+
if (!expectedTermination) fail(workerError("ERR_EGRESS_WORKER_EXIT"));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
function request(type, fields = {}) {
|
|
123
|
+
if (terminalError) return Promise.reject(terminalError);
|
|
124
|
+
if (closed && type !== "close") {
|
|
125
|
+
return Promise.reject(workerError("ERR_EGRESS_WORKER_CLOSED"));
|
|
126
|
+
}
|
|
127
|
+
const id = nextId++;
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const timer = setTimeout(() => {
|
|
130
|
+
const error = workerError("ERR_EGRESS_WORKER_RESPONSE_TIMEOUT");
|
|
131
|
+
fail(error);
|
|
132
|
+
void terminateWorker();
|
|
133
|
+
}, timeoutMs);
|
|
134
|
+
pending.set(id, { resolve, reject, timer });
|
|
135
|
+
try {
|
|
136
|
+
worker.postMessage({ id, type, ...fields });
|
|
137
|
+
} catch {
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
pending.delete(id);
|
|
140
|
+
reject(workerError("ERR_EGRESS_WORKER_REQUEST"));
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let endpoint;
|
|
146
|
+
try {
|
|
147
|
+
endpoint = await ready;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
await terminateWorker();
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
port: socketPath ? null : endpoint,
|
|
154
|
+
socketPath: socketPath ? endpoint : null,
|
|
155
|
+
get revision() {
|
|
156
|
+
return revision;
|
|
157
|
+
},
|
|
158
|
+
async updatePolicy(nextPolicy, expectedRevision) {
|
|
159
|
+
const response = await request("update", {
|
|
160
|
+
policy: nextPolicy,
|
|
161
|
+
expectedRevision,
|
|
162
|
+
});
|
|
163
|
+
if (!Number.isSafeInteger(response.revision)) {
|
|
164
|
+
fail(workerError("ERR_EGRESS_WORKER_PROTOCOL"));
|
|
165
|
+
void terminateWorker();
|
|
166
|
+
throw terminalError;
|
|
167
|
+
}
|
|
168
|
+
revision = response.revision;
|
|
169
|
+
return revision;
|
|
170
|
+
},
|
|
171
|
+
close() {
|
|
172
|
+
if (!shutdownPromise) {
|
|
173
|
+
closed = true;
|
|
174
|
+
shutdownPromise = (async () => {
|
|
175
|
+
try {
|
|
176
|
+
if (!terminalError) await request("close");
|
|
177
|
+
} finally {
|
|
178
|
+
await terminateWorker();
|
|
179
|
+
}
|
|
180
|
+
})();
|
|
181
|
+
}
|
|
182
|
+
return shutdownPromise;
|
|
183
|
+
},
|
|
184
|
+
abort() {
|
|
185
|
+
if (!shutdownPromise) {
|
|
186
|
+
fail(workerError("ERR_EGRESS_WORKER_ABORTED"));
|
|
187
|
+
closed = true;
|
|
188
|
+
shutdownPromise = terminateWorker().then(() => {});
|
|
189
|
+
} else if (!expectedTermination) {
|
|
190
|
+
fail(workerError("ERR_EGRESS_WORKER_ABORTED"));
|
|
191
|
+
void terminateWorker();
|
|
192
|
+
}
|
|
193
|
+
return shutdownPromise;
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|