agent-shim 11.62.3 → 11.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +1311 -911
- package/dist/index.cjs +117 -41
- package/dist/index.mjs +117 -41
- package/dist/types/index.d.ts +68 -10
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -3901,12 +3901,13 @@ function pruneDeadFrontDoorSessions(fs14, sessionsDir, listed, isRunning) {
|
|
|
3901
3901
|
}
|
|
3902
3902
|
return removed;
|
|
3903
3903
|
}
|
|
3904
|
-
var FRONT_DOOR_PROTOCOL, FrontDoorSessionSchema, FrontDoorStateSchema, MAX_TCP_PORT, FrontDoorDirectRecordSchema, RECORD_TEMPORARY_FILE;
|
|
3904
|
+
var FRONT_DOOR_PROTOCOL, FRONT_DOOR_HANDOVER_PROTOCOL, FrontDoorSessionSchema, FrontDoorStateSchema, MAX_TCP_PORT, FrontDoorDirectRecordSchema, RECORD_TEMPORARY_FILE;
|
|
3905
3905
|
var init_state = __esm({
|
|
3906
3906
|
"src/frontdoor/state.ts"() {
|
|
3907
3907
|
"use strict";
|
|
3908
3908
|
init_schema();
|
|
3909
|
-
FRONT_DOOR_PROTOCOL =
|
|
3909
|
+
FRONT_DOOR_PROTOCOL = 6;
|
|
3910
|
+
FRONT_DOOR_HANDOVER_PROTOCOL = 6;
|
|
3910
3911
|
FrontDoorSessionSchema = z5.strictObject({ pid: z5.number().int().positive(), startedAt: z5.number(), token: z5.string().min(1), supervisorPid: z5.number().int().positive().optional(), sessionId: z5.string().min(1).optional(), models: ModelsConfigSchema.optional() });
|
|
3911
3912
|
FrontDoorStateSchema = z5.strictObject({
|
|
3912
3913
|
/**
|
|
@@ -4003,18 +4004,24 @@ async function serveFrontDoor(idleShutdownMinutes, ports, options) {
|
|
|
4003
4004
|
...lastConnectPort === void 0 ? {} : { lastConnectPort },
|
|
4004
4005
|
...lastDirectPort === void 0 ? {} : { lastDirectPort }
|
|
4005
4006
|
});
|
|
4007
|
+
const { predecessor } = ports;
|
|
4008
|
+
const sourceOf = (listener, preferredPort) => predecessor === void 0 ? { preferredPort } : { inherit: async () => await predecessor.take(listener) };
|
|
4006
4009
|
const fail = (message) => {
|
|
4007
4010
|
ports.closeRcStreamHub();
|
|
4008
|
-
|
|
4009
|
-
|
|
4011
|
+
if (predecessor === void 0) {
|
|
4012
|
+
writeFrontDoorState(fs14, paths.frontdoorStateFile, { lastError: message, ...sticky() });
|
|
4013
|
+
fs14.removeRecursive(paths.frontdoorLockFile);
|
|
4014
|
+
}
|
|
4010
4015
|
ports.log(`agent-shim frontdoor supervisor: ${message}`);
|
|
4011
4016
|
return 1;
|
|
4012
4017
|
};
|
|
4013
|
-
|
|
4018
|
+
if (predecessor === void 0) {
|
|
4019
|
+
writeFrontDoorState(fs14, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, ...sticky() });
|
|
4020
|
+
}
|
|
4014
4021
|
const previousSticky = lastPort;
|
|
4015
4022
|
let http3;
|
|
4016
4023
|
try {
|
|
4017
|
-
http3 = await ports.startProviderListener(lastPort);
|
|
4024
|
+
http3 = await ports.startProviderListener(sourceOf("provider", lastPort));
|
|
4018
4025
|
} catch (error) {
|
|
4019
4026
|
ports.doorHealth.listenerFailed("provider", error instanceof Error ? error.message : String(error));
|
|
4020
4027
|
return fail(`could not start the provider listener: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -4026,7 +4033,7 @@ async function serveFrontDoor(idleShutdownMinutes, ports, options) {
|
|
|
4026
4033
|
const previousStickyConnect = lastConnectPort;
|
|
4027
4034
|
let connect3;
|
|
4028
4035
|
try {
|
|
4029
|
-
connect3 = await ports.startConnectListener(lastConnectPort);
|
|
4036
|
+
connect3 = await ports.startConnectListener(sourceOf("connect", lastConnectPort));
|
|
4030
4037
|
} catch (error) {
|
|
4031
4038
|
await http3.close();
|
|
4032
4039
|
ports.doorHealth.listenerFailed("connect", error instanceof Error ? error.message : String(error));
|
|
@@ -4039,7 +4046,7 @@ async function serveFrontDoor(idleShutdownMinutes, ports, options) {
|
|
|
4039
4046
|
const previousStickyDirect = lastDirectPort;
|
|
4040
4047
|
let direct;
|
|
4041
4048
|
try {
|
|
4042
|
-
direct = await ports.startDirectListener(lastDirectPort);
|
|
4049
|
+
direct = await ports.startDirectListener(sourceOf("direct", lastDirectPort));
|
|
4043
4050
|
} catch (error) {
|
|
4044
4051
|
await connect3.close();
|
|
4045
4052
|
await http3.close();
|
|
@@ -4053,8 +4060,12 @@ async function serveFrontDoor(idleShutdownMinutes, ports, options) {
|
|
|
4053
4060
|
writeFrontDoorDirectRecord(fs14, paths.frontdoorDirectRecordsDir, { pid: ports.ownPid, directPort: direct.port });
|
|
4054
4061
|
writeFrontDoorState(fs14, paths.frontdoorStateFile, { protocol: FRONT_DOOR_PROTOCOL, supervisorPid: ports.ownPid, port: http3.port, connectPort: connect3.port, directPort: direct.port, ...sticky() });
|
|
4055
4062
|
ports.doorHealth.generation({ providerPort: http3.port, connectPort: connect3.port, directPort: direct.port });
|
|
4056
|
-
|
|
4057
|
-
|
|
4063
|
+
if (predecessor === void 0) {
|
|
4064
|
+
fs14.removeRecursive(paths.frontdoorLockFile);
|
|
4065
|
+
} else {
|
|
4066
|
+
predecessor.serving();
|
|
4067
|
+
}
|
|
4068
|
+
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)}${predecessor === void 0 ? "" : ", taken over from the door it replaced"}`);
|
|
4058
4069
|
const watchdog = ports.startWatchdog?.();
|
|
4059
4070
|
let idleSince;
|
|
4060
4071
|
let ticks = 0;
|
|
@@ -4074,14 +4085,16 @@ async function serveFrontDoor(idleShutdownMinutes, ports, options) {
|
|
|
4074
4085
|
resolve();
|
|
4075
4086
|
};
|
|
4076
4087
|
ports.retire?.addEventListener("abort", wake, { once: true });
|
|
4088
|
+
ports.handover?.requested.addEventListener("abort", wake, { once: true });
|
|
4077
4089
|
void ports.sleep(FRONTDOOR_POLL_MS).then(() => {
|
|
4078
4090
|
ports.retire?.removeEventListener("abort", wake);
|
|
4091
|
+
ports.handover?.requested.removeEventListener("abort", wake);
|
|
4079
4092
|
resolve();
|
|
4080
4093
|
});
|
|
4081
4094
|
});
|
|
4082
4095
|
};
|
|
4083
4096
|
const retireByDrain = async () => {
|
|
4084
|
-
ports.log(`agent-shim frontdoor supervisor ${String(ports.ownPid)}:
|
|
4097
|
+
ports.log(`agent-shim frontdoor supervisor ${String(ports.ownPid)}: releasing the front door's addresses and finishing the connections it carries`);
|
|
4085
4098
|
watchdog?.stop();
|
|
4086
4099
|
ports.stopBackgroundWork?.();
|
|
4087
4100
|
const drained = [direct.drain(), connect3.drain(), http3.drain()];
|
|
@@ -4096,6 +4109,17 @@ async function serveFrontDoor(idleShutdownMinutes, ports, options) {
|
|
|
4096
4109
|
ports.log(`agent-shim frontdoor supervisor ${String(ports.ownPid)}: the connections this door carried have ended; exiting`);
|
|
4097
4110
|
return 0;
|
|
4098
4111
|
};
|
|
4112
|
+
const handOverThenDrain = async (run) => {
|
|
4113
|
+
ports.log(`agent-shim frontdoor supervisor ${String(ports.ownPid)}: asked to hand the front door's listeners to a successor; accepting on them until it serves`);
|
|
4114
|
+
const outcome = await run({ provider: http3.listening, connect: connect3.listening, direct: direct.listening });
|
|
4115
|
+
if (outcome.ok) {
|
|
4116
|
+
ports.log(`agent-shim frontdoor supervisor ${String(ports.ownPid)}: successor ${String(outcome.successorPid)} serves the front door's listeners`);
|
|
4117
|
+
ports.doorHealth.superseded(outcome.successorPid);
|
|
4118
|
+
} else {
|
|
4119
|
+
ports.log(`agent-shim frontdoor supervisor ${String(ports.ownPid)}: the handover failed (${outcome.reason}); retiring by drain so a successor is started on the released addresses`);
|
|
4120
|
+
}
|
|
4121
|
+
return await retireByDrain();
|
|
4122
|
+
};
|
|
4099
4123
|
const closeServing = async () => {
|
|
4100
4124
|
watchdog?.stop();
|
|
4101
4125
|
await direct.close();
|
|
@@ -4113,8 +4137,12 @@ async function serveFrontDoor(idleShutdownMinutes, ports, options) {
|
|
|
4113
4137
|
};
|
|
4114
4138
|
for (; ; ) {
|
|
4115
4139
|
if (ports.retire?.aborted === true) {
|
|
4140
|
+
ports.log(`agent-shim frontdoor supervisor ${String(ports.ownPid)}: asked to exit`);
|
|
4116
4141
|
return await retireByDrain();
|
|
4117
4142
|
}
|
|
4143
|
+
if (ports.handover?.requested.aborted === true && supersededBy === void 0) {
|
|
4144
|
+
return await handOverThenDrain(ports.handover.run);
|
|
4145
|
+
}
|
|
4118
4146
|
if (options.tickLimit !== void 0 && ticks >= options.tickLimit) {
|
|
4119
4147
|
watchdog?.stop();
|
|
4120
4148
|
return FRONTDOOR_SUPERVISOR_STILL_RUNNING;
|
|
@@ -4252,8 +4280,10 @@ function caParses(ca) {
|
|
|
4252
4280
|
return false;
|
|
4253
4281
|
}
|
|
4254
4282
|
}
|
|
4255
|
-
async function startConnectServer(config, effects,
|
|
4283
|
+
async function startConnectServer(config, effects, source) {
|
|
4256
4284
|
const tunnelTokens = /* @__PURE__ */ new WeakMap();
|
|
4285
|
+
const ledger = createConnectionLedger(config.limits, config.isLiveCapability);
|
|
4286
|
+
const lateSessionDeadlines = /* @__PURE__ */ new WeakMap();
|
|
4257
4287
|
const onUpgrade = (host) => (request4, socket, head) => {
|
|
4258
4288
|
config.capture?.upgrade(request4);
|
|
4259
4289
|
if (head.length > 0) {
|
|
@@ -4275,6 +4305,12 @@ ${request4.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
|
|
|
4275
4305
|
return { host, serveSecure: serveSecure2, tlsAcceptor: tlsAcceptor2 };
|
|
4276
4306
|
}
|
|
4277
4307
|
const handleHostRequest = async (request4, response) => {
|
|
4308
|
+
if (ledger.retiring()) {
|
|
4309
|
+
clearTimeout(lateSessionDeadlines.get(request4.socket));
|
|
4310
|
+
response.once("close", () => {
|
|
4311
|
+
request4.socket.end();
|
|
4312
|
+
});
|
|
4313
|
+
}
|
|
4278
4314
|
if (config.localSurface !== void 0 && await config.localSurface.serve(host, request4, response)) {
|
|
4279
4315
|
return;
|
|
4280
4316
|
}
|
|
@@ -4294,13 +4330,19 @@ ${request4.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
|
|
|
4294
4330
|
const httpSession = effects.createHttpSession(handler, onUpgrade(host));
|
|
4295
4331
|
const serveSecure = (secure, capability) => {
|
|
4296
4332
|
tunnelTokens.set(secure, capability);
|
|
4333
|
+
if (ledger.retiring()) {
|
|
4334
|
+
const deadline = setTimeout(() => {
|
|
4335
|
+
secure.destroy();
|
|
4336
|
+
}, config.limits.headDeadlineMs);
|
|
4337
|
+
deadline.unref();
|
|
4338
|
+
lateSessionDeadlines.set(secure, deadline);
|
|
4339
|
+
}
|
|
4297
4340
|
httpSession.serve(secure);
|
|
4298
4341
|
};
|
|
4299
4342
|
const tlsAcceptor = effects.createTlsAcceptor(config.leafFor(host), serveSecure);
|
|
4300
4343
|
return { host, serveSecure, httpSession, tlsAcceptor };
|
|
4301
4344
|
});
|
|
4302
|
-
const
|
|
4303
|
-
const listener = await effects.listenLoopback(preferredPort, (socket) => {
|
|
4345
|
+
const listener = await effects.listenLoopback(source, (socket) => {
|
|
4304
4346
|
handleConnect(socket, { config, effects, sessions, ledger });
|
|
4305
4347
|
});
|
|
4306
4348
|
const apiSession = sessions.find((session) => session.host === config.routedHost);
|
|
@@ -4313,7 +4355,7 @@ ${request4.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
|
|
|
4313
4355
|
}
|
|
4314
4356
|
session.serveSecure(secure, config.transparentCapability ?? "");
|
|
4315
4357
|
});
|
|
4316
|
-
const transparent = config.transparentPort === void 0 || apiSession === void 0 ? void 0 : await effects.listenLoopback(config.transparentPort, (socket) => {
|
|
4358
|
+
const transparent = config.transparentPort === void 0 || apiSession === void 0 ? void 0 : await effects.listenLoopback({ preferredPort: config.transparentPort }, (socket) => {
|
|
4317
4359
|
sniAcceptor.accept(socket);
|
|
4318
4360
|
});
|
|
4319
4361
|
const closeSurface = async () => {
|
|
@@ -4328,6 +4370,7 @@ ${request4.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
|
|
|
4328
4370
|
};
|
|
4329
4371
|
return {
|
|
4330
4372
|
port: listener.port,
|
|
4373
|
+
listening: listener.listening,
|
|
4331
4374
|
...transparent === void 0 ? {} : { transparentPort: transparent.port },
|
|
4332
4375
|
openTunnels: ledger.openTunnels,
|
|
4333
4376
|
drain: async () => {
|
|
@@ -4344,7 +4387,9 @@ function createConnectionLedger(limits, isLiveCapability2) {
|
|
|
4344
4387
|
const authenticated = /* @__PURE__ */ new Map();
|
|
4345
4388
|
let tunnels = 0;
|
|
4346
4389
|
const blind = /* @__PURE__ */ new Set();
|
|
4347
|
-
|
|
4390
|
+
const live = /* @__PURE__ */ new Set();
|
|
4391
|
+
let retiring = false;
|
|
4392
|
+
let onNoConnections;
|
|
4348
4393
|
const leavePending = (socket) => {
|
|
4349
4394
|
const deadline = pending.get(socket);
|
|
4350
4395
|
if (deadline !== void 0) {
|
|
@@ -4376,20 +4421,19 @@ function createConnectionLedger(limits, isLiveCapability2) {
|
|
|
4376
4421
|
refuse(socket, REFUSAL.requestTimeout);
|
|
4377
4422
|
}, limits.headDeadlineMs)
|
|
4378
4423
|
);
|
|
4424
|
+
live.add(socket);
|
|
4379
4425
|
socket.once("close", () => {
|
|
4380
4426
|
leavePending(socket);
|
|
4427
|
+
live.delete(socket);
|
|
4428
|
+
if (live.size === 0) {
|
|
4429
|
+
onNoConnections?.();
|
|
4430
|
+
}
|
|
4381
4431
|
});
|
|
4382
4432
|
},
|
|
4383
4433
|
headReceived: leavePending,
|
|
4384
4434
|
openTunnels: () => tunnels,
|
|
4385
4435
|
retire: async () => {
|
|
4386
|
-
|
|
4387
|
-
socket.destroy();
|
|
4388
|
-
}
|
|
4389
|
-
for (const deadline of pending.values()) {
|
|
4390
|
-
clearTimeout(deadline);
|
|
4391
|
-
}
|
|
4392
|
-
pending.clear();
|
|
4436
|
+
retiring = true;
|
|
4393
4437
|
for (const sockets of authenticated.values()) {
|
|
4394
4438
|
for (const socket of sockets) {
|
|
4395
4439
|
if (!blind.has(socket)) {
|
|
@@ -4397,12 +4441,13 @@ function createConnectionLedger(limits, isLiveCapability2) {
|
|
|
4397
4441
|
}
|
|
4398
4442
|
}
|
|
4399
4443
|
}
|
|
4400
|
-
if (
|
|
4444
|
+
if (live.size > 0) {
|
|
4401
4445
|
await new Promise((resolve) => {
|
|
4402
|
-
|
|
4446
|
+
onNoConnections = resolve;
|
|
4403
4447
|
});
|
|
4404
4448
|
}
|
|
4405
4449
|
},
|
|
4450
|
+
retiring: () => retiring,
|
|
4406
4451
|
establish: (socket, capability, kind) => {
|
|
4407
4452
|
if (tunnels >= limits.maxTunnels) {
|
|
4408
4453
|
return false;
|
|
@@ -4417,9 +4462,6 @@ function createConnectionLedger(limits, isLiveCapability2) {
|
|
|
4417
4462
|
socket.once("close", () => {
|
|
4418
4463
|
tunnels -= 1;
|
|
4419
4464
|
blind.delete(socket);
|
|
4420
|
-
if (blind.size === 0) {
|
|
4421
|
-
onNoBlindTunnels?.();
|
|
4422
|
-
}
|
|
4423
4465
|
sockets.delete(socket);
|
|
4424
4466
|
if (sockets.size === 0 && authenticated.get(capability) === sockets) {
|
|
4425
4467
|
authenticated.delete(capability);
|
|
@@ -4627,7 +4669,7 @@ var init_package = __esm({
|
|
|
4627
4669
|
"package.json"() {
|
|
4628
4670
|
package_default = {
|
|
4629
4671
|
name: "agent-shim",
|
|
4630
|
-
version: "11.
|
|
4672
|
+
version: "11.63.0",
|
|
4631
4673
|
description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
|
|
4632
4674
|
license: "MIT",
|
|
4633
4675
|
author: "Joseph Mearman <joseph@mearman.co.uk>",
|
|
@@ -10154,7 +10196,8 @@ var init_replacement = __esm({
|
|
|
10154
10196
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
10155
10197
|
function ensureFrontDoor(params) {
|
|
10156
10198
|
const { paths, ports } = params;
|
|
10157
|
-
const
|
|
10199
|
+
const startedAt = ports.now();
|
|
10200
|
+
let deadline = startedAt + FRONTDOOR_START_TIMEOUT_MS;
|
|
10158
10201
|
let spawned = false;
|
|
10159
10202
|
let replaced = false;
|
|
10160
10203
|
const distrusted = /* @__PURE__ */ new Set();
|
|
@@ -10194,6 +10237,19 @@ function ensureFrontDoor(params) {
|
|
|
10194
10237
|
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
10195
10238
|
}
|
|
10196
10239
|
};
|
|
10240
|
+
const handOverAndAwait = (supervisorPid, doorProtocol, reason, request4) => {
|
|
10241
|
+
recordReplacement(supervisorPid, doorProtocol, reason, "SIGUSR2");
|
|
10242
|
+
request4(supervisorPid);
|
|
10243
|
+
const waitEnds = ports.now() + FRONTDOOR_START_TIMEOUT_MS + FRONTDOOR_EXIT_GRACE_MS;
|
|
10244
|
+
deadline += FRONTDOOR_START_TIMEOUT_MS + FRONTDOOR_EXIT_GRACE_MS;
|
|
10245
|
+
while (ports.isRunning(supervisorPid) && readFrontDoorState(ports.fs, paths.frontdoorStateFile)?.supervisorPid === supervisorPid) {
|
|
10246
|
+
if (ports.now() >= waitEnds) {
|
|
10247
|
+
stopAndAwaitExit(supervisorPid, doorProtocol, reason, `the front door (pid ${String(supervisorPid)}) did not act on a request to hand its listeners over, and did not exit when asked to`);
|
|
10248
|
+
return;
|
|
10249
|
+
}
|
|
10250
|
+
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
10251
|
+
}
|
|
10252
|
+
};
|
|
10197
10253
|
for (; ; ) {
|
|
10198
10254
|
const state = readFrontDoorState(ports.fs, paths.frontdoorStateFile);
|
|
10199
10255
|
const supervisorPid = state?.supervisorPid;
|
|
@@ -10207,12 +10263,16 @@ function ensureFrontDoor(params) {
|
|
|
10207
10263
|
const verdict = verifyWithRetry(state.port);
|
|
10208
10264
|
const doorProtocol = state.protocol ?? 0;
|
|
10209
10265
|
if (verdict.ok && !replaced && !spawned && (params.replace !== void 0 || doorProtocol < FRONT_DOOR_PROTOCOL)) {
|
|
10210
|
-
|
|
10211
|
-
supervisorPid,
|
|
10212
|
-
|
|
10213
|
-
|
|
10214
|
-
|
|
10215
|
-
|
|
10266
|
+
if (params.replace !== void 0 && doorProtocol >= FRONT_DOOR_HANDOVER_PROTOCOL && ports.requestHandover !== void 0 && ports.isFrontDoorSupervisor(supervisorPid)) {
|
|
10267
|
+
handOverAndAwait(supervisorPid, doorProtocol, params.replace, ports.requestHandover);
|
|
10268
|
+
} else {
|
|
10269
|
+
stopAndAwaitExit(
|
|
10270
|
+
supervisorPid,
|
|
10271
|
+
doorProtocol,
|
|
10272
|
+
params.replace ?? "protocol-older",
|
|
10273
|
+
`the front door (pid ${String(supervisorPid)}, protocol ${String(doorProtocol)}; this launcher speaks protocol ${String(FRONT_DOOR_PROTOCOL)}) was asked to be replaced and did not exit`
|
|
10274
|
+
);
|
|
10275
|
+
}
|
|
10216
10276
|
replaced = true;
|
|
10217
10277
|
continue;
|
|
10218
10278
|
}
|
|
@@ -10264,7 +10324,7 @@ function ensureFrontDoor(params) {
|
|
|
10264
10324
|
if (ports.now() >= deadline) {
|
|
10265
10325
|
const lastError = state?.lastError;
|
|
10266
10326
|
throw new FrontDoorStartError(
|
|
10267
|
-
`the front door did not become ready within ${String(
|
|
10327
|
+
`the front door did not become ready within ${String(deadline - startedAt)}ms` + (lastError === void 0 ? "" : ` (last error: ${lastError})`) + (lastRejection === void 0 ? "" : ` (${lastRejection})`) + (busyDoor === void 0 ? "" : `. It was not replaced because ${String(busyDoor.sessions)} live session(s) are using it and would lose their connections; run \`agent-shim frontdoor restart\` to replace it deliberately`) + `. Daemon log: ${paths.frontdoorLogPath}`
|
|
10268
10328
|
);
|
|
10269
10329
|
}
|
|
10270
10330
|
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
@@ -11907,6 +11967,12 @@ function realEnsurePorts(paths, spawnDaemon) {
|
|
|
11907
11967
|
killSupervisor: (pid) => {
|
|
11908
11968
|
process.kill(pid, "SIGKILL");
|
|
11909
11969
|
},
|
|
11970
|
+
// Windows has no user signals (Node refuses to send one), so a replacement there keeps the stop-and-start path.
|
|
11971
|
+
...process.platform === "win32" ? {} : {
|
|
11972
|
+
requestHandover: (pid) => {
|
|
11973
|
+
process.kill(pid, "SIGUSR2");
|
|
11974
|
+
}
|
|
11975
|
+
},
|
|
11910
11976
|
// Read fresh for each probe: a replacement supervisor may have regenerated an unparseable CA, and the probe must trust exactly what the serving door's leaf chains to.
|
|
11911
11977
|
verifyListener: (port) => probeFrontDoorSync(port, fs13.readFileSync(paths.frontdoorCaCertFile, "utf8"))
|
|
11912
11978
|
};
|
|
@@ -14336,7 +14402,7 @@ function realRcStreamDial(target = { host: CONNECT_INTERCEPT_HOST, port: HTTPS_P
|
|
|
14336
14402
|
function realConnectEffects(ports = DOOR_SOURCE_PORTS) {
|
|
14337
14403
|
const plainAgent = new ExemptHttpAgent(ports);
|
|
14338
14404
|
const tlsAgent = new ExemptTlsAgent(ports);
|
|
14339
|
-
const listenOnce = async (
|
|
14405
|
+
const listenOnce = async (target, onSocket) => await new Promise((resolve, reject) => {
|
|
14340
14406
|
const accepted = /* @__PURE__ */ new Set();
|
|
14341
14407
|
const server = net2.createServer((socket) => {
|
|
14342
14408
|
accepted.add(socket);
|
|
@@ -14346,7 +14412,7 @@ function realConnectEffects(ports = DOOR_SOURCE_PORTS) {
|
|
|
14346
14412
|
onSocket(socket);
|
|
14347
14413
|
});
|
|
14348
14414
|
server.once("error", reject);
|
|
14349
|
-
|
|
14415
|
+
const onListening = () => {
|
|
14350
14416
|
const address = server.address();
|
|
14351
14417
|
const bound = typeof address === "object" && address !== null ? address.port : 0;
|
|
14352
14418
|
if (bound === 0) {
|
|
@@ -14355,6 +14421,7 @@ function realConnectEffects(ports = DOOR_SOURCE_PORTS) {
|
|
|
14355
14421
|
}
|
|
14356
14422
|
resolve({
|
|
14357
14423
|
port: bound,
|
|
14424
|
+
listening: server,
|
|
14358
14425
|
// The listening socket is released by `close` at once; the connections already accepted are untouched and keep the server's `close` callback pending until they end.
|
|
14359
14426
|
stopAccepting: () => {
|
|
14360
14427
|
server.close();
|
|
@@ -14370,10 +14437,19 @@ function realConnectEffects(ports = DOOR_SOURCE_PORTS) {
|
|
|
14370
14437
|
});
|
|
14371
14438
|
}
|
|
14372
14439
|
});
|
|
14373
|
-
}
|
|
14440
|
+
};
|
|
14441
|
+
if (typeof target === "number") {
|
|
14442
|
+
server.listen(target, "127.0.0.1", onListening);
|
|
14443
|
+
} else {
|
|
14444
|
+
server.listen(target, onListening);
|
|
14445
|
+
}
|
|
14374
14446
|
});
|
|
14375
14447
|
return {
|
|
14376
|
-
listenLoopback: async (
|
|
14448
|
+
listenLoopback: async (source, onSocket) => {
|
|
14449
|
+
if ("inherit" in source) {
|
|
14450
|
+
return await listenOnce(await source.inherit(), onSocket);
|
|
14451
|
+
}
|
|
14452
|
+
const { preferredPort } = source;
|
|
14377
14453
|
if (preferredPort !== void 0) {
|
|
14378
14454
|
try {
|
|
14379
14455
|
return await listenOnce(preferredPort, onSocket);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as zod from 'zod';
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'node:http';
|
|
6
6
|
import * as net from 'node:net';
|
|
7
|
+
import net__default from 'node:net';
|
|
7
8
|
import { Duplex, Readable } from 'node:stream';
|
|
8
9
|
import * as _orpc_client from '@orpc/client';
|
|
9
10
|
import * as zod_v4_core from 'zod/v4/core';
|
|
@@ -5785,6 +5786,42 @@ interface ConnectCapture {
|
|
|
5785
5786
|
readonly upgrade: (request: CapturedRequestShape) => void;
|
|
5786
5787
|
}
|
|
5787
5788
|
|
|
5789
|
+
/**
|
|
5790
|
+
* The listening-socket handover between a serving front door and the successor it starts for a planned replacement (`agent-shim frontdoor restart`, `agent-shim update --restart-door`), so the sticky addresses never stop accepting: the kernel's accept queue for each listener is held by the old door until the successor holds it too, and only then does the old door stop accepting and drain.
|
|
5791
|
+
*
|
|
5792
|
+
* The two doors speak over the IPC channel Node opens between a parent and the child it spawns with one, and each listening socket crosses it as a handle attached to a message (`child.send(message, server)`), which is the only way Node passes a listening socket between processes. The successor asks for each listener only once its own server for that listener is built, and adopts the handle in the same turn the message is delivered in: Node starts accepting on a received listening handle inside a server of its own before it hands the handle over, and a connection accepted there before the successor's server adopts it would reach a server with no connection listener and hang. Asking per listener keeps every adoption inside one delivery.
|
|
5793
|
+
*
|
|
5794
|
+
* The exchange, in order: the successor sends `take` naming one listener; the old door answers with `listener` naming it and carrying the handle; once the successor has adopted all three and recorded itself as the owner in the state file, it sends `serving` and ends the channel. The old door then drains exactly as it does on `SIGTERM`. Any other outcome (the successor exits or ends the channel early, sends something this release cannot read, or is still not serving when the budget ends) ends the successor if it is still running and leaves the old door to drain the ordinary way, so the replacement still happens, through the path that has a gap.
|
|
5795
|
+
*/
|
|
5796
|
+
/** Where a listener's socket comes from: bound afresh, preferring the sticky port and taking any free one when it is held, or adopted from the door being replaced. */
|
|
5797
|
+
type ListenSource = {
|
|
5798
|
+
readonly preferredPort: number | undefined;
|
|
5799
|
+
} | {
|
|
5800
|
+
readonly inherit: () => Promise<net__default.Server>;
|
|
5801
|
+
};
|
|
5802
|
+
/** The listeners a door hands over: the HTTPS provider listener, the CONNECT surface, and the plain-HTTP direct listener. */
|
|
5803
|
+
declare const HANDOVER_LISTENERS: readonly ["provider", "connect", "direct"];
|
|
5804
|
+
type HandoverListenerName = (typeof HANDOVER_LISTENERS)[number];
|
|
5805
|
+
/** The listening sockets a door hands over, one per listener. */
|
|
5806
|
+
type HandoverSockets = Readonly<Record<HandoverListenerName, net__default.Server>>;
|
|
5807
|
+
/** How a handover ended, as the old door sees it. */
|
|
5808
|
+
type HandoverOutcome = {
|
|
5809
|
+
readonly ok: true;
|
|
5810
|
+
readonly successorPid: number;
|
|
5811
|
+
} | {
|
|
5812
|
+
readonly ok: false;
|
|
5813
|
+
readonly reason: string;
|
|
5814
|
+
};
|
|
5815
|
+
/** What a successor door does with the channel to the door it replaces: take each listener's socket, then report that it serves. */
|
|
5816
|
+
interface Predecessor {
|
|
5817
|
+
/**
|
|
5818
|
+
* Asks for one listener's socket and resolves with it in the turn the old door's answer is delivered in, so the caller's continuation (which must adopt it at once, see the module comment) runs before the event loop can accept a connection on it. Rejects when the channel ends first or the answer cannot be read.
|
|
5819
|
+
*/
|
|
5820
|
+
readonly take: (listener: HandoverListenerName) => Promise<net__default.Server>;
|
|
5821
|
+
/** Tells the old door this door accepts on every listener and owns the state file, then ends the channel. */
|
|
5822
|
+
readonly serving: () => void;
|
|
5823
|
+
}
|
|
5824
|
+
|
|
5788
5825
|
/**
|
|
5789
5826
|
* The front door's CONNECT surface, so an OAuth session can have agent-shim's routing AND Claude Code's Remote Control at once: Remote Control refuses any `ANTHROPIC_BASE_URL` other than the real API, but happily honours `HTTPS_PROXY`, so OAuth launches route at the proxy layer instead.
|
|
5790
5827
|
*
|
|
@@ -5892,6 +5929,8 @@ type ConnectRequestHandler = (request: IncomingMessage, response: ServerResponse
|
|
|
5892
5929
|
/** A bound loopback listener and how to stop it. */
|
|
5893
5930
|
interface ConnectListenerHandle {
|
|
5894
5931
|
readonly port: number;
|
|
5932
|
+
/** The listening socket itself, which a planned replacement hands to the successor door (see `./handover.ts`). */
|
|
5933
|
+
readonly listening: net.Server;
|
|
5895
5934
|
/** Stops accepting and releases the port at once, leaving every connection already accepted open: a successor can bind the same port while they finish. */
|
|
5896
5935
|
readonly stopAccepting: () => void;
|
|
5897
5936
|
/** Stops accepting, ends every live connection, and resolves once the port is released. */
|
|
@@ -5920,8 +5959,8 @@ interface HttpParserSession {
|
|
|
5920
5959
|
* Every effect the MITM proxy performs, injected so the routing decisions and lifecycle run against fakes in unit tests (the SupervisorPorts pattern): TCP listening, TLS termination, HTTP parsing, plain TCP tunnels, and request forwarding. The real implementation is `realConnectEffects`; the TLS round-trip test uses it with redirected targets.
|
|
5921
5960
|
*/
|
|
5922
5961
|
interface ConnectEffects {
|
|
5923
|
-
/** Binds a loopback TCP listener
|
|
5924
|
-
readonly listenLoopback: (
|
|
5962
|
+
/** Binds a loopback TCP listener, or adopts the one `source` inherits from the door being replaced; a bound one tries the preferred port first and uses any free port when it is taken (bind, do not probe: check-then-bind races). Resolves with the bound port and a close handle. */
|
|
5963
|
+
readonly listenLoopback: (source: ListenSource, onSocket: (socket: net.Socket) => void) => Promise<ConnectListenerHandle>;
|
|
5925
5964
|
/** Builds the TLS terminator for one leaf, handing each successfully handshaked session to `onSecure` together with the context its `accept` call carried. `alpnProtocols`, when given, is advertised during the handshake, which is how a tap host's session can offer HTTP/2 the way its real server does. */
|
|
5926
5965
|
readonly createTlsAcceptor: (leaf: LeafCert, onSecure: (secure: net.Socket, context: string) => void, alpnProtocols?: readonly string[]) => TlsAcceptor;
|
|
5927
5966
|
/** Builds the transparent surface's TLS terminator: one handshake whose SNI name selects which served host's leaf answers it, so a connection redirected from any of the hosts sharing the redirected address is terminated as the host it named. A name the surface does not serve is answered with no certificate at all (a client that sends no SNI never reaches the selection and finds no default certificate either), because the only leaf that could answer such a name is another host's, which the client's own hostname check would reject anyway. No ALPN is offered: node's TLS server negotiates ALPN from the listener's own options and ignores protocols named on an SNI-selected secure context, and the API host's terminated session is parsed as HTTP/1.1, which a negotiated `h2` would strand. */
|
|
@@ -5975,12 +6014,14 @@ interface ConnectServerConfig {
|
|
|
5975
6014
|
/** A running connect surface. */
|
|
5976
6015
|
interface ConnectServerHandle {
|
|
5977
6016
|
readonly port: number;
|
|
6017
|
+
/** The CONNECT listener's listening socket, which a planned replacement hands to the successor door (see `./handover.ts`). The transparent listener is not handed over: it exists only when the door's own environment asks for it, which a successor decides for itself. */
|
|
6018
|
+
readonly listening: net.Server;
|
|
5978
6019
|
/** The transparent listener's port, when one was requested: what an operating-system redirect should point at. */
|
|
5979
6020
|
readonly transparentPort?: number;
|
|
5980
6021
|
/** How many authenticated connections (blind tunnels and terminated sessions together) are open now: what closing this surface, or exiting its process, drops. */
|
|
5981
6022
|
readonly openTunnels: () => number;
|
|
5982
6023
|
/**
|
|
5983
|
-
* Retires the surface without cutting off the tunnels it carries: the port is released at once so a successor can bind it,
|
|
6024
|
+
* Retires the surface without cutting off the tunnels it carries: the port is released at once so a successor can bind it, the terminated sessions (which carry this door's own routing state) are ended so their clients reconnect to the successor, and the blind tunnels, which are byte pumps to hosts the door never looks into, keep running until they end by themselves or their launch ends. A connection the listener accepted before it stopped but whose head has not arrived yet is still answered: its blind tunnel runs like any other, its terminated session serves one request and ends, and one whose head never comes is refused at its deadline. Resolves when the last connection has closed, with the surface fully closed.
|
|
5984
6025
|
*/
|
|
5985
6026
|
readonly drain: () => Promise<void>;
|
|
5986
6027
|
/** Stops the listener, drops every live tunnel and terminated session, and resolves once the port is released. */
|
|
@@ -6002,7 +6043,7 @@ interface ConnectLocalSurface {
|
|
|
6002
6043
|
*
|
|
6003
6044
|
* The connection flow: read the CONNECT head within the deadline (never more of the stream than that, so an early ClientHello glued to the head is pushed back with `unshift` and still seen by whatever consumes the socket next), authenticate it, then either blind-tunnel the target or terminate its TLS and parse HTTP on the session. Every network effect flows through `effects`; nothing here touches the network or filesystem itself.
|
|
6004
6045
|
*/
|
|
6005
|
-
declare function startConnectServer(config: ConnectServerConfig, effects: ConnectEffects,
|
|
6046
|
+
declare function startConnectServer(config: ConnectServerConfig, effects: ConnectEffects, source: ListenSource): Promise<ConnectServerHandle>;
|
|
6006
6047
|
/** The real `ConnectCertStore` on the filesystem: `ca.pem` world-readable (it is public material), `ca.key` mode 0600 exactly, chmod'ed after the write because a create's mode bits pass through the process umask. */
|
|
6007
6048
|
declare function realConnectCertStore(paths: {
|
|
6008
6049
|
readonly frontdoorCaDir: string;
|
|
@@ -23843,6 +23884,10 @@ interface EnsureFrontDoorPorts {
|
|
|
23843
23884
|
readonly verifyListener: (port: number) => ListenerVerdict;
|
|
23844
23885
|
/** Asks a running front-door supervisor to exit, without waiting for it to: the caller polls `isRunning`. Only ever called for a pid whose recorded listener has just authenticated as agent-shim's own, or whose process is agent-shim's front-door supervisor (`isFrontDoorSupervisor`). */
|
|
23845
23886
|
readonly stopSupervisor: (pid: number) => void;
|
|
23887
|
+
/**
|
|
23888
|
+
* Asks a running front-door supervisor to hand its listening sockets to a successor it starts itself (see `./handover.ts`), without waiting for it to: the caller polls the state file for the successor. Only ever called for a pid whose listener has just authenticated, whose recorded protocol is `FRONT_DOOR_HANDOVER_PROTOCOL` or later, and whose process is agent-shim's own front-door supervisor, the process that installs the handler for the request. Absent where the platform has no way to deliver the request (Windows, which has no user signals), so every replacement there takes the stop-and-start path.
|
|
23889
|
+
*/
|
|
23890
|
+
readonly requestHandover?: (pid: number) => void;
|
|
23846
23891
|
/** Appends one line to the front door's own log. Called before every signal sent to a door, so the door's log names the cause of its own exit. */
|
|
23847
23892
|
readonly recordReplacement: (line: string) => void;
|
|
23848
23893
|
/** Ends a supervisor that did not exit when asked to, without giving it the chance to refuse: a door whose event loop is blocked never runs its signal handler. Called only for a pid `isFrontDoorSupervisor` has just confirmed, after `stopSupervisor` and a grace period. */
|
|
@@ -23878,6 +23923,8 @@ interface DoorWatchdog {
|
|
|
23878
23923
|
/** A bound front-door listener in this process, and how to stop it. */
|
|
23879
23924
|
interface FrontDoorListenerHandle {
|
|
23880
23925
|
readonly port: number;
|
|
23926
|
+
/** The listening socket itself, which a planned replacement hands to the successor door (see `./handover.ts`). */
|
|
23927
|
+
readonly listening: net__default.Server;
|
|
23881
23928
|
/** Stops accepting, ends every live connection, and resolves once the port is released. */
|
|
23882
23929
|
readonly close: () => Promise<void>;
|
|
23883
23930
|
/** Releases the port at once (before the first await) and leaves the work already in progress to finish; resolves when the listener has nothing left to carry. */
|
|
@@ -23911,17 +23958,17 @@ interface FrontDoorSupervisorPorts {
|
|
|
23911
23958
|
*/
|
|
23912
23959
|
readonly runningAmong: (pids: readonly number[]) => Promise<ReadonlySet<number>>;
|
|
23913
23960
|
/**
|
|
23914
|
-
* Starts the provider listener in this process
|
|
23961
|
+
* Starts the provider listener in this process from `source`: bound on its preferred port (falling back to any free port when the sticky one is taken, decided at bind time rather than by a racy check-then-bind probe), or adopted from the door this one replaces. Resolves once it accepts and has answered its own health probe over TLS. This is the HTTPS listener every provider session's base URL points at, serving a loopback leaf signed by agent-shim's CA.
|
|
23915
23962
|
*/
|
|
23916
|
-
readonly startProviderListener: (
|
|
23963
|
+
readonly startProviderListener: (source: ListenSource) => Promise<FrontDoorListenerHandle>;
|
|
23917
23964
|
/**
|
|
23918
|
-
* Starts the CONNECT surface in this process
|
|
23965
|
+
* Starts the CONNECT surface in this process from `source` (same bind-time fallback, or adopted): the TLS-terminating listener an OAuth session's HTTPS_PROXY points at. Bound before any state names it, so an OAuth launch never finds one listener up without the other.
|
|
23919
23966
|
*/
|
|
23920
|
-
readonly startConnectListener: (
|
|
23967
|
+
readonly startConnectListener: (source: ListenSource) => Promise<FrontDoorListenerHandle>;
|
|
23921
23968
|
/**
|
|
23922
|
-
* Starts the direct listener
|
|
23969
|
+
* Starts the direct listener from `source` (same bind-time fallback, or adopted): the same routes as the provider listener, minus the headroom hop, over plain HTTP because headroom is what connects to it. Headroom forwards routed traffic back here, which is what keeps the hop from looping, and the port is sticky like the others because headroom's allowlist admits its exact origin. It admits only the hop's own requests and holds no credential until it redeems one from the hop's custody.
|
|
23923
23970
|
*/
|
|
23924
|
-
readonly startDirectListener: (
|
|
23971
|
+
readonly startDirectListener: (source: ListenSource) => Promise<FrontDoorListenerHandle>;
|
|
23925
23972
|
/**
|
|
23926
23973
|
* Stops the Remote Control stream hub: persists every live attachment's sequence cursor synchronously, then frees the dials. Called exactly once on every exit the supervisor owns, after every listener has stopped accepting. A closing exit has every listener down by then; a drain still has responses settling, which is safe because the hub refuses to attach once it is closed (otherwise the reconcile after an observed exchange would re-attach a stream the close had just ended).
|
|
23927
23974
|
*/
|
|
@@ -23934,6 +23981,17 @@ interface FrontDoorSupervisorPorts {
|
|
|
23934
23981
|
* Aborted when the process is asked to exit (the CLI's `__frontdoor-supervisor` aborts it on its first SIGTERM or SIGINT). The door then retires by draining: it releases its listening addresses at once, so a successor can bind the same sticky ports, ends what holds its own routing state, and stays up only as long as the connections it still carries (blind tunnels, a response being streamed) take to end. Optional for a host that decides when the door stops by other means.
|
|
23935
23982
|
*/
|
|
23936
23983
|
readonly retire?: AbortSignal;
|
|
23984
|
+
/**
|
|
23985
|
+
* A planned replacement that keeps every address accepting (see `./handover.ts`): `requested` is aborted when a launcher asks this door to hand its listeners to a successor (the CLI's `__frontdoor-supervisor` aborts it on `SIGUSR2`), and `run` starts the successor, hands it the three listening sockets and resolves once it serves them or with why it did not. The door keeps accepting throughout and drains afterwards either way, so a failed handover still ends in the ordinary replacement. Optional because only a door whose process can start a successor of itself can offer it.
|
|
23986
|
+
*/
|
|
23987
|
+
readonly handover?: {
|
|
23988
|
+
readonly requested: AbortSignal;
|
|
23989
|
+
readonly run: (sockets: HandoverSockets) => Promise<HandoverOutcome>;
|
|
23990
|
+
};
|
|
23991
|
+
/**
|
|
23992
|
+
* The door this one replaces through a handover, when it was started for one: its listeners are adopted from that door's sockets instead of bound, the state file is not written until this door accepts on all three (the old door owns it and keeps serving until then), and the old door is told once this door serves. Absent for every other start.
|
|
23993
|
+
*/
|
|
23994
|
+
readonly predecessor?: Predecessor;
|
|
23937
23995
|
/** Stops the work the door runs on its own account rather than for a request (the usage, window, job and catalogue keepers), called once when a drain begins so the successor's are the only ones acting. Optional because a host without such work has nothing to stop. */
|
|
23938
23996
|
readonly stopBackgroundWork?: () => void;
|
|
23939
23997
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-shim",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.63.0",
|
|
4
4
|
"description": "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Joseph Mearman <joseph@mearman.co.uk>",
|