agent-shim 8.2.0 → 8.3.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.mjs CHANGED
@@ -2449,6 +2449,7 @@ function pumpTapSession(secure, openUpstream, tap, prefix = Buffer.alloc(0)) {
2449
2449
  }
2450
2450
 
2451
2451
  // src/frontdoor/capability.ts
2452
+ import { createHash as createHash2, timingSafeEqual } from "node:crypto";
2452
2453
  var CONNECT_PROXY_USER = "agent-shim";
2453
2454
  var CONNECT_PROXY_REALM = "agent-shim front door";
2454
2455
  var BASIC_SCHEME = "basic";
@@ -2469,6 +2470,18 @@ function capabilityFromProxyAuthorization(value) {
2469
2470
  const password = userPass.slice(colon + 1);
2470
2471
  return password === "" ? void 0 : password;
2471
2472
  }
2473
+ function digest(token) {
2474
+ return createHash2("sha256").update(token, "utf8").digest();
2475
+ }
2476
+ function isLiveCapability(presented, live) {
2477
+ const presentedDigest = digest(presented);
2478
+ let matched = false;
2479
+ for (const token of live) {
2480
+ const equal = timingSafeEqual(presentedDigest, digest(token));
2481
+ matched = equal || matched;
2482
+ }
2483
+ return matched;
2484
+ }
2472
2485
 
2473
2486
  // src/frontdoor/route.ts
2474
2487
  var IDENTITY_HEADER = "x-agent-shim-identity";
@@ -2779,14 +2792,14 @@ function caParses(ca) {
2779
2792
  var TAP_ALPN_PROTOCOLS = ["h2", "http/1.1"];
2780
2793
  async function startConnectServer(config, effects, preferredPort) {
2781
2794
  const tunnelTokens = /* @__PURE__ */ new WeakMap();
2782
- const onUpgrade = (host) => (request3, socket, head) => {
2783
- config.capture?.upgrade(request3);
2795
+ const onUpgrade = (host) => (request4, socket, head) => {
2796
+ config.capture?.upgrade(request4);
2784
2797
  if (head.length > 0) {
2785
2798
  socket.pause();
2786
2799
  socket.unshift(head);
2787
2800
  }
2788
- const headText = `${request3.method ?? "GET"} ${request3.url ?? "/"} HTTP/1.1\r
2789
- ${request3.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `${accumulated}${value}: ` : `${accumulated}${value}\r
2801
+ const headText = `${request4.method ?? "GET"} ${request4.url ?? "/"} HTTP/1.1\r
2802
+ ${request4.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `${accumulated}${value}: ` : `${accumulated}${value}\r
2790
2803
  `, "")}\r
2791
2804
  `;
2792
2805
  pumpTapSession(socket, async (clientAlpn) => await effects.connectTlsUpstream(host, HTTPS_PORT, clientAlpn), config.capture?.tapStream?.(host), Buffer.from(headText, "utf8"));
@@ -2799,16 +2812,16 @@ ${request3.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
2799
2812
  const tlsAcceptor2 = effects.createTlsAcceptor(config.leafFor(host), serveSecure2, TAP_ALPN_PROTOCOLS);
2800
2813
  return { host, serveSecure: serveSecure2, tlsAcceptor: tlsAcceptor2 };
2801
2814
  }
2802
- const handler = (request3, response) => {
2803
- if (host === config.routedHost && servedByPipeline(request3.url)) {
2804
- const tunnelToken = tunnelTokens.get(request3.socket);
2805
- if (tunnelToken !== void 0 && request3.headers[AUTH_HEADER] === void 0) {
2806
- request3.headers[AUTH_HEADER] = tunnelToken;
2815
+ const handler = (request4, response) => {
2816
+ if (host === config.routedHost && servedByPipeline(request4.url)) {
2817
+ const tunnelToken = tunnelTokens.get(request4.socket);
2818
+ if (tunnelToken !== void 0 && request4.headers[AUTH_HEADER] === void 0) {
2819
+ request4.headers[AUTH_HEADER] = tunnelToken;
2807
2820
  }
2808
- config.serveRouted(request3, response);
2821
+ config.serveRouted(request4, response);
2809
2822
  return;
2810
2823
  }
2811
- effects.forwardHttp(config.upstreamFor(host), request3, response, config.capture?.observePassthrough(request3));
2824
+ effects.forwardHttp(config.upstreamFor(host), request4, response, config.capture?.observePassthrough(request4));
2812
2825
  };
2813
2826
  const httpSession = effects.createHttpSession(handler, onUpgrade(host));
2814
2827
  const serveSecure = (secure, capability) => {
@@ -2850,7 +2863,7 @@ ${request3.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
2850
2863
  }
2851
2864
  };
2852
2865
  }
2853
- function createConnectionLedger(limits, isLiveCapability) {
2866
+ function createConnectionLedger(limits, isLiveCapability2) {
2854
2867
  const pending = /* @__PURE__ */ new Map();
2855
2868
  const authenticated = /* @__PURE__ */ new Map();
2856
2869
  let tunnels = 0;
@@ -2863,7 +2876,7 @@ function createConnectionLedger(limits, isLiveCapability) {
2863
2876
  };
2864
2877
  const revalidation = setInterval(() => {
2865
2878
  for (const [capability, sockets] of authenticated) {
2866
- if (!isLiveCapability(capability)) {
2879
+ if (!isLiveCapability2(capability)) {
2867
2880
  for (const socket of sockets) {
2868
2881
  socket.destroy();
2869
2882
  }
@@ -3105,6 +3118,40 @@ var ExemptTlsAgent = class extends https.Agent {
3105
3118
  return tls.connect(tlsOptions);
3106
3119
  }
3107
3120
  };
3121
+ function realRcEventDial(target = { host: CONNECT_INTERCEPT_HOST, port: HTTPS_PORT }) {
3122
+ const agent = new ExemptTlsAgent();
3123
+ return {
3124
+ writeEvents: async (sessionId, headers, body) => await new Promise((resolve, reject) => {
3125
+ const options = {
3126
+ host: target.host,
3127
+ port: target.port,
3128
+ method: "POST",
3129
+ path: `/v1/code/sessions/${encodeURIComponent(sessionId)}/events`,
3130
+ // The Host header names the host whose API this is, not the address dialled: a redirected test dials a loopback stand-in while still speaking to the API host by name, exactly as the forwarded paths present their SNI.
3131
+ headers: { ...headers, host: target.servername ?? target.host, "content-length": String(Buffer.byteLength(body, "utf8")) },
3132
+ agent
3133
+ };
3134
+ options.servername = target.servername ?? target.host;
3135
+ if (target.ca !== void 0) {
3136
+ options.ca = [...target.ca];
3137
+ }
3138
+ if (target.rejectUnauthorized !== void 0) {
3139
+ options.rejectUnauthorized = target.rejectUnauthorized;
3140
+ }
3141
+ const request4 = https.request(options, (response) => {
3142
+ const chunks = [];
3143
+ response.on("data", (chunk) => {
3144
+ chunks.push(chunk);
3145
+ });
3146
+ response.on("end", () => {
3147
+ resolve({ status: response.statusCode ?? HTTP_BAD_GATEWAY, body: Buffer.concat(chunks).toString("utf8") });
3148
+ });
3149
+ });
3150
+ request4.once("error", reject);
3151
+ request4.end(body);
3152
+ })
3153
+ };
3154
+ }
3108
3155
  function realConnectEffects() {
3109
3156
  const plainAgent = new ExemptHttpAgent();
3110
3157
  const tlsAgent = new ExemptTlsAgent();
@@ -3210,8 +3257,8 @@ function realConnectEffects() {
3210
3257
  socket.destroy();
3211
3258
  });
3212
3259
  if (onUpgrade !== void 0) {
3213
- server.on("upgrade", (request3, socket, head) => {
3214
- onUpgrade(request3, socket, head);
3260
+ server.on("upgrade", (request4, socket, head) => {
3261
+ onUpgrade(request4, socket, head);
3215
3262
  });
3216
3263
  }
3217
3264
  return {
@@ -3250,14 +3297,14 @@ function realConnectEffects() {
3250
3297
  });
3251
3298
  socket.once("error", reject);
3252
3299
  }),
3253
- forwardHttp: (target, request3, response, observer) => {
3254
- const headers = forwardableHeaders(request3.headers);
3300
+ forwardHttp: (target, request4, response, observer) => {
3301
+ const headers = forwardableHeaders(request4.headers);
3255
3302
  const options = {
3256
3303
  host: target.host,
3257
3304
  port: target.port,
3258
3305
  lookup: realAddressLookup,
3259
- method: request3.method,
3260
- path: request3.url,
3306
+ method: request4.method,
3307
+ path: request4.url,
3261
3308
  headers,
3262
3309
  agent: target.tls ? tlsAgent : plainAgent
3263
3310
  };
@@ -3292,15 +3339,15 @@ function realConnectEffects() {
3292
3339
  response.end(body);
3293
3340
  });
3294
3341
  if (observer !== void 0) {
3295
- request3.on("data", (chunk) => {
3342
+ request4.on("data", (chunk) => {
3296
3343
  observer.onRequestChunk(chunk);
3297
3344
  });
3298
3345
  response.on("close", () => {
3299
3346
  observer.onEnd();
3300
3347
  });
3301
3348
  }
3302
- request3.pipe(upstream);
3303
- request3.socket.setNoDelay(true);
3349
+ request4.pipe(upstream);
3350
+ request4.socket.setNoDelay(true);
3304
3351
  upstream.on("socket", (socket) => {
3305
3352
  socket.setNoDelay(true);
3306
3353
  });
@@ -3308,11 +3355,488 @@ function realConnectEffects() {
3308
3355
  };
3309
3356
  }
3310
3357
 
3358
+ // src/codex/http.ts
3359
+ var HTTP_STATUS = {
3360
+ ok: 200,
3361
+ badRequest: 400,
3362
+ unauthorized: 401,
3363
+ forbidden: 403,
3364
+ notFound: 404,
3365
+ methodNotAllowed: 405,
3366
+ payloadTooLarge: 413,
3367
+ tooManyRequests: 429,
3368
+ /** Not sent to anyone: the client is gone. Recorded for a response abandoned because the client disconnected, following the convention nginx uses for the same case. */
3369
+ clientClosedRequest: 499,
3370
+ internalServerError: 500,
3371
+ badGateway: 502
3372
+ };
3373
+
3374
+ // src/frontdoor/rcSessions.ts
3375
+ var RC_SESSIONS_PATH_PREFIX = "/v1/code/sessions";
3376
+ var RC_SESSION_ID_PREFIX = "cse_";
3377
+ var RC_IDLE_EXPIRY_MS = 45e3;
3378
+ var RC_CREATE_RESPONSE_CAP_BYTES = 8192;
3379
+ var CONFLICT_REASON_HEADER = "x-ccr-conflict-reason";
3380
+ var SUCCESS_STATUS_MAX_EXCLUSIVE = 300;
3381
+ function isSuccessful(status) {
3382
+ return status >= HTTP_STATUS.ok && status < SUCCESS_STATUS_MAX_EXCLUSIVE;
3383
+ }
3384
+ function singleHeader(headers, name) {
3385
+ const value = headers[name];
3386
+ return typeof value === "string" ? value : void 0;
3387
+ }
3388
+ function sessionCreateId(body) {
3389
+ let parsed;
3390
+ try {
3391
+ parsed = JSON.parse(body);
3392
+ } catch {
3393
+ return void 0;
3394
+ }
3395
+ if (typeof parsed !== "object" || parsed === null || !("session" in parsed)) {
3396
+ return void 0;
3397
+ }
3398
+ const session = parsed.session;
3399
+ if (typeof session !== "object" || session === null || !("id" in session)) {
3400
+ return void 0;
3401
+ }
3402
+ const id = session.id;
3403
+ return typeof id === "string" && id.startsWith(RC_SESSION_ID_PREFIX) ? id : void 0;
3404
+ }
3405
+ function createRcSessionTracker(deps) {
3406
+ const entries = /* @__PURE__ */ new Map();
3407
+ const expire = (id) => {
3408
+ entries.delete(id);
3409
+ };
3410
+ const sweep = () => {
3411
+ for (const [id, entry] of entries) {
3412
+ if (deps.now() - entry.lastSeenAt >= deps.idleMs) {
3413
+ expire(id);
3414
+ }
3415
+ }
3416
+ };
3417
+ const timer = setInterval(sweep, deps.idleMs);
3418
+ timer.unref();
3419
+ const observeExchange = (request4) => {
3420
+ const method = (request4.method ?? "").toUpperCase();
3421
+ const path29 = new URL(request4.url ?? "/", "http://127.0.0.1").pathname;
3422
+ if (!path29.startsWith(RC_SESSIONS_PATH_PREFIX)) {
3423
+ return void 0;
3424
+ }
3425
+ const tail = path29.slice(RC_SESSIONS_PATH_PREFIX.length);
3426
+ const facts = {
3427
+ authorization: singleHeader(request4.headers, "authorization"),
3428
+ anthropicVersion: singleHeader(request4.headers, "anthropic-version"),
3429
+ anthropicClientPlatform: singleHeader(request4.headers, "anthropic-client-platform")
3430
+ };
3431
+ if (tail === "") {
3432
+ if (method !== "POST") {
3433
+ return void 0;
3434
+ }
3435
+ let accepted = false;
3436
+ let buffered = 0;
3437
+ let body = "";
3438
+ return {
3439
+ onResponse: (status) => {
3440
+ accepted = isSuccessful(status);
3441
+ },
3442
+ onBodyChunk: (chunk) => {
3443
+ if (!accepted || buffered >= RC_CREATE_RESPONSE_CAP_BYTES) {
3444
+ return;
3445
+ }
3446
+ const text = Buffer.from(chunk).toString("utf8");
3447
+ body = body + text.slice(0, RC_CREATE_RESPONSE_CAP_BYTES - buffered);
3448
+ buffered = body.length;
3449
+ },
3450
+ onEnd: () => {
3451
+ if (!accepted) {
3452
+ return;
3453
+ }
3454
+ const id2 = sessionCreateId(body);
3455
+ if (id2 === void 0) {
3456
+ return;
3457
+ }
3458
+ entries.set(id2, { id: id2, createdAt: deps.now(), lastSeenAt: deps.now(), ...facts });
3459
+ }
3460
+ };
3461
+ }
3462
+ const rest = tail.slice(1);
3463
+ const [id] = rest.split("/");
3464
+ if (id?.startsWith(RC_SESSION_ID_PREFIX) !== true) {
3465
+ return void 0;
3466
+ }
3467
+ const entry = entries.get(id);
3468
+ if (entry === void 0) {
3469
+ entries.set(id, { id, createdAt: deps.now(), lastSeenAt: deps.now(), ...facts });
3470
+ } else {
3471
+ entry.lastSeenAt = deps.now();
3472
+ entry.authorization = facts.authorization ?? entry.authorization;
3473
+ entry.anthropicVersion = facts.anthropicVersion ?? entry.anthropicVersion;
3474
+ entry.anthropicClientPlatform = facts.anthropicClientPlatform ?? entry.anthropicClientPlatform;
3475
+ }
3476
+ const archiving = method === "POST" && rest === `${id}/archive`;
3477
+ return {
3478
+ onResponse: (status, headers) => {
3479
+ if (singleHeader(headers, CONFLICT_REASON_HEADER) !== void 0 || archiving && isSuccessful(status)) {
3480
+ expire(id);
3481
+ }
3482
+ },
3483
+ onBodyChunk: () => {
3484
+ },
3485
+ onEnd: () => {
3486
+ }
3487
+ };
3488
+ };
3489
+ return {
3490
+ observeExchange,
3491
+ list: () => {
3492
+ sweep();
3493
+ return [...entries.values()].sort((left, right) => left.createdAt - right.createdAt || (left.id < right.id ? -1 : 1)).map(({ id, createdAt, lastSeenAt }) => ({ id, createdAt, lastSeenAt }));
3494
+ },
3495
+ credentialOf: (sessionId) => {
3496
+ const entry = entries.get(sessionId);
3497
+ return entry === void 0 ? void 0 : { authorization: entry.authorization, anthropicVersion: entry.anthropicVersion, anthropicClientPlatform: entry.anthropicClientPlatform };
3498
+ },
3499
+ close: () => {
3500
+ clearInterval(timer);
3501
+ }
3502
+ };
3503
+ }
3504
+ function observingRoutedRoute(route, tracker) {
3505
+ return {
3506
+ ...route,
3507
+ serve: async (request4, response) => {
3508
+ const observed = tracker.observeExchange({ method: request4.method, url: request4.url, headers: request4.headers });
3509
+ await route.serve(request4, observed === void 0 ? response : teeRoutedResponse(response, observed));
3510
+ }
3511
+ };
3512
+ }
3513
+ function teeRoutedResponse(response, observer) {
3514
+ return {
3515
+ get headersSent() {
3516
+ return response.headersSent;
3517
+ },
3518
+ start: (status, headers) => {
3519
+ observer.onResponse(status, headers);
3520
+ response.start(status, headers);
3521
+ },
3522
+ flush: () => {
3523
+ response.flush();
3524
+ },
3525
+ write: async (chunk) => {
3526
+ observer.onBodyChunk(typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk);
3527
+ await response.write(chunk);
3528
+ },
3529
+ end: () => {
3530
+ response.end();
3531
+ observer.onEnd();
3532
+ },
3533
+ destroy: () => {
3534
+ response.destroy();
3535
+ observer.onEnd();
3536
+ }
3537
+ };
3538
+ }
3539
+ function rcObserverAsPassthrough(observer) {
3540
+ return {
3541
+ // The request body carries nothing the tracker reads: the credential and the session id are in the request's headers and path, already handed over with the request.
3542
+ onRequestChunk: () => {
3543
+ return;
3544
+ },
3545
+ onResponse: (status, headers) => {
3546
+ observer.onResponse(status, headers);
3547
+ },
3548
+ onResponseChunk: (chunk) => {
3549
+ observer.onBodyChunk(chunk);
3550
+ },
3551
+ onEnd: () => {
3552
+ observer.onEnd();
3553
+ }
3554
+ };
3555
+ }
3556
+ function buildRcUserMessagePayload(uuid, sessionId, text) {
3557
+ return {
3558
+ type: "user",
3559
+ uuid,
3560
+ session_id: sessionId,
3561
+ parent_tool_use_id: null,
3562
+ message: { role: "user", content: text }
3563
+ };
3564
+ }
3565
+ function buildRcEventWriteBody(payload) {
3566
+ return { events: [{ payload }] };
3567
+ }
3568
+ function sequenceNumsOf(body) {
3569
+ let parsed;
3570
+ try {
3571
+ parsed = JSON.parse(body);
3572
+ } catch {
3573
+ return void 0;
3574
+ }
3575
+ if (typeof parsed !== "object" || parsed === null || !("results" in parsed) || !Array.isArray(parsed.results)) {
3576
+ return void 0;
3577
+ }
3578
+ const sequenceNums = [];
3579
+ const results = parsed.results;
3580
+ for (const result of results) {
3581
+ if (typeof result !== "object" || result === null || !("sequence_num" in result)) {
3582
+ return void 0;
3583
+ }
3584
+ const sequenceNum = result.sequence_num;
3585
+ if (typeof sequenceNum !== "number") {
3586
+ return void 0;
3587
+ }
3588
+ sequenceNums.push(sequenceNum);
3589
+ }
3590
+ return sequenceNums.length === 0 ? void 0 : sequenceNums;
3591
+ }
3592
+ function rcInjectResultFromAnswer(answer) {
3593
+ if (!isSuccessful(answer.status)) {
3594
+ if (answer.status === HTTP_STATUS.unauthorized) {
3595
+ return { ok: false, message: `the API refused the observed Authorization bearer for this session (HTTP 401): the bearer went stale after the door last saw the session's traffic. The session's own client refreshes it on its next call, which the door will observe, so retry once Remote Control has been active again. API answer: ${answer.body}` };
3596
+ }
3597
+ return { ok: false, message: `the API answered HTTP ${String(answer.status)} to the injected event: ${answer.body}` };
3598
+ }
3599
+ const sequenceNums = sequenceNumsOf(answer.body);
3600
+ if (sequenceNums === void 0) {
3601
+ return { ok: false, message: `the API accepted the injected event but its answer named no results[].sequence_num, so delivery cannot be confirmed: ${answer.body}` };
3602
+ }
3603
+ return { ok: true, sequenceNums };
3604
+ }
3605
+ async function injectRcUserMessage(deps, sessionId, text) {
3606
+ const credential = deps.credentialOf(sessionId);
3607
+ if (credential === void 0) {
3608
+ return { ok: false, message: `the front door has not observed Remote Control session ${sessionId}: it may never have passed through this door, or it ended or expired (an entry lives only a bounded idle period past its last observed traffic)` };
3609
+ }
3610
+ const headers = { "content-type": "application/json" };
3611
+ if (credential.authorization !== void 0) {
3612
+ headers.authorization = credential.authorization;
3613
+ }
3614
+ if (credential.anthropicVersion !== void 0) {
3615
+ headers["anthropic-version"] = credential.anthropicVersion;
3616
+ }
3617
+ if (credential.anthropicClientPlatform !== void 0) {
3618
+ headers["anthropic-client-platform"] = credential.anthropicClientPlatform;
3619
+ }
3620
+ const body = JSON.stringify(buildRcEventWriteBody(buildRcUserMessagePayload(deps.newUuid(), sessionId, text)));
3621
+ let answer;
3622
+ try {
3623
+ answer = await deps.dial.writeEvents(sessionId, headers, body);
3624
+ } catch (error) {
3625
+ return { ok: false, message: `the door could not reach the API host to inject into session ${sessionId}: ${error instanceof Error ? error.message : String(error)}` };
3626
+ }
3627
+ return rcInjectResultFromAnswer(answer);
3628
+ }
3629
+
3630
+ // src/frontdoor/rcControl.ts
3631
+ import * as https2 from "node:https";
3632
+ var CONTROL_PATH_PREFIX = "/__agent-shim/rc";
3633
+ var CONTROL_BODY_CAP_BYTES = 10485760;
3634
+ function answerJson(response, status, body) {
3635
+ const text = JSON.stringify(body);
3636
+ response.writeHead(status, { "content-type": "application/json", "content-length": String(Buffer.byteLength(text, "utf8")) });
3637
+ response.end(text);
3638
+ }
3639
+ async function readBody(request4) {
3640
+ return await new Promise((resolve, reject) => {
3641
+ const chunks = [];
3642
+ let total = 0;
3643
+ request4.on("data", (chunk) => {
3644
+ total += chunk.length;
3645
+ if (total > CONTROL_BODY_CAP_BYTES) {
3646
+ request4.destroy();
3647
+ reject(new Error("too large"));
3648
+ return;
3649
+ }
3650
+ chunks.push(chunk);
3651
+ });
3652
+ request4.on("end", () => {
3653
+ resolve(Buffer.concat(chunks).toString("utf8"));
3654
+ });
3655
+ request4.on("error", () => {
3656
+ reject(new Error("the request body ended early"));
3657
+ });
3658
+ });
3659
+ }
3660
+ function createRcControlHandler(deps) {
3661
+ const sessionsPath = `${CONTROL_PATH_PREFIX}/sessions`;
3662
+ const injectPath = `${CONTROL_PATH_PREFIX}/inject`;
3663
+ return (request4, response) => {
3664
+ const presented = request4.headers.authorization;
3665
+ const bearerPrefix = "bearer ".length;
3666
+ const token = typeof presented === "string" && presented.slice(0, bearerPrefix).toLowerCase() === "bearer " ? presented.slice(bearerPrefix) : void 0;
3667
+ if (token === void 0 || !isLiveCapability(token, [deps.expectedToken])) {
3668
+ answerJson(response, HTTP_STATUS.unauthorized, { error: "the front door's control routes demand this generation's control token as a Bearer credential" });
3669
+ return;
3670
+ }
3671
+ const url = request4.url ?? "/";
3672
+ const handle = async () => {
3673
+ if (url === sessionsPath) {
3674
+ if (request4.method !== "GET") {
3675
+ answerJson(response, HTTP_STATUS.methodNotAllowed, { error: "the session list is a GET" });
3676
+ return;
3677
+ }
3678
+ answerJson(response, HTTP_STATUS.ok, { sessions: deps.list() });
3679
+ return;
3680
+ }
3681
+ if (url === injectPath) {
3682
+ if (request4.method !== "POST") {
3683
+ answerJson(response, HTTP_STATUS.methodNotAllowed, { error: "the inject operation is a POST" });
3684
+ return;
3685
+ }
3686
+ let parsed;
3687
+ try {
3688
+ parsed = JSON.parse(await readBody(request4));
3689
+ } catch {
3690
+ answerJson(response, HTTP_STATUS.badRequest, { error: "the inject body is not readable JSON within the control body cap" });
3691
+ return;
3692
+ }
3693
+ if (typeof parsed !== "object" || parsed === null || !("session" in parsed) || !("text" in parsed) || typeof parsed.session !== "string" || typeof parsed.text !== "string" || parsed.session === "" || parsed.text === "") {
3694
+ answerJson(response, HTTP_STATUS.badRequest, { error: "the inject body must be JSON naming a non-empty session id and text" });
3695
+ return;
3696
+ }
3697
+ const result = await deps.inject(parsed.session, parsed.text);
3698
+ if (result.ok) {
3699
+ answerJson(response, HTTP_STATUS.ok, { session: parsed.session, sequenceNums: result.sequenceNums });
3700
+ return;
3701
+ }
3702
+ answerJson(response, HTTP_STATUS.badGateway, { error: result.message });
3703
+ return;
3704
+ }
3705
+ answerJson(response, HTTP_STATUS.notFound, { error: `no such control route: ${url}` });
3706
+ };
3707
+ handle().catch((error) => {
3708
+ answerJson(response, HTTP_STATUS.internalServerError, { error: `the control route failed: ${error instanceof Error ? error.message : String(error)}` });
3709
+ });
3710
+ };
3711
+ }
3712
+ function realRcControlTransport(port, ca) {
3713
+ return {
3714
+ request: async (options) => await new Promise((resolve, reject) => {
3715
+ const request4 = https2.request(
3716
+ {
3717
+ host: "127.0.0.1",
3718
+ port,
3719
+ method: options.method,
3720
+ path: options.path,
3721
+ headers: options.body === void 0 ? options.headers : { ...options.headers, "content-length": String(Buffer.byteLength(options.body, "utf8")) },
3722
+ agent: false,
3723
+ ca: [ca]
3724
+ },
3725
+ (response) => {
3726
+ const chunks = [];
3727
+ response.on("data", (chunk) => {
3728
+ chunks.push(chunk);
3729
+ });
3730
+ response.on("end", () => {
3731
+ resolve({ status: response.statusCode ?? 0, body: Buffer.concat(chunks).toString("utf8") });
3732
+ });
3733
+ }
3734
+ );
3735
+ request4.once("error", reject);
3736
+ if (options.body === void 0) {
3737
+ request4.end();
3738
+ } else {
3739
+ request4.end(options.body);
3740
+ }
3741
+ })
3742
+ };
3743
+ }
3744
+ function controlErrorOf(body) {
3745
+ try {
3746
+ const parsed = JSON.parse(body);
3747
+ if (typeof parsed === "object" && parsed !== null && "error" in parsed && typeof parsed.error === "string") {
3748
+ return parsed.error;
3749
+ }
3750
+ } catch {
3751
+ return body;
3752
+ }
3753
+ return void 0;
3754
+ }
3755
+ function isRcSessionSummary(value) {
3756
+ if (typeof value !== "object" || value === null) {
3757
+ return false;
3758
+ }
3759
+ if (!("id" in value) || !("createdAt" in value) || !("lastSeenAt" in value)) {
3760
+ return false;
3761
+ }
3762
+ return typeof value.id === "string" && typeof value.createdAt === "number" && typeof value.lastSeenAt === "number";
3763
+ }
3764
+ function sequenceNumsOfBody(body) {
3765
+ let parsed;
3766
+ try {
3767
+ parsed = JSON.parse(body);
3768
+ } catch {
3769
+ return void 0;
3770
+ }
3771
+ if (typeof parsed !== "object" || parsed === null || !("sequenceNums" in parsed) || !Array.isArray(parsed.sequenceNums)) {
3772
+ return void 0;
3773
+ }
3774
+ const sequenceNums = [];
3775
+ const values = parsed.sequenceNums;
3776
+ for (const value of values) {
3777
+ if (typeof value !== "number") {
3778
+ return void 0;
3779
+ }
3780
+ sequenceNums.push(value);
3781
+ }
3782
+ return sequenceNums;
3783
+ }
3784
+ function frontDoorRcControl(transport, token) {
3785
+ const headers = { authorization: `Bearer ${token}` };
3786
+ const sessionsOf = (answer, what) => {
3787
+ if (answer.status !== HTTP_STATUS.ok) {
3788
+ throw new Error(`${what} failed (HTTP ${String(answer.status)}): ${controlErrorOf(answer.body) ?? answer.body}`);
3789
+ }
3790
+ let parsed;
3791
+ try {
3792
+ parsed = JSON.parse(answer.body);
3793
+ } catch {
3794
+ throw new Error(`${what}: the door's answer was not JSON: ${answer.body}`);
3795
+ }
3796
+ if (typeof parsed !== "object" || parsed === null || !("sessions" in parsed) || !Array.isArray(parsed.sessions)) {
3797
+ throw new Error(`${what}: the door's answer named no sessions list: ${answer.body}`);
3798
+ }
3799
+ const candidates = parsed.sessions;
3800
+ const sessions = [];
3801
+ for (const value of candidates) {
3802
+ if (!isRcSessionSummary(value)) {
3803
+ throw new Error(`${what}: the door's answer named no sessions list: ${answer.body}`);
3804
+ }
3805
+ sessions.push(value);
3806
+ }
3807
+ return sessions;
3808
+ };
3809
+ return {
3810
+ listSessions: async () => sessionsOf(await transport.request({ method: "GET", path: `${CONTROL_PATH_PREFIX}/sessions`, headers }), "listing the door's Remote Control sessions"),
3811
+ sendPrompt: async (sessionId, text) => {
3812
+ let answer;
3813
+ try {
3814
+ answer = await transport.request({
3815
+ method: "POST",
3816
+ path: `${CONTROL_PATH_PREFIX}/inject`,
3817
+ headers: { ...headers, "content-type": "application/json" },
3818
+ body: JSON.stringify({ session: sessionId, text })
3819
+ });
3820
+ } catch (error) {
3821
+ return { ok: false, message: `could not reach the front door's control listener: ${error instanceof Error ? error.message : String(error)}` };
3822
+ }
3823
+ if (answer.status === HTTP_STATUS.ok) {
3824
+ const sequenceNums = sequenceNumsOfBody(answer.body);
3825
+ if (sequenceNums === void 0) {
3826
+ return { ok: false, message: `the door accepted the event but its answer named no sequence numbers: ${answer.body}` };
3827
+ }
3828
+ return { ok: true, sequenceNums };
3829
+ }
3830
+ return { ok: false, message: `injecting into ${sessionId} failed (HTTP ${String(answer.status)}): ${controlErrorOf(answer.body) ?? answer.body}` };
3831
+ }
3832
+ };
3833
+ }
3834
+
3311
3835
  // src/frontdoor/ensure.ts
3312
3836
  import { randomUUID as randomUUID3 } from "node:crypto";
3313
3837
 
3314
3838
  // src/headroom/state.ts
3315
- import { createHash as createHash2 } from "node:crypto";
3839
+ import { createHash as createHash3 } from "node:crypto";
3316
3840
  import path8 from "node:path";
3317
3841
  import { z as z4 } from "zod";
3318
3842
  var HEADROOM_ANTHROPIC_UPSTREAM = "https://api.anthropic.com";
@@ -3373,7 +3897,7 @@ function headroomUpstreams(providers, frontDoorOrigin2) {
3373
3897
  return upstreams;
3374
3898
  }
3375
3899
  function hashAllowlist(allowlist) {
3376
- return createHash2("sha256").update(allowlist.join("\n")).digest("hex");
3900
+ return createHash3("sha256").update(allowlist.join("\n")).digest("hex");
3377
3901
  }
3378
3902
  function readAllProviders(fs12, providersDir) {
3379
3903
  const result = [];
@@ -3871,7 +4395,7 @@ function ensureHeadroom(params) {
3871
4395
  }
3872
4396
 
3873
4397
  // src/headroom/settings.ts
3874
- import { createHash as createHash3 } from "node:crypto";
4398
+ import { createHash as createHash4 } from "node:crypto";
3875
4399
  function settingsOf(config) {
3876
4400
  return {
3877
4401
  ...config.mode === void 0 ? {} : { mode: config.mode },
@@ -3896,7 +4420,7 @@ function settingsEnv(settings) {
3896
4420
  return settings.rolloutChannel === void 0 ? {} : { HEADROOM_ROLLOUT_CHANNEL: settings.rolloutChannel };
3897
4421
  }
3898
4422
  function hashSettings(settings) {
3899
- return createHash3("sha256").update(JSON.stringify([...settingsArgs(settings), JSON.stringify(settingsEnv(settings))])).digest("hex");
4423
+ return createHash4("sha256").update(JSON.stringify([...settingsArgs(settings), JSON.stringify(settingsEnv(settings))])).digest("hex");
3900
4424
  }
3901
4425
 
3902
4426
  // src/headroom/socket.ts
@@ -4327,6 +4851,7 @@ function buildLayoutPaths(root) {
4327
4851
  frontdoorCaBundlesDir: path12.join(root, "frontdoor", "ca", "bundles"),
4328
4852
  frontdoorStateFile: path12.join(root, "frontdoor", "state.json"),
4329
4853
  frontdoorLockFile: path12.join(root, "frontdoor", "start.lock"),
4854
+ frontdoorControlTokenFile: path12.join(root, "frontdoor", "control-token"),
4330
4855
  frontdoorSessionsDir: path12.join(root, "frontdoor", "sessions"),
4331
4856
  frontdoorLogPath: path12.join(root, "logs", "frontdoor.log"),
4332
4857
  usageDir: path12.join(root, "usage"),
@@ -5821,7 +6346,7 @@ function collectPoolPick(params) {
5821
6346
 
5822
6347
  // src/realPorts.ts
5823
6348
  import { spawn, spawnSync } from "node:child_process";
5824
- import { createHash as createHash4 } from "node:crypto";
6349
+ import { createHash as createHash5 } from "node:crypto";
5825
6350
  import fs7 from "node:fs";
5826
6351
  import os3 from "node:os";
5827
6352
  import path21 from "node:path";
@@ -6016,7 +6541,7 @@ var realFarmFs = {
6016
6541
  },
6017
6542
  hashFile(filePath) {
6018
6543
  try {
6019
- return createHash4("sha256").update(fs7.readFileSync(filePath)).digest("hex");
6544
+ return createHash5("sha256").update(fs7.readFileSync(filePath)).digest("hex");
6020
6545
  } catch (error) {
6021
6546
  if (isEnoent3(error) || isErrorWithCode(error, "EISDIR")) {
6022
6547
  return void 0;
@@ -8012,7 +8537,7 @@ function probeFrontDoorSync(port, caPem) {
8012
8537
  }
8013
8538
 
8014
8539
  // src/frontdoor/trust.ts
8015
- import { createHash as createHash5 } from "node:crypto";
8540
+ import { createHash as createHash6 } from "node:crypto";
8016
8541
  import path28 from "node:path";
8017
8542
  var BUNDLE_NAME_HEX_DIGITS = 16;
8018
8543
  function resolveTrustBundle(params) {
@@ -8035,7 +8560,7 @@ function resolveTrustBundle(params) {
8035
8560
  }
8036
8561
  const combined = `${inheritedPem.trimEnd()}
8037
8562
  ${caPem}`;
8038
- const bundle = path28.join(bundlesDir, `${createHash5("sha256").update(combined).digest("hex").slice(0, BUNDLE_NAME_HEX_DIGITS)}.pem`);
8563
+ const bundle = path28.join(bundlesDir, `${createHash6("sha256").update(combined).digest("hex").slice(0, BUNDLE_NAME_HEX_DIGITS)}.pem`);
8039
8564
  if (!fs12.exists(bundle)) {
8040
8565
  fs12.mkdirp(bundlesDir);
8041
8566
  fs12.writeFileAtomic(bundle, combined);
@@ -8178,6 +8703,7 @@ export {
8178
8703
  CONNECT_INTERCEPT_HOST,
8179
8704
  CONNECT_INTERCEPT_HOSTS,
8180
8705
  CONNECT_LIMITS,
8706
+ CONTROL_PATH_PREFIX,
8181
8707
  CategoryClassificationOverlaySchema,
8182
8708
  CategoryClassificationSchema,
8183
8709
  CategoryMapSchema,
@@ -8216,6 +8742,8 @@ export {
8216
8742
  ProviderKindMismatchError,
8217
8743
  ProviderNotFoundError,
8218
8744
  ProviderSchema,
8745
+ RC_IDLE_EXPIRY_MS,
8746
+ RC_SESSIONS_PATH_PREFIX,
8219
8747
  ROUTED_PATH_PREFIX,
8220
8748
  UsageError,
8221
8749
  UsageSnapshotError,
@@ -8227,6 +8755,8 @@ export {
8227
8755
  addProvider,
8228
8756
  buildEntryFacts,
8229
8757
  buildLayoutPaths,
8758
+ buildRcEventWriteBody,
8759
+ buildRcUserMessagePayload,
8230
8760
  carryOver,
8231
8761
  checkReportHasWarnings,
8232
8762
  checkReportToJson,
@@ -8234,6 +8764,8 @@ export {
8234
8764
  collectDoctorReport,
8235
8765
  createLeafCache,
8236
8766
  createProfile,
8767
+ createRcControlHandler,
8768
+ createRcSessionTracker,
8237
8769
  describeProviderEndpoint,
8238
8770
  detectAmbientCredential,
8239
8771
  ensureCa,
@@ -8244,8 +8776,10 @@ export {
8244
8776
  formatCheckReport,
8245
8777
  formatDoctorReport,
8246
8778
  forwardableHeaders,
8779
+ frontDoorRcControl,
8247
8780
  generateCa,
8248
8781
  identityExists,
8782
+ injectRcUserMessage,
8249
8783
  isIdentityDirectoryName,
8250
8784
  isInterceptedHost,
8251
8785
  listDirectoryRules,
@@ -8254,11 +8788,14 @@ export {
8254
8788
  listProviders,
8255
8789
  listUsageSnapshots,
8256
8790
  mintLeaf,
8791
+ observingRoutedRoute,
8257
8792
  parseConnectTarget,
8258
8793
  prepareClaudeLaunch,
8259
8794
  prepareLaunch,
8260
8795
  profileExists,
8261
8796
  providerExists,
8797
+ rcInjectResultFromAnswer,
8798
+ rcObserverAsPassthrough,
8262
8799
  readActiveIdentity,
8263
8800
  readDirectoryRules,
8264
8801
  readFarmManifest,
@@ -8270,6 +8807,8 @@ export {
8270
8807
  readUsageSnapshot,
8271
8808
  realConnectCertStore,
8272
8809
  realConnectEffects,
8810
+ realRcControlTransport,
8811
+ realRcEventDial,
8273
8812
  recoverFarm,
8274
8813
  recoveryDiagnostics,
8275
8814
  removeDirectoryRule,