agent-shim 8.20.2 → 8.21.0
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/dist/cli.cjs +365 -48
- package/dist/index.cjs +344 -12
- package/dist/index.mjs +336 -12
- package/dist/types/index.d.ts +92 -10
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -107,6 +107,10 @@ __export(index_exports, {
|
|
|
107
107
|
RC_SELF_HOST_WORKER_JWT_TTL_SECONDS: () => RC_SELF_HOST_WORKER_JWT_TTL_SECONDS,
|
|
108
108
|
RC_SESSIONS_PATH_PREFIX: () => RC_SESSIONS_PATH_PREFIX,
|
|
109
109
|
RC_STREAM_BACKOFF_MS: () => RC_STREAM_BACKOFF_MS,
|
|
110
|
+
RC_WEB_FETCH_ALLOW_PRIVATE_ENV: () => RC_WEB_FETCH_ALLOW_PRIVATE_ENV,
|
|
111
|
+
RC_WEB_FETCH_DEADLINE_MS: () => RC_WEB_FETCH_DEADLINE_MS,
|
|
112
|
+
RC_WEB_FETCH_MAX_BYTES: () => RC_WEB_FETCH_MAX_BYTES,
|
|
113
|
+
RC_WEB_FETCH_MAX_REDIRECTS: () => RC_WEB_FETCH_MAX_REDIRECTS,
|
|
110
114
|
ROUTED_PATH_PREFIX: () => ROUTED_PATH_PREFIX,
|
|
111
115
|
RcPermissionModeSchema: () => RcPermissionModeSchema,
|
|
112
116
|
RcStreamEnvelopeSchema: () => RcStreamEnvelopeSchema,
|
|
@@ -165,6 +169,7 @@ __export(index_exports, {
|
|
|
165
169
|
ensureHeadroom: () => ensureHeadroom,
|
|
166
170
|
evaluateAmbientCredentialGuard: () => evaluateAmbientCredentialGuard,
|
|
167
171
|
evaluateWhen: () => evaluateWhen,
|
|
172
|
+
fetchThroughWebProxy: () => fetchThroughWebProxy,
|
|
168
173
|
formatAmbientCredentialGuardMessage: () => formatAmbientCredentialGuardMessage,
|
|
169
174
|
formatCheckReport: () => formatCheckReport,
|
|
170
175
|
formatDoctorReport: () => formatDoctorReport,
|
|
@@ -181,6 +186,7 @@ __export(index_exports, {
|
|
|
181
186
|
interruptRcSession: () => interruptRcSession,
|
|
182
187
|
isIdentityDirectoryName: () => isIdentityDirectoryName,
|
|
183
188
|
isInterceptedHost: () => isInterceptedHost,
|
|
189
|
+
isPrivateAddress: () => isPrivateAddress,
|
|
184
190
|
isRcPermissionMode: () => isRcPermissionMode,
|
|
185
191
|
lateRcEventDial: () => lateRcEventDial,
|
|
186
192
|
lateRcStreamDial: () => lateRcStreamDial,
|
|
@@ -205,6 +211,7 @@ __export(index_exports, {
|
|
|
205
211
|
rcFanoutOnDoorHub: () => rcFanoutOnDoorHub,
|
|
206
212
|
rcObserverAsPassthrough: () => rcObserverAsPassthrough,
|
|
207
213
|
rcSelfHostFromEnv: () => rcSelfHostFromEnv,
|
|
214
|
+
rcWebFetchAllowsPrivateFromEnv: () => rcWebFetchAllowsPrivateFromEnv,
|
|
208
215
|
readActiveIdentity: () => readActiveIdentity,
|
|
209
216
|
readDirectoryRules: () => readDirectoryRules,
|
|
210
217
|
readFarmManifest: () => readFarmManifest,
|
|
@@ -220,6 +227,7 @@ __export(index_exports, {
|
|
|
220
227
|
realRcControlTransport: () => realRcControlTransport,
|
|
221
228
|
realRcEventDial: () => realRcEventDial,
|
|
222
229
|
realRcStreamDial: () => realRcStreamDial,
|
|
230
|
+
realRcWebFetch: () => realRcWebFetch,
|
|
223
231
|
recoverFarm: () => recoverFarm,
|
|
224
232
|
recoveryDiagnostics: () => recoveryDiagnostics,
|
|
225
233
|
removeDirectoryRule: () => removeDirectoryRule,
|
|
@@ -2983,22 +2991,22 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
|
|
|
2983
2991
|
};
|
|
2984
2992
|
writeFrontDoorState(fs12, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, ...sticky() });
|
|
2985
2993
|
const previousSticky = lastPort;
|
|
2986
|
-
let
|
|
2994
|
+
let http3;
|
|
2987
2995
|
try {
|
|
2988
|
-
|
|
2996
|
+
http3 = await ports.startProviderListener(lastPort);
|
|
2989
2997
|
} catch (error) {
|
|
2990
2998
|
return fail(`could not start the provider listener: ${error instanceof Error ? error.message : String(error)}`);
|
|
2991
2999
|
}
|
|
2992
|
-
if (previousSticky !== void 0 &&
|
|
2993
|
-
ports.log(`agent-shim frontdoor supervisor: sticky port ${String(previousSticky)} was occupied; moving to ${String(
|
|
3000
|
+
if (previousSticky !== void 0 && http3.port !== previousSticky) {
|
|
3001
|
+
ports.log(`agent-shim frontdoor supervisor: sticky port ${String(previousSticky)} was occupied; moving to ${String(http3.port)} (sessions launched against the old port are stale until they relaunch)`);
|
|
2994
3002
|
}
|
|
2995
|
-
lastPort =
|
|
3003
|
+
lastPort = http3.port;
|
|
2996
3004
|
const previousStickyConnect = lastConnectPort;
|
|
2997
3005
|
let connect3;
|
|
2998
3006
|
try {
|
|
2999
3007
|
connect3 = await ports.startConnectListener(lastConnectPort);
|
|
3000
3008
|
} catch (error) {
|
|
3001
|
-
await
|
|
3009
|
+
await http3.close();
|
|
3002
3010
|
return fail(`could not start the CONNECT surface: ${error instanceof Error ? error.message : String(error)}`);
|
|
3003
3011
|
}
|
|
3004
3012
|
if (previousStickyConnect !== void 0 && connect3.port !== previousStickyConnect) {
|
|
@@ -3011,16 +3019,16 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
|
|
|
3011
3019
|
direct = await ports.startDirectListener(lastDirectPort);
|
|
3012
3020
|
} catch (error) {
|
|
3013
3021
|
await connect3.close();
|
|
3014
|
-
await
|
|
3022
|
+
await http3.close();
|
|
3015
3023
|
return fail(`could not start the direct listener: ${error instanceof Error ? error.message : String(error)}`);
|
|
3016
3024
|
}
|
|
3017
3025
|
if (previousStickyDirect !== void 0 && direct.port !== previousStickyDirect) {
|
|
3018
3026
|
ports.log(`agent-shim frontdoor supervisor: sticky direct port ${String(previousStickyDirect)} was occupied; moving to ${String(direct.port)} (headroom must be restarted to admit the new address)`);
|
|
3019
3027
|
}
|
|
3020
3028
|
lastDirectPort = direct.port;
|
|
3021
|
-
writeFrontDoorState(fs12, paths.frontdoorStateFile, { protocol: FRONT_DOOR_PROTOCOL, supervisorPid: ports.ownPid, port:
|
|
3029
|
+
writeFrontDoorState(fs12, paths.frontdoorStateFile, { protocol: FRONT_DOOR_PROTOCOL, supervisorPid: ports.ownPid, port: http3.port, connectPort: connect3.port, directPort: direct.port, ...sticky() });
|
|
3022
3030
|
fs12.removeRecursive(paths.frontdoorLockFile);
|
|
3023
|
-
ports.log(`agent-shim frontdoor supervisor ${String(ports.ownPid)}: front door on 127.0.0.1:${String(
|
|
3031
|
+
ports.log(`agent-shim frontdoor supervisor ${String(ports.ownPid)}: front door on 127.0.0.1:${String(http3.port)}, CONNECT surface on 127.0.0.1:${String(connect3.port)}`);
|
|
3024
3032
|
let idleSince;
|
|
3025
3033
|
let ticks = 0;
|
|
3026
3034
|
for (; ; ) {
|
|
@@ -3038,7 +3046,7 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
|
|
|
3038
3046
|
ports.log(`agent-shim frontdoor supervisor: no sessions for ${String(idleShutdownMinutes)} minute(s); closing the front door and exiting`);
|
|
3039
3047
|
await direct.close();
|
|
3040
3048
|
await connect3.close();
|
|
3041
|
-
await
|
|
3049
|
+
await http3.close();
|
|
3042
3050
|
ports.closeRcStreamHub();
|
|
3043
3051
|
writeFrontDoorState(fs12, paths.frontdoorStateFile, sticky());
|
|
3044
3052
|
return 0;
|
|
@@ -10252,6 +10260,252 @@ function createRcStreamHub(deps) {
|
|
|
10252
10260
|
};
|
|
10253
10261
|
}
|
|
10254
10262
|
|
|
10263
|
+
// src/frontdoor/rcWebFetch.ts
|
|
10264
|
+
var dns2 = __toESM(require("node:dns"), 1);
|
|
10265
|
+
var http2 = __toESM(require("node:http"), 1);
|
|
10266
|
+
var https3 = __toESM(require("node:https"), 1);
|
|
10267
|
+
var net3 = __toESM(require("node:net"), 1);
|
|
10268
|
+
var RC_WEB_FETCH_MAX_BYTES = 12582912;
|
|
10269
|
+
var RC_WEB_FETCH_DEADLINE_MS = 4e4;
|
|
10270
|
+
var RC_WEB_FETCH_MAX_REDIRECTS = 20;
|
|
10271
|
+
var RC_WEB_FETCH_ALLOW_PRIVATE_ENV = "AGENT_SHIM_FRONTDOOR_RC_SELF_HOST_WEB_FETCH_ALLOW_PRIVATE";
|
|
10272
|
+
function rcWebFetchAllowsPrivateFromEnv(env) {
|
|
10273
|
+
return env[RC_WEB_FETCH_ALLOW_PRIVATE_ENV] === "1";
|
|
10274
|
+
}
|
|
10275
|
+
var WEB_FETCH_USER_AGENT = "agent-shim-frontdoor";
|
|
10276
|
+
var ERROR_PREFIX = "web_fetch_";
|
|
10277
|
+
var V4_THIS_NETWORK_OCTET = 0;
|
|
10278
|
+
var V4_PRIVATE_A_OCTET = 10;
|
|
10279
|
+
var V4_LOOPBACK_OCTET = 127;
|
|
10280
|
+
var V4_CGNAT_OCTET = 100;
|
|
10281
|
+
var V4_CGNAT_SECOND_FROM = 64;
|
|
10282
|
+
var V4_CGNAT_SECOND_TO = 127;
|
|
10283
|
+
var V4_LINK_LOCAL_OCTET = 169;
|
|
10284
|
+
var V4_LINK_LOCAL_SECOND = 254;
|
|
10285
|
+
var V4_PRIVATE_B_OCTET = 172;
|
|
10286
|
+
var V4_PRIVATE_B_SECOND_FROM = 16;
|
|
10287
|
+
var V4_PRIVATE_B_SECOND_TO = 31;
|
|
10288
|
+
var V4_PRIVATE_C_OCTET = 192;
|
|
10289
|
+
var V4_PRIVATE_C_SECOND = 168;
|
|
10290
|
+
var V4_MULTICAST_FROM_OCTET = 224;
|
|
10291
|
+
var V6_UNIQUE_LOCAL_FROM = 64512;
|
|
10292
|
+
var V6_UNIQUE_LOCAL_TO = 65023;
|
|
10293
|
+
var V6_LINK_LOCAL_FROM = 65152;
|
|
10294
|
+
var V6_LINK_LOCAL_TO = 65215;
|
|
10295
|
+
var V6_MULTICAST_FROM = 65280;
|
|
10296
|
+
var IP_VERSION_4 = 4;
|
|
10297
|
+
var IP_VERSION_6 = 6;
|
|
10298
|
+
var HTTP_MOVED_PERMANENTLY = 301;
|
|
10299
|
+
var HTTP_FOUND = 302;
|
|
10300
|
+
var HTTP_SEE_OTHER = 303;
|
|
10301
|
+
var HTTP_TEMPORARY_REDIRECT = 307;
|
|
10302
|
+
var HTTP_PERMANENT_REDIRECT = 308;
|
|
10303
|
+
var SUCCESS_STATUS_MAX_EXCLUSIVE4 = 300;
|
|
10304
|
+
function refused(errorType, errorMessage2) {
|
|
10305
|
+
return { kind: "refused", errorType: `${ERROR_PREFIX}${errorType}`, errorMessage: errorMessage2 };
|
|
10306
|
+
}
|
|
10307
|
+
function isPrivateAddress(address) {
|
|
10308
|
+
const version = net3.isIP(address);
|
|
10309
|
+
if (version === IP_VERSION_4) {
|
|
10310
|
+
const octets = address.split(".").map((part) => Number(part));
|
|
10311
|
+
const first = octets[0] ?? -1;
|
|
10312
|
+
const second = octets[1] ?? -1;
|
|
10313
|
+
if (first === V4_THIS_NETWORK_OCTET || first === V4_PRIVATE_A_OCTET || first === V4_LOOPBACK_OCTET) {
|
|
10314
|
+
return true;
|
|
10315
|
+
}
|
|
10316
|
+
if (first === V4_CGNAT_OCTET && second >= V4_CGNAT_SECOND_FROM && second <= V4_CGNAT_SECOND_TO) {
|
|
10317
|
+
return true;
|
|
10318
|
+
}
|
|
10319
|
+
if (first === V4_LINK_LOCAL_OCTET && second === V4_LINK_LOCAL_SECOND) {
|
|
10320
|
+
return true;
|
|
10321
|
+
}
|
|
10322
|
+
if (first === V4_PRIVATE_B_OCTET && second >= V4_PRIVATE_B_SECOND_FROM && second <= V4_PRIVATE_B_SECOND_TO) {
|
|
10323
|
+
return true;
|
|
10324
|
+
}
|
|
10325
|
+
if (first === V4_PRIVATE_C_OCTET && second === V4_PRIVATE_C_SECOND) {
|
|
10326
|
+
return true;
|
|
10327
|
+
}
|
|
10328
|
+
return first >= V4_MULTICAST_FROM_OCTET;
|
|
10329
|
+
}
|
|
10330
|
+
if (version === IP_VERSION_6) {
|
|
10331
|
+
const lower = address.toLowerCase();
|
|
10332
|
+
const mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(lower);
|
|
10333
|
+
if (mapped !== null) {
|
|
10334
|
+
return isPrivateAddress(mapped[1] ?? "");
|
|
10335
|
+
}
|
|
10336
|
+
if (lower === "::" || lower === "::1") {
|
|
10337
|
+
return true;
|
|
10338
|
+
}
|
|
10339
|
+
const firstGroup = Number.parseInt(/^([0-9a-f]+):/.exec(lower)?.[1] ?? "0", 16);
|
|
10340
|
+
if (Number.isNaN(firstGroup)) {
|
|
10341
|
+
return true;
|
|
10342
|
+
}
|
|
10343
|
+
if (firstGroup >= V6_UNIQUE_LOCAL_FROM && firstGroup <= V6_UNIQUE_LOCAL_TO) {
|
|
10344
|
+
return true;
|
|
10345
|
+
}
|
|
10346
|
+
if (firstGroup >= V6_LINK_LOCAL_FROM && firstGroup <= V6_LINK_LOCAL_TO) {
|
|
10347
|
+
return true;
|
|
10348
|
+
}
|
|
10349
|
+
return firstGroup >= V6_MULTICAST_FROM;
|
|
10350
|
+
}
|
|
10351
|
+
return true;
|
|
10352
|
+
}
|
|
10353
|
+
function isSuccessful4(status) {
|
|
10354
|
+
return status >= HTTP_STATUS.ok && status < SUCCESS_STATUS_MAX_EXCLUSIVE4;
|
|
10355
|
+
}
|
|
10356
|
+
function isRedirect(status) {
|
|
10357
|
+
return status === HTTP_MOVED_PERMANENTLY || status === HTTP_FOUND || status === HTTP_SEE_OTHER || status === HTTP_TEMPORARY_REDIRECT || status === HTTP_PERMANENT_REDIRECT;
|
|
10358
|
+
}
|
|
10359
|
+
function readableContentType(contentType) {
|
|
10360
|
+
if (contentType === void 0 || contentType.trim() === "") {
|
|
10361
|
+
return true;
|
|
10362
|
+
}
|
|
10363
|
+
const normalised = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
10364
|
+
const slash = normalised.indexOf("/");
|
|
10365
|
+
const main = slash === -1 ? normalised : normalised.slice(0, slash);
|
|
10366
|
+
const sub = slash === -1 ? "" : normalised.slice(slash + 1);
|
|
10367
|
+
if (main === "text") {
|
|
10368
|
+
return true;
|
|
10369
|
+
}
|
|
10370
|
+
return /json|xml|javascript|ecmascript|yaml|csv/.test(sub);
|
|
10371
|
+
}
|
|
10372
|
+
async function readCapped(chunks) {
|
|
10373
|
+
const collected = [];
|
|
10374
|
+
let total = 0;
|
|
10375
|
+
for await (const chunk of chunks) {
|
|
10376
|
+
total += chunk.byteLength;
|
|
10377
|
+
if (total > RC_WEB_FETCH_MAX_BYTES) {
|
|
10378
|
+
return { tooLarge: true, text: "" };
|
|
10379
|
+
}
|
|
10380
|
+
collected.push(Buffer.from(chunk));
|
|
10381
|
+
}
|
|
10382
|
+
return { tooLarge: false, text: Buffer.concat(collected).toString("utf8") };
|
|
10383
|
+
}
|
|
10384
|
+
async function checkTarget(url, deps) {
|
|
10385
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
10386
|
+
return refused("scheme", `the web-fetch proxy dials http and https only, not ${url.protocol}`);
|
|
10387
|
+
}
|
|
10388
|
+
if (deps.allowPrivate) {
|
|
10389
|
+
return void 0;
|
|
10390
|
+
}
|
|
10391
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
10392
|
+
let addresses;
|
|
10393
|
+
try {
|
|
10394
|
+
addresses = net3.isIP(hostname) === 0 ? await deps.lookup(hostname) : [hostname];
|
|
10395
|
+
} catch {
|
|
10396
|
+
return refused("resolve", `${hostname} did not resolve`);
|
|
10397
|
+
}
|
|
10398
|
+
for (const address of addresses) {
|
|
10399
|
+
if (isPrivateAddress(address)) {
|
|
10400
|
+
return refused("private_address", `${hostname} resolves to ${address}, a private address this web-fetch proxy refuses; set ${RC_WEB_FETCH_ALLOW_PRIVATE_ENV}=1 on the door if serving internal targets is intended`);
|
|
10401
|
+
}
|
|
10402
|
+
}
|
|
10403
|
+
return void 0;
|
|
10404
|
+
}
|
|
10405
|
+
async function fetchThroughWebProxy(url, deps) {
|
|
10406
|
+
let current;
|
|
10407
|
+
try {
|
|
10408
|
+
current = new URL(url);
|
|
10409
|
+
} catch {
|
|
10410
|
+
return refused("malformed_url", `${url} is not a URL this proxy can dial`);
|
|
10411
|
+
}
|
|
10412
|
+
const controller = new AbortController();
|
|
10413
|
+
const timer = setTimeout(() => {
|
|
10414
|
+
controller.abort();
|
|
10415
|
+
}, deps.deadlineMs);
|
|
10416
|
+
timer.unref();
|
|
10417
|
+
try {
|
|
10418
|
+
let redirects = 0;
|
|
10419
|
+
for (; ; ) {
|
|
10420
|
+
const refusal = await checkTarget(current, deps);
|
|
10421
|
+
if (refusal !== void 0) {
|
|
10422
|
+
return refusal;
|
|
10423
|
+
}
|
|
10424
|
+
let hop;
|
|
10425
|
+
try {
|
|
10426
|
+
hop = await deps.dial(current, controller.signal);
|
|
10427
|
+
} catch (error) {
|
|
10428
|
+
if (controller.signal.aborted) {
|
|
10429
|
+
return refused("deadline", `the fetch of ${url} did not settle within its deadline`);
|
|
10430
|
+
}
|
|
10431
|
+
return refused("transport", `the dial of ${current.toString()} failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
10432
|
+
}
|
|
10433
|
+
if (isRedirect(hop.status) && hop.location !== void 0) {
|
|
10434
|
+
if (redirects >= RC_WEB_FETCH_MAX_REDIRECTS) {
|
|
10435
|
+
controller.abort();
|
|
10436
|
+
return refused("too_many_redirects", `${url} followed more than ${String(RC_WEB_FETCH_MAX_REDIRECTS)} redirects without settling`);
|
|
10437
|
+
}
|
|
10438
|
+
let next;
|
|
10439
|
+
try {
|
|
10440
|
+
next = new URL(hop.location, current);
|
|
10441
|
+
} catch {
|
|
10442
|
+
controller.abort();
|
|
10443
|
+
return refused("malformed_url", `a redirect of ${current.toString()} named ${hop.location}, which is not a URL this proxy can dial`);
|
|
10444
|
+
}
|
|
10445
|
+
const drained = await readCapped(hop.chunks);
|
|
10446
|
+
if (drained.tooLarge) {
|
|
10447
|
+
controller.abort();
|
|
10448
|
+
return refused("too_large", `the body of a redirect from ${url} exceeds the ${String(RC_WEB_FETCH_MAX_BYTES)} byte cap the CLI's proxy client reads`);
|
|
10449
|
+
}
|
|
10450
|
+
redirects += 1;
|
|
10451
|
+
current = next;
|
|
10452
|
+
continue;
|
|
10453
|
+
}
|
|
10454
|
+
if (!isSuccessful4(hop.status)) {
|
|
10455
|
+
controller.abort();
|
|
10456
|
+
return refused("status", `the target answered HTTP ${String(hop.status)} for ${current.toString()}`);
|
|
10457
|
+
}
|
|
10458
|
+
const contentType = hop.contentType ?? "";
|
|
10459
|
+
if (!readableContentType(contentType)) {
|
|
10460
|
+
controller.abort();
|
|
10461
|
+
return refused("content_type", `the target at ${current.toString()} served a ${contentType} answer, which the proxy's text field cannot carry`);
|
|
10462
|
+
}
|
|
10463
|
+
const read = await readCapped(hop.chunks);
|
|
10464
|
+
if (read.tooLarge) {
|
|
10465
|
+
controller.abort();
|
|
10466
|
+
return refused("too_large", `the body of ${current.toString()} exceeds the ${String(RC_WEB_FETCH_MAX_BYTES)} byte cap the CLI's proxy client reads`);
|
|
10467
|
+
}
|
|
10468
|
+
return { kind: "fetched", url, destinationUrl: current.toString(), contentType: hop.contentType, text: read.text };
|
|
10469
|
+
}
|
|
10470
|
+
} finally {
|
|
10471
|
+
clearTimeout(timer);
|
|
10472
|
+
}
|
|
10473
|
+
}
|
|
10474
|
+
function realRcWebFetch(options) {
|
|
10475
|
+
const plainAgent = new ExemptHttpAgent();
|
|
10476
|
+
const tlsAgent = new ExemptTlsAgent();
|
|
10477
|
+
const deps = {
|
|
10478
|
+
dial: async (url, signal) => await new Promise((resolve, reject) => {
|
|
10479
|
+
const request4 = (url.protocol === "https:" ? https3 : http2).request(
|
|
10480
|
+
{
|
|
10481
|
+
hostname: url.hostname,
|
|
10482
|
+
port: url.port === "" ? void 0 : Number(url.port),
|
|
10483
|
+
method: "GET",
|
|
10484
|
+
path: `${url.pathname}${url.search}`,
|
|
10485
|
+
headers: { accept: "*/*", "accept-encoding": "identity", "user-agent": WEB_FETCH_USER_AGENT },
|
|
10486
|
+
agent: url.protocol === "https:" ? tlsAgent : plainAgent,
|
|
10487
|
+
signal
|
|
10488
|
+
},
|
|
10489
|
+
(response) => {
|
|
10490
|
+
const location = response.headers.location;
|
|
10491
|
+
resolve({
|
|
10492
|
+
status: response.statusCode ?? 0,
|
|
10493
|
+
location: typeof location === "string" ? location : void 0,
|
|
10494
|
+
contentType: typeof response.headers["content-type"] === "string" ? response.headers["content-type"] : void 0,
|
|
10495
|
+
chunks: response
|
|
10496
|
+
});
|
|
10497
|
+
}
|
|
10498
|
+
);
|
|
10499
|
+
request4.once("error", reject);
|
|
10500
|
+
request4.end();
|
|
10501
|
+
}),
|
|
10502
|
+
lookup: async (hostname) => (await dns2.promises.lookup(hostname, { all: true })).map((entry) => entry.address),
|
|
10503
|
+
allowPrivate: options.allowPrivate,
|
|
10504
|
+
deadlineMs: RC_WEB_FETCH_DEADLINE_MS
|
|
10505
|
+
};
|
|
10506
|
+
return async (url) => await fetchThroughWebProxy(url, deps);
|
|
10507
|
+
}
|
|
10508
|
+
|
|
10255
10509
|
// src/frontdoor/rcSelfHost.ts
|
|
10256
10510
|
var RC_SELF_HOST_ENV = "AGENT_SHIM_FRONTDOOR_RC_SELF_HOST";
|
|
10257
10511
|
function rcSelfHostFromEnv(env) {
|
|
@@ -10281,6 +10535,9 @@ var CONFLICT_REASON_EPOCH_STALE = "epoch_stale";
|
|
|
10281
10535
|
function isRecord6(value) {
|
|
10282
10536
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10283
10537
|
}
|
|
10538
|
+
function isWebSearchRefusal(value) {
|
|
10539
|
+
return !Array.isArray(value);
|
|
10540
|
+
}
|
|
10284
10541
|
function nonEmptyString2(record, field) {
|
|
10285
10542
|
const value = record === void 0 ? void 0 : record[field];
|
|
10286
10543
|
return typeof value === "string" && value !== "" ? value : void 0;
|
|
@@ -10295,6 +10552,20 @@ function numberOf(record, field) {
|
|
|
10295
10552
|
}
|
|
10296
10553
|
return void 0;
|
|
10297
10554
|
}
|
|
10555
|
+
function stringArray(record, field) {
|
|
10556
|
+
const value = record === void 0 ? void 0 : record[field];
|
|
10557
|
+
if (!Array.isArray(value)) {
|
|
10558
|
+
return void 0;
|
|
10559
|
+
}
|
|
10560
|
+
const members = [];
|
|
10561
|
+
for (const member of value) {
|
|
10562
|
+
if (typeof member !== "string") {
|
|
10563
|
+
return void 0;
|
|
10564
|
+
}
|
|
10565
|
+
members.push(member);
|
|
10566
|
+
}
|
|
10567
|
+
return members;
|
|
10568
|
+
}
|
|
10298
10569
|
function singleHeader2(headers, name) {
|
|
10299
10570
|
const value = headers[name];
|
|
10300
10571
|
return typeof value === "string" ? value : void 0;
|
|
@@ -10525,11 +10796,12 @@ data: ${JSON.stringify(envelope)}
|
|
|
10525
10796
|
return;
|
|
10526
10797
|
}
|
|
10527
10798
|
if (tail === "worker" || tail.startsWith("worker/")) {
|
|
10528
|
-
|
|
10799
|
+
const workerTail = tail === "worker" ? "" : tail.slice("worker/".length);
|
|
10800
|
+
const webProxy = workerTail === "web-fetch" || workerTail === "web-search";
|
|
10801
|
+
if (!(webProxy ? presentsCredential(request4.headers) || presentsWorkerJwt(request4.headers, session) : presentsWorkerJwt(request4.headers, session))) {
|
|
10529
10802
|
await unauthorized(response);
|
|
10530
10803
|
return;
|
|
10531
10804
|
}
|
|
10532
|
-
const workerTail = tail === "worker" ? "" : tail.slice("worker/".length);
|
|
10533
10805
|
if (workerTail === "" && method === "GET") {
|
|
10534
10806
|
await answerJson2(response, HTTP_STATUS.ok, { worker: { external_metadata: session.externalMetadata ?? null } });
|
|
10535
10807
|
return;
|
|
@@ -10628,6 +10900,58 @@ data: ${JSON.stringify(envelope)}
|
|
|
10628
10900
|
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
10629
10901
|
return;
|
|
10630
10902
|
}
|
|
10903
|
+
if (workerTail === "web-fetch" && method === "POST") {
|
|
10904
|
+
const body = parseJsonObject2(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
|
|
10905
|
+
const target = nonEmptyString2(body, "url");
|
|
10906
|
+
if (target === void 0) {
|
|
10907
|
+
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-fetch carries one url" } });
|
|
10908
|
+
return;
|
|
10909
|
+
}
|
|
10910
|
+
const outcome = await deps.webFetch(target);
|
|
10911
|
+
if (outcome.kind === "fetched") {
|
|
10912
|
+
const answer = { url: target, destination_url: outcome.destinationUrl, text: outcome.text, ...outcome.contentType === void 0 ? {} : { content_type: outcome.contentType } };
|
|
10913
|
+
if (Buffer.byteLength(JSON.stringify(answer), "utf8") > RC_WEB_FETCH_MAX_BYTES) {
|
|
10914
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-fetch of ${target} escaped past the reader cap when framed`);
|
|
10915
|
+
await answerJson2(response, HTTP_STATUS.ok, { error: { error_type: "web_fetch_too_large", error_message: `the answer for ${target} exceeds the ${String(RC_WEB_FETCH_MAX_BYTES)} byte cap the CLI's proxy client reads` } });
|
|
10916
|
+
return;
|
|
10917
|
+
}
|
|
10918
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-fetch served ${target} as ${outcome.destinationUrl}`);
|
|
10919
|
+
await answerJson2(response, HTTP_STATUS.ok, answer);
|
|
10920
|
+
return;
|
|
10921
|
+
}
|
|
10922
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-fetch of ${target} refused: ${outcome.errorType}`);
|
|
10923
|
+
await answerJson2(response, HTTP_STATUS.ok, { error: { error_type: outcome.errorType, error_message: outcome.errorMessage } });
|
|
10924
|
+
return;
|
|
10925
|
+
}
|
|
10926
|
+
if (workerTail === "web-search" && method === "POST") {
|
|
10927
|
+
const body = parseJsonObject2(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
|
|
10928
|
+
const query = nonEmptyString2(body, "query");
|
|
10929
|
+
if (query === void 0) {
|
|
10930
|
+
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-search carries one query" } });
|
|
10931
|
+
return;
|
|
10932
|
+
}
|
|
10933
|
+
const allowedDomains = stringArray(body, "allowed_domains");
|
|
10934
|
+
const blockedDomains = stringArray(body, "blocked_domains");
|
|
10935
|
+
if (body?.allowed_domains !== void 0 && allowedDomains === void 0 || body?.blocked_domains !== void 0 && blockedDomains === void 0) {
|
|
10936
|
+
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "the domain filters of a worker web-search are string arrays" } });
|
|
10937
|
+
return;
|
|
10938
|
+
}
|
|
10939
|
+
const searchProfile = nonEmptyString2(body, "search_profile");
|
|
10940
|
+
const backend = deps.webSearch;
|
|
10941
|
+
if (backend === void 0) {
|
|
10942
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-search of "${query}" refused: no search backend serves this door`);
|
|
10943
|
+
await answerJson2(response, HTTP_STATUS.ok, { results: [], error: { error_type: "web_search_unavailable", error_message: "this front door serves the Remote Control web-search proxy without a search backend, so no query can be served here" } });
|
|
10944
|
+
return;
|
|
10945
|
+
}
|
|
10946
|
+
const answered = await backend({ query, ...allowedDomains === void 0 ? {} : { allowedDomains }, ...blockedDomains === void 0 ? {} : { blockedDomains }, ...searchProfile === void 0 ? {} : { searchProfile } });
|
|
10947
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-search of "${query}" answered by its backend`);
|
|
10948
|
+
if (isWebSearchRefusal(answered)) {
|
|
10949
|
+
await answerJson2(response, HTTP_STATUS.ok, { results: [], error: { error_type: answered.errorType, error_message: answered.errorMessage } });
|
|
10950
|
+
return;
|
|
10951
|
+
}
|
|
10952
|
+
await answerJson2(response, HTTP_STATUS.ok, { results: answered.map((result) => ({ title: result.title ?? "", url: result.url, snippet: result.snippet ?? "" })) });
|
|
10953
|
+
return;
|
|
10954
|
+
}
|
|
10631
10955
|
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the self-hosted Remote Control surface serves no worker path "${workerTail}"` } });
|
|
10632
10956
|
return;
|
|
10633
10957
|
}
|
|
@@ -12174,6 +12498,10 @@ function prepareClaudeLaunch(options) {
|
|
|
12174
12498
|
RC_SELF_HOST_WORKER_JWT_TTL_SECONDS,
|
|
12175
12499
|
RC_SESSIONS_PATH_PREFIX,
|
|
12176
12500
|
RC_STREAM_BACKOFF_MS,
|
|
12501
|
+
RC_WEB_FETCH_ALLOW_PRIVATE_ENV,
|
|
12502
|
+
RC_WEB_FETCH_DEADLINE_MS,
|
|
12503
|
+
RC_WEB_FETCH_MAX_BYTES,
|
|
12504
|
+
RC_WEB_FETCH_MAX_REDIRECTS,
|
|
12177
12505
|
ROUTED_PATH_PREFIX,
|
|
12178
12506
|
RcPermissionModeSchema,
|
|
12179
12507
|
RcStreamEnvelopeSchema,
|
|
@@ -12232,6 +12560,7 @@ function prepareClaudeLaunch(options) {
|
|
|
12232
12560
|
ensureHeadroom,
|
|
12233
12561
|
evaluateAmbientCredentialGuard,
|
|
12234
12562
|
evaluateWhen,
|
|
12563
|
+
fetchThroughWebProxy,
|
|
12235
12564
|
formatAmbientCredentialGuardMessage,
|
|
12236
12565
|
formatCheckReport,
|
|
12237
12566
|
formatDoctorReport,
|
|
@@ -12248,6 +12577,7 @@ function prepareClaudeLaunch(options) {
|
|
|
12248
12577
|
interruptRcSession,
|
|
12249
12578
|
isIdentityDirectoryName,
|
|
12250
12579
|
isInterceptedHost,
|
|
12580
|
+
isPrivateAddress,
|
|
12251
12581
|
isRcPermissionMode,
|
|
12252
12582
|
lateRcEventDial,
|
|
12253
12583
|
lateRcStreamDial,
|
|
@@ -12272,6 +12602,7 @@ function prepareClaudeLaunch(options) {
|
|
|
12272
12602
|
rcFanoutOnDoorHub,
|
|
12273
12603
|
rcObserverAsPassthrough,
|
|
12274
12604
|
rcSelfHostFromEnv,
|
|
12605
|
+
rcWebFetchAllowsPrivateFromEnv,
|
|
12275
12606
|
readActiveIdentity,
|
|
12276
12607
|
readDirectoryRules,
|
|
12277
12608
|
readFarmManifest,
|
|
@@ -12287,6 +12618,7 @@ function prepareClaudeLaunch(options) {
|
|
|
12287
12618
|
realRcControlTransport,
|
|
12288
12619
|
realRcEventDial,
|
|
12289
12620
|
realRcStreamDial,
|
|
12621
|
+
realRcWebFetch,
|
|
12290
12622
|
recoverFarm,
|
|
12291
12623
|
recoveryDiagnostics,
|
|
12292
12624
|
removeDirectoryRule,
|