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/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 http2;
2994
+ let http3;
2987
2995
  try {
2988
- http2 = await ports.startProviderListener(lastPort);
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 && http2.port !== previousSticky) {
2993
- ports.log(`agent-shim frontdoor supervisor: sticky port ${String(previousSticky)} was occupied; moving to ${String(http2.port)} (sessions launched against the old port are stale until they relaunch)`);
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 = http2.port;
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 http2.close();
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 http2.close();
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: http2.port, connectPort: connect3.port, directPort: direct.port, ...sticky() });
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(http2.port)}, CONNECT surface on 127.0.0.1:${String(connect3.port)}`);
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 http2.close();
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
- if (!presentsWorkerJwt(request4.headers, session)) {
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,