agent-shim 8.20.2 → 8.21.1
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 -0
- package/dist/cli.cjs +1126 -421
- package/dist/index.cjs +386 -27
- package/dist/index.mjs +378 -27
- 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;
|
|
@@ -10306,6 +10577,17 @@ function bearerOf(headers) {
|
|
|
10306
10577
|
}
|
|
10307
10578
|
return presented.slice(0, "bearer ".length).toLowerCase() === "bearer " ? presented.slice("bearer ".length) : void 0;
|
|
10308
10579
|
}
|
|
10580
|
+
function presentationRefusal(headers) {
|
|
10581
|
+
const presented = singleHeader2(headers, "authorization");
|
|
10582
|
+
if (presented === void 0) {
|
|
10583
|
+
return "absent";
|
|
10584
|
+
}
|
|
10585
|
+
if (presented.slice(0, "bearer ".length).toLowerCase() !== "bearer ") {
|
|
10586
|
+
return "not-a-bearer";
|
|
10587
|
+
}
|
|
10588
|
+
const bearer = presented.slice("bearer ".length);
|
|
10589
|
+
return bearer.slice(0, "bearer ".length).toLowerCase() === "bearer " ? "doubled-scheme-word" : "unrecognised";
|
|
10590
|
+
}
|
|
10309
10591
|
async function readBody2(request4, capBytes) {
|
|
10310
10592
|
return await new Promise((resolve, reject) => {
|
|
10311
10593
|
const chunks = [];
|
|
@@ -10394,6 +10676,9 @@ function createRcSelfHostSurface(deps) {
|
|
|
10394
10676
|
}
|
|
10395
10677
|
return false;
|
|
10396
10678
|
};
|
|
10679
|
+
const credentialRefusal = (headers) => presentsCredential(headers) ? void 0 : presentationRefusal(headers);
|
|
10680
|
+
const workerRefusal = (headers, session) => presentsWorkerJwt(headers, session) ? void 0 : presentationRefusal(headers);
|
|
10681
|
+
const workerOrCredentialRefusal = (headers, session) => presentsWorkerJwt(headers, session) || presentsCredential(headers) ? void 0 : presentationRefusal(headers);
|
|
10397
10682
|
const frameOf = (event) => {
|
|
10398
10683
|
const envelope = {
|
|
10399
10684
|
event_id: event.eventId,
|
|
@@ -10482,8 +10767,14 @@ data: ${JSON.stringify(envelope)}
|
|
|
10482
10767
|
const epochStale = async (response) => {
|
|
10483
10768
|
await answerJson2(response, HTTP_CONFLICT, { type: "error", error: { type: "api_error", message: "worker_epoch does not match the session's live epoch" } }, { [CONFLICT_REASON_HEADER2]: CONFLICT_REASON_EPOCH_STALE });
|
|
10484
10769
|
};
|
|
10485
|
-
const
|
|
10486
|
-
|
|
10770
|
+
const refusalMessages = {
|
|
10771
|
+
absent: "the self-hosted Remote Control surface authenticates by the Authorization header, and this call carried none",
|
|
10772
|
+
"not-a-bearer": "the self-hosted Remote Control surface reads a Bearer authorization, and this call's Authorization header did not carry the Bearer scheme word",
|
|
10773
|
+
"doubled-scheme-word": "the self-hosted Remote Control surface reads one Bearer scheme word, and this call's Authorization header carried it twice: a persisted session record holds the whole header value with the scheme word included, so prefixing Bearer onto it presents a different credential",
|
|
10774
|
+
unrecognised: "the self-hosted Remote Control surface did not recognise the presented credential"
|
|
10775
|
+
};
|
|
10776
|
+
const unauthorized = async (response, refusal) => {
|
|
10777
|
+
await answerJson2(response, HTTP_STATUS.unauthorized, { type: "error", error: { type: "authentication_error", message: refusalMessages[refusal] } });
|
|
10487
10778
|
};
|
|
10488
10779
|
const serveSessionPath = async (session, tail, request4, response) => {
|
|
10489
10780
|
const method = request4.method.toUpperCase();
|
|
@@ -10501,8 +10792,9 @@ data: ${JSON.stringify(envelope)}
|
|
|
10501
10792
|
return;
|
|
10502
10793
|
}
|
|
10503
10794
|
if (tail === "bridge" && method === "POST") {
|
|
10504
|
-
|
|
10505
|
-
|
|
10795
|
+
const refusal = credentialRefusal(request4.headers);
|
|
10796
|
+
if (refusal !== void 0) {
|
|
10797
|
+
await unauthorized(response, refusal);
|
|
10506
10798
|
return;
|
|
10507
10799
|
}
|
|
10508
10800
|
const workerJwt = mintWorkerJwt(deps.randomToken, deps.now());
|
|
@@ -10512,8 +10804,9 @@ data: ${JSON.stringify(envelope)}
|
|
|
10512
10804
|
return;
|
|
10513
10805
|
}
|
|
10514
10806
|
if (tail === "archive" && method === "POST") {
|
|
10515
|
-
|
|
10516
|
-
|
|
10807
|
+
const refusal = credentialRefusal(request4.headers);
|
|
10808
|
+
if (refusal !== void 0) {
|
|
10809
|
+
await unauthorized(response, refusal);
|
|
10517
10810
|
return;
|
|
10518
10811
|
}
|
|
10519
10812
|
for (const sink of [...session.workerStreams, ...session.clientStreams]) {
|
|
@@ -10525,11 +10818,13 @@ data: ${JSON.stringify(envelope)}
|
|
|
10525
10818
|
return;
|
|
10526
10819
|
}
|
|
10527
10820
|
if (tail === "worker" || tail.startsWith("worker/")) {
|
|
10528
|
-
|
|
10529
|
-
|
|
10821
|
+
const workerTail = tail === "worker" ? "" : tail.slice("worker/".length);
|
|
10822
|
+
const webProxy = workerTail === "web-fetch" || workerTail === "web-search";
|
|
10823
|
+
const refusal = webProxy ? workerOrCredentialRefusal(request4.headers, session) : workerRefusal(request4.headers, session);
|
|
10824
|
+
if (refusal !== void 0) {
|
|
10825
|
+
await unauthorized(response, refusal);
|
|
10530
10826
|
return;
|
|
10531
10827
|
}
|
|
10532
|
-
const workerTail = tail === "worker" ? "" : tail.slice("worker/".length);
|
|
10533
10828
|
if (workerTail === "" && method === "GET") {
|
|
10534
10829
|
await answerJson2(response, HTTP_STATUS.ok, { worker: { external_metadata: session.externalMetadata ?? null } });
|
|
10535
10830
|
return;
|
|
@@ -10628,11 +10923,64 @@ data: ${JSON.stringify(envelope)}
|
|
|
10628
10923
|
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
10629
10924
|
return;
|
|
10630
10925
|
}
|
|
10926
|
+
if (workerTail === "web-fetch" && method === "POST") {
|
|
10927
|
+
const body = parseJsonObject2(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
|
|
10928
|
+
const target = nonEmptyString2(body, "url");
|
|
10929
|
+
if (target === void 0) {
|
|
10930
|
+
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-fetch carries one url" } });
|
|
10931
|
+
return;
|
|
10932
|
+
}
|
|
10933
|
+
const outcome = await deps.webFetch(target);
|
|
10934
|
+
if (outcome.kind === "fetched") {
|
|
10935
|
+
const answer = { url: target, destination_url: outcome.destinationUrl, text: outcome.text, ...outcome.contentType === void 0 ? {} : { content_type: outcome.contentType } };
|
|
10936
|
+
if (Buffer.byteLength(JSON.stringify(answer), "utf8") > RC_WEB_FETCH_MAX_BYTES) {
|
|
10937
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-fetch of ${target} escaped past the reader cap when framed`);
|
|
10938
|
+
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` } });
|
|
10939
|
+
return;
|
|
10940
|
+
}
|
|
10941
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-fetch served ${target} as ${outcome.destinationUrl}`);
|
|
10942
|
+
await answerJson2(response, HTTP_STATUS.ok, answer);
|
|
10943
|
+
return;
|
|
10944
|
+
}
|
|
10945
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-fetch of ${target} refused: ${outcome.errorType}`);
|
|
10946
|
+
await answerJson2(response, HTTP_STATUS.ok, { error: { error_type: outcome.errorType, error_message: outcome.errorMessage } });
|
|
10947
|
+
return;
|
|
10948
|
+
}
|
|
10949
|
+
if (workerTail === "web-search" && method === "POST") {
|
|
10950
|
+
const body = parseJsonObject2(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
|
|
10951
|
+
const query = nonEmptyString2(body, "query");
|
|
10952
|
+
if (query === void 0) {
|
|
10953
|
+
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-search carries one query" } });
|
|
10954
|
+
return;
|
|
10955
|
+
}
|
|
10956
|
+
const allowedDomains = stringArray(body, "allowed_domains");
|
|
10957
|
+
const blockedDomains = stringArray(body, "blocked_domains");
|
|
10958
|
+
if (body?.allowed_domains !== void 0 && allowedDomains === void 0 || body?.blocked_domains !== void 0 && blockedDomains === void 0) {
|
|
10959
|
+
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" } });
|
|
10960
|
+
return;
|
|
10961
|
+
}
|
|
10962
|
+
const searchProfile = nonEmptyString2(body, "search_profile");
|
|
10963
|
+
const backend = deps.webSearch;
|
|
10964
|
+
if (backend === void 0) {
|
|
10965
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-search of "${query}" refused: no search backend serves this door`);
|
|
10966
|
+
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" } });
|
|
10967
|
+
return;
|
|
10968
|
+
}
|
|
10969
|
+
const answered = await backend({ query, ...allowedDomains === void 0 ? {} : { allowedDomains }, ...blockedDomains === void 0 ? {} : { blockedDomains }, ...searchProfile === void 0 ? {} : { searchProfile } });
|
|
10970
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-search of "${query}" answered by its backend`);
|
|
10971
|
+
if (isWebSearchRefusal(answered)) {
|
|
10972
|
+
await answerJson2(response, HTTP_STATUS.ok, { results: [], error: { error_type: answered.errorType, error_message: answered.errorMessage } });
|
|
10973
|
+
return;
|
|
10974
|
+
}
|
|
10975
|
+
await answerJson2(response, HTTP_STATUS.ok, { results: answered.map((result) => ({ title: result.title ?? "", url: result.url, snippet: result.snippet ?? "" })) });
|
|
10976
|
+
return;
|
|
10977
|
+
}
|
|
10631
10978
|
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
10979
|
return;
|
|
10633
10980
|
}
|
|
10634
|
-
|
|
10635
|
-
|
|
10981
|
+
const clientRefusal = credentialRefusal(request4.headers);
|
|
10982
|
+
if (clientRefusal !== void 0) {
|
|
10983
|
+
await unauthorized(response, clientRefusal);
|
|
10636
10984
|
return;
|
|
10637
10985
|
}
|
|
10638
10986
|
if (tail === "events" && method === "POST") {
|
|
@@ -10697,8 +11045,9 @@ data: ${JSON.stringify(envelope)}
|
|
|
10697
11045
|
await answerJson2(response, HTTP_STATUS.methodNotAllowed, { type: "error", error: { type: "api_error", message: "the compatibility session list is a GET" } });
|
|
10698
11046
|
return;
|
|
10699
11047
|
}
|
|
10700
|
-
|
|
10701
|
-
|
|
11048
|
+
const listRefusal = credentialRefusal(request4.headers);
|
|
11049
|
+
if (listRefusal !== void 0) {
|
|
11050
|
+
await unauthorized(response, listRefusal);
|
|
10702
11051
|
return;
|
|
10703
11052
|
}
|
|
10704
11053
|
await answerJson2(response, HTTP_STATUS.ok, { data: [] });
|
|
@@ -10710,8 +11059,9 @@ data: ${JSON.stringify(envelope)}
|
|
|
10710
11059
|
}
|
|
10711
11060
|
if (pathname === RC_SESSIONS_PATH_PREFIX2) {
|
|
10712
11061
|
if (method === "POST") {
|
|
10713
|
-
|
|
10714
|
-
|
|
11062
|
+
const createRefusal = credentialRefusal(request4.headers);
|
|
11063
|
+
if (createRefusal !== void 0) {
|
|
11064
|
+
await unauthorized(response, createRefusal);
|
|
10715
11065
|
return;
|
|
10716
11066
|
}
|
|
10717
11067
|
const body = parseJsonObject2(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
|
|
@@ -10736,8 +11086,9 @@ data: ${JSON.stringify(envelope)}
|
|
|
10736
11086
|
return;
|
|
10737
11087
|
}
|
|
10738
11088
|
if (method === "GET") {
|
|
10739
|
-
|
|
10740
|
-
|
|
11089
|
+
const readRefusal = credentialRefusal(request4.headers);
|
|
11090
|
+
if (readRefusal !== void 0) {
|
|
11091
|
+
await unauthorized(response, readRefusal);
|
|
10741
11092
|
return;
|
|
10742
11093
|
}
|
|
10743
11094
|
await answerJson2(response, HTTP_STATUS.ok, { data: [...sessions.values()].map((session) => ({ id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), updated_at: new Date(session.lastTrafficAt).toISOString(), session_status: "active" })) });
|
|
@@ -12174,6 +12525,10 @@ function prepareClaudeLaunch(options) {
|
|
|
12174
12525
|
RC_SELF_HOST_WORKER_JWT_TTL_SECONDS,
|
|
12175
12526
|
RC_SESSIONS_PATH_PREFIX,
|
|
12176
12527
|
RC_STREAM_BACKOFF_MS,
|
|
12528
|
+
RC_WEB_FETCH_ALLOW_PRIVATE_ENV,
|
|
12529
|
+
RC_WEB_FETCH_DEADLINE_MS,
|
|
12530
|
+
RC_WEB_FETCH_MAX_BYTES,
|
|
12531
|
+
RC_WEB_FETCH_MAX_REDIRECTS,
|
|
12177
12532
|
ROUTED_PATH_PREFIX,
|
|
12178
12533
|
RcPermissionModeSchema,
|
|
12179
12534
|
RcStreamEnvelopeSchema,
|
|
@@ -12232,6 +12587,7 @@ function prepareClaudeLaunch(options) {
|
|
|
12232
12587
|
ensureHeadroom,
|
|
12233
12588
|
evaluateAmbientCredentialGuard,
|
|
12234
12589
|
evaluateWhen,
|
|
12590
|
+
fetchThroughWebProxy,
|
|
12235
12591
|
formatAmbientCredentialGuardMessage,
|
|
12236
12592
|
formatCheckReport,
|
|
12237
12593
|
formatDoctorReport,
|
|
@@ -12248,6 +12604,7 @@ function prepareClaudeLaunch(options) {
|
|
|
12248
12604
|
interruptRcSession,
|
|
12249
12605
|
isIdentityDirectoryName,
|
|
12250
12606
|
isInterceptedHost,
|
|
12607
|
+
isPrivateAddress,
|
|
12251
12608
|
isRcPermissionMode,
|
|
12252
12609
|
lateRcEventDial,
|
|
12253
12610
|
lateRcStreamDial,
|
|
@@ -12272,6 +12629,7 @@ function prepareClaudeLaunch(options) {
|
|
|
12272
12629
|
rcFanoutOnDoorHub,
|
|
12273
12630
|
rcObserverAsPassthrough,
|
|
12274
12631
|
rcSelfHostFromEnv,
|
|
12632
|
+
rcWebFetchAllowsPrivateFromEnv,
|
|
12275
12633
|
readActiveIdentity,
|
|
12276
12634
|
readDirectoryRules,
|
|
12277
12635
|
readFarmManifest,
|
|
@@ -12287,6 +12645,7 @@ function prepareClaudeLaunch(options) {
|
|
|
12287
12645
|
realRcControlTransport,
|
|
12288
12646
|
realRcEventDial,
|
|
12289
12647
|
realRcStreamDial,
|
|
12648
|
+
realRcWebFetch,
|
|
12290
12649
|
recoverFarm,
|
|
12291
12650
|
recoveryDiagnostics,
|
|
12292
12651
|
removeDirectoryRule,
|