@kitman-sh/cli 0.10.54 → 0.10.55
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/{chunk-19aqk325.js → chunk-99ver3za.js} +1 -1
- package/dist/{chunk-my987gxf.js → chunk-axdmbr61.js} +6 -2
- package/dist/{chunk-c0wrcam4.js → chunk-dgb6s5vq.js} +453 -69
- package/dist/{chunk-y48wz1sa.js → chunk-dj7pp4w4.js} +26 -6
- package/dist/egress-proxy.ts +138 -21
- package/dist/half-close-relay.ts +125 -0
- package/dist/index.js +2 -2
- package/dist/tcp-forwarder.ts +29 -0
- package/package.json +1 -1
- /package/dist/{chunk-3d6abgjh.js → chunk-g53wn1q8.js} +0 -0
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
decideSecretRequest,
|
|
7
7
|
readSecretRegistry,
|
|
8
8
|
appendSecretRefusals
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-g53wn1q8.js";
|
|
10
10
|
|
|
11
11
|
// packages/agent-runtime/src/srt-step.ts
|
|
12
12
|
import { SandboxRuntimeConfigSchema } from "@anthropic-ai/sandbox-runtime";
|
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
String2
|
|
45
45
|
} from "./chunk-xgwjcq4z.js";
|
|
46
46
|
import {
|
|
47
|
+
SecureRandom,
|
|
48
|
+
layerWebCrypto,
|
|
47
49
|
canonicalJson,
|
|
48
50
|
digestSha256,
|
|
49
51
|
schemaBodyJson,
|
|
@@ -408,7 +410,9 @@ var buildKit = (directory, options = {}) => gen(function* () {
|
|
|
408
410
|
if (!installed.ok)
|
|
409
411
|
return yield* new DependencyInstallFailed({ reason: failure(installed) });
|
|
410
412
|
const packages = yield* kitModules(root);
|
|
411
|
-
const
|
|
413
|
+
const id = yield* SecureRandom.pipe(flatMap3((random) => random.uuidV4()), provide3(layerWebCrypto));
|
|
414
|
+
const entryName = `.kitman-build-entry-${id}.ts`;
|
|
415
|
+
const entry = path.join(root, entryName);
|
|
412
416
|
yield* acquireRelease(fs.writeFileString(entry, buildEntry(identity, packages)), () => ignore(fs.remove(entry)));
|
|
413
417
|
const out = yield* fs.makeTempDirectoryScoped({ prefix: "kitman-kit-" });
|
|
414
418
|
const bundleFile = path.join(out, "bundle.js");
|
|
@@ -474,7 +478,7 @@ var buildKit = (directory, options = {}) => gen(function* () {
|
|
|
474
478
|
...own.schedules === undefined ? undefined : { schedules: own.schedules },
|
|
475
479
|
carried
|
|
476
480
|
}).pipe(mapError2((error) => new DeclarationsInvalid({ reason: error.message })));
|
|
477
|
-
const bundle = yield* fs.
|
|
481
|
+
const bundle = yield* fs.readFileString(bundleFile).pipe(map8((text) => new TextEncoder().encode(text.replaceAll(entryName, ".kitman-build-entry.ts"))), mapError2((error) => new BundleFailed({ reason: error.message })));
|
|
478
482
|
return {
|
|
479
483
|
...identity,
|
|
480
484
|
bundle,
|
|
@@ -287,7 +287,7 @@ import {
|
|
|
287
287
|
writeSecretRegistry,
|
|
288
288
|
secretRefusalLog,
|
|
289
289
|
parseSecretRefusals
|
|
290
|
-
} from "./chunk-
|
|
290
|
+
} from "./chunk-g53wn1q8.js";
|
|
291
291
|
import {
|
|
292
292
|
uploadRunFile,
|
|
293
293
|
programLease,
|
|
@@ -5310,8 +5310,71 @@ var harnessesInSrt = (input) => gen(function* () {
|
|
|
5310
5310
|
binaries: Object.fromEntries(probed.flatMap((entry) => entry.binary === undefined ? [] : [[entry.harness, entry.binary]]))
|
|
5311
5311
|
};
|
|
5312
5312
|
});
|
|
5313
|
-
// packages/agent-runtime/src/check-
|
|
5313
|
+
// packages/agent-runtime/src/check-docker.ts
|
|
5314
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5315
|
+
|
|
5316
|
+
// packages/agent-runtime/src/loopback-tunnel.ts
|
|
5314
5317
|
import * as net from "net";
|
|
5318
|
+
|
|
5319
|
+
class LoopbackTunnelUnavailable extends TaggedError("LoopbackTunnelUnavailable") {
|
|
5320
|
+
}
|
|
5321
|
+
var srtLoopbackTunnel = (tunnel) => [
|
|
5322
|
+
"proxy=${HTTP_PROXY:?srt set no HTTP_PROXY}",
|
|
5323
|
+
"proxy=${proxy#*://}",
|
|
5324
|
+
"proxy=${proxy%%/*}",
|
|
5325
|
+
"auth=",
|
|
5326
|
+
"case $proxy in *@*)",
|
|
5327
|
+
` auth=$(printf %s "\${proxy%@*}" | awk 'BEGIN { for (i = 1; i < 256; i++) hex[sprintf("%02X", i)] = sprintf("%c", i) }
|
|
5328
|
+
{ out = ""; while (match($0, /%[0-9A-Fa-f][0-9A-Fa-f]/)) { out = out substr($0, 1, RSTART - 1) hex[toupper(substr($0, RSTART + 1, 2))]; $0 = substr($0, RSTART + 3) } printf "%s", out $0 }')`,
|
|
5329
|
+
" proxy=${proxy##*@} ;;",
|
|
5330
|
+
"esac",
|
|
5331
|
+
"log=$(mktemp)",
|
|
5332
|
+
`${tunnel.listener ?? `socat -t 86400 TCP-LISTEN:${tunnel.listenPort},bind=127.0.0.1,fork,reuseaddr "PROXY:\${proxy%:*}:127.0.0.1:${tunnel.hostPort},proxyport=\${proxy##*:}\${auth:+,proxyauth=$auth}"`} 2>"$log" &`,
|
|
5333
|
+
"tunnel=$!",
|
|
5334
|
+
`fail() { echo "Kitman: the ${tunnel.service} tunnel did not open: $1" >&2; cat "$log" >&2; kill $tunnel 2>/dev/null; rm -f "$log"; exit 1; }`,
|
|
5335
|
+
"tries=0",
|
|
5336
|
+
"while :; do",
|
|
5337
|
+
' kill -0 $tunnel 2>/dev/null || fail "its listener stopped"',
|
|
5338
|
+
` answer=$( (${tunnel.probe}; sleep 1) | socat - TCP:127.0.0.1:${tunnel.listenPort} 2>/dev/null | ${tunnel.read})`,
|
|
5339
|
+
` case $answer in ${tunnel.answers}) break ;; esac`,
|
|
5340
|
+
" tries=$((tries + 1))",
|
|
5341
|
+
` [ $tries -lt 30 ] || fail "${tunnel.service} did not answer through it"`,
|
|
5342
|
+
" sleep 0.2",
|
|
5343
|
+
"done",
|
|
5344
|
+
'kill -0 $tunnel 2>/dev/null || fail "its listener stopped"',
|
|
5345
|
+
'sh -c "$1"',
|
|
5346
|
+
"status=$?",
|
|
5347
|
+
"kill $tunnel 2>/dev/null",
|
|
5348
|
+
'rm -f "$log"',
|
|
5349
|
+
"exit $status"
|
|
5350
|
+
].join(`
|
|
5351
|
+
`);
|
|
5352
|
+
var loopbackSocketForwarder = (hostPort, socket) => gen(function* () {
|
|
5353
|
+
const fs = yield* FileSystem;
|
|
5354
|
+
const dir = yield* flatMap3(fs.makeTempDirectoryScoped({ prefix: "kitman-lo-" }), fs.realPath).pipe(mapError2((error) => new LoopbackTunnelUnavailable({ reason: error.message })));
|
|
5355
|
+
yield* acquireRelease(callback3((resume) => {
|
|
5356
|
+
const open = new Set;
|
|
5357
|
+
const server = net.createServer({ allowHalfOpen: true }, (client) => {
|
|
5358
|
+
const upstream = net.connect({ port: hostPort, host: "127.0.0.1", allowHalfOpen: true });
|
|
5359
|
+
for (const connection of [client, upstream]) {
|
|
5360
|
+
open.add(connection);
|
|
5361
|
+
connection.on("close", () => open.delete(connection));
|
|
5362
|
+
}
|
|
5363
|
+
client.pipe(upstream);
|
|
5364
|
+
upstream.pipe(client);
|
|
5365
|
+
client.on("error", () => upstream.destroy());
|
|
5366
|
+
upstream.on("error", () => client.destroy());
|
|
5367
|
+
});
|
|
5368
|
+
server.once("error", (error) => resume(fail7(new LoopbackTunnelUnavailable({ reason: error.message }))));
|
|
5369
|
+
server.listen(`${dir}/${socket}`, () => resume(succeed6({ server, open })));
|
|
5370
|
+
}), ({ server, open }) => callback3((resume) => {
|
|
5371
|
+
server.close(() => resume(void_3));
|
|
5372
|
+
open.forEach((connection) => connection.destroy());
|
|
5373
|
+
}));
|
|
5374
|
+
return dir;
|
|
5375
|
+
});
|
|
5376
|
+
|
|
5377
|
+
// packages/agent-runtime/src/check-postgres.ts
|
|
5315
5378
|
var CheckPostgresImage = "pgvector/pgvector:0.8.7-pg16@sha256:7b822b0aac60967beb1ea5e576b8602c94c300a157d187f385ae3e0da199b90a";
|
|
5316
5379
|
var checkPostgresSandboxPort = 5432;
|
|
5317
5380
|
var checkPostgresAlias = "kitman-postgres";
|
|
@@ -5411,60 +5474,202 @@ var dockerCheckPostgres = (runnerId, replicaId) => (id) => gen(function* () {
|
|
|
5411
5474
|
return { container, hostPort, user, password };
|
|
5412
5475
|
});
|
|
5413
5476
|
var sslRequestOctal = String.raw`\000\000\000\010\004\322\026\057`;
|
|
5414
|
-
var srtPostgresTunnel = (hostPort, listenPort = checkPostgresSandboxPort) =>
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
"
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5477
|
+
var srtPostgresTunnel = (hostPort, listenPort = checkPostgresSandboxPort) => srtLoopbackTunnel({
|
|
5478
|
+
service: "Postgres",
|
|
5479
|
+
hostPort,
|
|
5480
|
+
listenPort,
|
|
5481
|
+
probe: `printf '${sslRequestOctal}'`,
|
|
5482
|
+
read: "head -c 1",
|
|
5483
|
+
answers: "N|S"
|
|
5484
|
+
});
|
|
5485
|
+
var postgresSocketForwarder = (hostPort) => loopbackSocketForwarder(hostPort, `.s.PGSQL.${checkPostgresSandboxPort}`).pipe(mapError2((error) => new CheckPostgresUnavailable({ reason: error.reason })));
|
|
5486
|
+
|
|
5487
|
+
// packages/agent-runtime/src/check-docker.ts
|
|
5488
|
+
var checkDockerPort = 2375;
|
|
5489
|
+
var checkDockerAlias = "kitman-docker";
|
|
5490
|
+
var DockerHubRegistry = "https://registry-1.docker.io";
|
|
5491
|
+
var CheckDockerLabel = "kitman.check-docker";
|
|
5492
|
+
var CheckDockerReplicaLabel = "kitman.check-docker-replica";
|
|
5493
|
+
var rootlessDataRoot = "/home/rootless/.local/share/docker";
|
|
5494
|
+
var registryDataRoot = "/var/lib/registry";
|
|
5495
|
+
var registryMirrorPort = 5000;
|
|
5496
|
+
var egressProxyPort = 3128;
|
|
5497
|
+
|
|
5498
|
+
class CheckDockerUnavailable extends TaggedError("CheckDockerUnavailable") {
|
|
5499
|
+
get message() {
|
|
5500
|
+
return `A pipeline check runs with a Docker daemon of its own, which this Runner could not start: ${this.reason}`;
|
|
5501
|
+
}
|
|
5502
|
+
}
|
|
5503
|
+
var dockerName2 = (id) => id.replaceAll(/[^a-zA-Z0-9_.-]/g, "-");
|
|
5504
|
+
var checkRegistryCacheVolume = (runnerId) => `kitman-check-registry-cache-${dockerName2(runnerId)}`;
|
|
5505
|
+
var checkDockerProxyEnvironment = (proxy, mirror) => {
|
|
5506
|
+
const direct = `localhost,127.0.0.1,${new URL(mirror).hostname}`;
|
|
5507
|
+
return {
|
|
5508
|
+
HTTP_PROXY: proxy,
|
|
5509
|
+
HTTPS_PROXY: proxy,
|
|
5510
|
+
http_proxy: proxy,
|
|
5511
|
+
https_proxy: proxy,
|
|
5512
|
+
NO_PROXY: direct,
|
|
5513
|
+
no_proxy: direct
|
|
5514
|
+
};
|
|
5515
|
+
};
|
|
5516
|
+
var checkDockerDaemonArgs = (input) => [
|
|
5517
|
+
"run",
|
|
5518
|
+
"--detach",
|
|
5519
|
+
"--name",
|
|
5520
|
+
input.name,
|
|
5521
|
+
...input.labels.flatMap(([key, value]) => ["--label", `${key}=${value}`]),
|
|
5522
|
+
"--network",
|
|
5523
|
+
input.network,
|
|
5524
|
+
"--network-alias",
|
|
5525
|
+
checkDockerAlias,
|
|
5526
|
+
"--security-opt",
|
|
5527
|
+
"seccomp=unconfined",
|
|
5528
|
+
"--security-opt",
|
|
5529
|
+
"apparmor=unconfined",
|
|
5530
|
+
"--security-opt",
|
|
5531
|
+
"systempaths=unconfined",
|
|
5532
|
+
"--device",
|
|
5533
|
+
"/dev/net/tun",
|
|
5534
|
+
"--volume",
|
|
5535
|
+
rootlessDataRoot,
|
|
5536
|
+
"--env",
|
|
5537
|
+
"DOCKER_TLS_CERTDIR=",
|
|
5538
|
+
...Object.entries(input.proxy === undefined ? {} : checkDockerProxyEnvironment(input.proxy, input.mirror)).flatMap(([key, value]) => ["--env", `${key}=${value}`]),
|
|
5539
|
+
input.image,
|
|
5540
|
+
"--registry-mirror",
|
|
5541
|
+
input.mirror,
|
|
5542
|
+
"--tls=false"
|
|
5543
|
+
];
|
|
5544
|
+
var checkRegistryMirrorArgs = (input) => [
|
|
5545
|
+
"run",
|
|
5546
|
+
"--detach",
|
|
5547
|
+
"--rm",
|
|
5548
|
+
"--name",
|
|
5549
|
+
input.name,
|
|
5550
|
+
...input.labels.flatMap(([key, value]) => ["--label", `${key}=${value}`]),
|
|
5551
|
+
"--network",
|
|
5552
|
+
input.network,
|
|
5553
|
+
"--volume",
|
|
5554
|
+
`${input.volume}:${registryDataRoot}`,
|
|
5555
|
+
"--env",
|
|
5556
|
+
`REGISTRY_PROXY_REMOTEURL=${DockerHubRegistry}`,
|
|
5557
|
+
"--env",
|
|
5558
|
+
"REGISTRY_PROXY_TTL=0",
|
|
5559
|
+
input.image
|
|
5560
|
+
];
|
|
5561
|
+
var checkDockerClientConfig = (proxy) => proxy === undefined ? {} : { proxies: { default: { httpProxy: proxy, httpsProxy: proxy, noProxy: "localhost,127.0.0.1" } } };
|
|
5562
|
+
var orphanedCheckDocker = (listing, live) => listing.split(`
|
|
5563
|
+
`).map((line) => line.trim().split(/\s+/)).flatMap(([id, replica]) => id === undefined || id === "" || live.has(replica ?? "") ? [] : [id]);
|
|
5564
|
+
var removeCheckDocker = (runnerId, liveReplicas) => gen(function* () {
|
|
5565
|
+
const filter = ["--filter", `label=${CheckDockerLabel}=${runnerId}`];
|
|
5566
|
+
const format = `{{.ID}} {{.Label "${CheckDockerReplicaLabel}"}}`;
|
|
5567
|
+
const containers = orphanedCheckDocker(yield* runCommand2("docker", ["ps", "--all", ...filter, "--format", format], { timeout: "10 seconds" }), liveReplicas);
|
|
5568
|
+
if (containers.length > 0)
|
|
5569
|
+
yield* runCommand2("docker", ["rm", "--force", "--volumes", ...containers], { timeout: "30 seconds" });
|
|
5570
|
+
const networks = orphanedCheckDocker(yield* runCommand2("docker", ["network", "ls", ...filter, "--format", format], { timeout: "10 seconds" }), liveReplicas);
|
|
5571
|
+
if (networks.length > 0)
|
|
5572
|
+
yield* runCommand2("docker", ["network", "rm", ...networks], { timeout: "30 seconds" });
|
|
5573
|
+
}).pipe(ignore);
|
|
5574
|
+
var daemonLogs = (container) => gen(function* () {
|
|
5575
|
+
const state = yield* runCommand2("docker", [
|
|
5576
|
+
"inspect",
|
|
5577
|
+
"--format",
|
|
5578
|
+
"{{.State.Status}} (exit code {{.State.ExitCode}})",
|
|
5579
|
+
container
|
|
5580
|
+
]);
|
|
5581
|
+
const logs = yield* runCommand2("sh", ["-c", 'docker logs --tail 40 "$0" 2>&1', container]);
|
|
5582
|
+
return `${state.trim()}
|
|
5583
|
+
${logs.trim().slice(-3000)}`;
|
|
5584
|
+
}).pipe(orElseSucceed(() => "nothing, since docker could not show its state or logs"));
|
|
5585
|
+
var randomHex2 = () => Array.from(crypto.getRandomValues(new Uint8Array(4)), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
5586
|
+
var dockerCheckDaemon = (input) => ({ id, network: allowed }) => gen(function* () {
|
|
5587
|
+
const run = `${dockerName2(id)}-${randomHex2()}`;
|
|
5588
|
+
const daemon = `kitman-check-docker-${run}`;
|
|
5589
|
+
const network = `kitman-check-docker-net-${run}`;
|
|
5590
|
+
const outside = `kitman-check-docker-out-${run}`;
|
|
5591
|
+
const mirror = `kitman-check-docker-mirror-${run}`;
|
|
5592
|
+
const proxyContainer = `kitman-check-docker-egress-${run}`;
|
|
5593
|
+
const forwarder = `kitman-check-docker-api-${run}`;
|
|
5594
|
+
const runLabel = "kitman.check-docker-run";
|
|
5595
|
+
const labels = [
|
|
5596
|
+
[CheckDockerLabel, input.runnerId],
|
|
5597
|
+
[CheckDockerReplicaLabel, input.replicaId],
|
|
5598
|
+
[runLabel, run]
|
|
5599
|
+
];
|
|
5600
|
+
const labelArgs = labels.flatMap(([key, value]) => ["--label", `${key}=${value}`]);
|
|
5601
|
+
const docker = (args, timeout = "30 seconds") => runCommand2("docker", args, { timeout }).pipe(mapError2((error) => new CheckDockerUnavailable({ reason: error.message })));
|
|
5602
|
+
yield* acquireRelease(void_3, () => gen(function* () {
|
|
5603
|
+
const ids = (yield* docker(["ps", "--all", "--quiet", "--filter", `label=${runLabel}=${run}`])).split(`
|
|
5604
|
+
`).filter((line) => line.trim() !== "");
|
|
5605
|
+
if (ids.length > 0)
|
|
5606
|
+
yield* docker(["rm", "--force", "--volumes", ...ids], "2 minutes");
|
|
5607
|
+
yield* ignore(docker(["network", "rm", network]));
|
|
5608
|
+
yield* ignore(docker(["network", "rm", outside]));
|
|
5609
|
+
}).pipe(ignore));
|
|
5610
|
+
yield* docker(["network", "create", "--opt", "com.docker.network.bridge.enable_icc=false", ...labelArgs, outside]);
|
|
5611
|
+
yield* docker(["network", "create", ...allowed === "open" ? [] : ["--internal"], ...labelArgs, network]);
|
|
5612
|
+
const addressOn = (container) => gen(function* () {
|
|
5613
|
+
yield* docker(["network", "connect", network, container]);
|
|
5614
|
+
const address = (yield* docker([
|
|
5615
|
+
"inspect",
|
|
5616
|
+
"--format",
|
|
5617
|
+
`{{(index .NetworkSettings.Networks "${network}").IPAddress}}`,
|
|
5618
|
+
container
|
|
5619
|
+
])).trim();
|
|
5620
|
+
if (address === "")
|
|
5621
|
+
return yield* new CheckDockerUnavailable({ reason: `${container} has no address` });
|
|
5622
|
+
return address;
|
|
5623
|
+
});
|
|
5624
|
+
const helper = (name, args) => docker(["run", "--detach", "--rm", "--name", name, ...labelArgs, "--network", outside, ...args], "30 minutes");
|
|
5625
|
+
yield* docker(checkRegistryMirrorArgs({
|
|
5626
|
+
name: mirror,
|
|
5627
|
+
image: input.registryImage,
|
|
5628
|
+
labels,
|
|
5629
|
+
network: outside,
|
|
5630
|
+
volume: checkRegistryCacheVolume(input.runnerId)
|
|
5631
|
+
}), "30 minutes");
|
|
5632
|
+
const mirrorUrl = `http://${yield* addressOn(mirror)}:${registryMirrorPort}`;
|
|
5633
|
+
const proxy = allowed === "open" ? undefined : yield* gen(function* () {
|
|
5634
|
+
yield* helper(proxyContainer, [
|
|
5635
|
+
"--volume",
|
|
5636
|
+
`${fileURLToPath2(import.meta.resolve("./egress-proxy.ts"))}:/kitman/egress-proxy.ts:ro`,
|
|
5637
|
+
input.proxyImage ?? EgressProxyImage,
|
|
5638
|
+
"bun",
|
|
5639
|
+
"/kitman/egress-proxy.ts",
|
|
5640
|
+
allowed.hosts.join(",")
|
|
5641
|
+
]);
|
|
5642
|
+
return `http://${yield* addressOn(proxyContainer)}:${egressProxyPort}`;
|
|
5643
|
+
});
|
|
5644
|
+
yield* docker(checkDockerDaemonArgs({
|
|
5645
|
+
name: daemon,
|
|
5646
|
+
image: input.image,
|
|
5647
|
+
labels,
|
|
5648
|
+
network,
|
|
5649
|
+
mirror: mirrorUrl,
|
|
5650
|
+
...proxy === undefined ? undefined : { proxy }
|
|
5651
|
+
}), "30 minutes");
|
|
5652
|
+
yield* helper(forwarder, [
|
|
5653
|
+
"--publish",
|
|
5654
|
+
`127.0.0.1::${checkDockerPort}`,
|
|
5655
|
+
"--volume",
|
|
5656
|
+
`${fileURLToPath2(import.meta.resolve("./tcp-forwarder.ts"))}:/kitman/tcp-forwarder.ts:ro`,
|
|
5657
|
+
input.proxyImage ?? EgressProxyImage,
|
|
5658
|
+
"bun",
|
|
5659
|
+
"/kitman/tcp-forwarder.ts",
|
|
5660
|
+
String(checkDockerPort),
|
|
5661
|
+
checkDockerAlias,
|
|
5662
|
+
String(checkDockerPort)
|
|
5663
|
+
]);
|
|
5664
|
+
yield* docker(["network", "connect", network, forwarder]);
|
|
5665
|
+
const hostPort = publishedPort(yield* docker(["port", forwarder, `${checkDockerPort}/tcp`]));
|
|
5666
|
+
if (hostPort === undefined)
|
|
5667
|
+
return yield* new CheckDockerUnavailable({ reason: "docker published no port" });
|
|
5668
|
+
yield* docker(["--host", `tcp://127.0.0.1:${hostPort}`, "version", "--format", "{{.Server.Version}}"], "10 seconds").pipe(retry({ schedule: spaced("500 millis"), times: 240 }), catch_((error) => gen(function* () {
|
|
5669
|
+
const logs = yield* daemonLogs(daemon);
|
|
5670
|
+
return yield* new CheckDockerUnavailable({ reason: `${error.reason}; the daemon logged: ${logs}` });
|
|
5671
|
+
})));
|
|
5672
|
+
return { container: daemon, hostPort, ...proxy === undefined ? undefined : { proxy } };
|
|
5468
5673
|
});
|
|
5469
5674
|
// packages/agent-runtime/src/codex-hook-trust.ts
|
|
5470
5675
|
class CodexAppServerFailed extends TaggedError("CodexAppServerFailed") {
|
|
@@ -5794,9 +5999,110 @@ var programProcessExecutor = (input) => (work) => gen(function* () {
|
|
|
5794
5999
|
status: "deferred",
|
|
5795
6000
|
crashed: `it did not start: ${error.message}`.slice(0, 500)
|
|
5796
6001
|
})));
|
|
6002
|
+
// packages/agent-runtime/src/half-close-relay.ts
|
|
6003
|
+
import { once } from "events";
|
|
6004
|
+
import { connect as connect2, createServer as createServer2 } from "net";
|
|
6005
|
+
var frameHeader = 4;
|
|
6006
|
+
var endOfInput = Buffer.alloc(frameHeader);
|
|
6007
|
+
var maxFrame = 1024 * 1024;
|
|
6008
|
+
var keepUp = (from, to, accepted) => {
|
|
6009
|
+
if (accepted)
|
|
6010
|
+
return;
|
|
6011
|
+
from.pause();
|
|
6012
|
+
to.once("drain", () => from.resume());
|
|
6013
|
+
};
|
|
6014
|
+
var closeTogether = (...sockets) => sockets.forEach((socket) => {
|
|
6015
|
+
socket.on("error", () => sockets.forEach((other) => other.destroy()));
|
|
6016
|
+
socket.on("close", () => sockets.forEach((other) => other.end()));
|
|
6017
|
+
});
|
|
6018
|
+
var listening = (server, port) => once(server.listen(port, "127.0.0.1"), "listening").then(() => server);
|
|
6019
|
+
var startHalfCloseDecoder = (input) => listening(createServer2({ allowHalfOpen: true }, (client) => {
|
|
6020
|
+
const upstream = connect2({ port: input.targetPort, host: "127.0.0.1", allowHalfOpen: true });
|
|
6021
|
+
let pending = Buffer.alloc(0);
|
|
6022
|
+
client.on("data", (chunk) => {
|
|
6023
|
+
pending = Buffer.concat([pending, chunk]);
|
|
6024
|
+
if (pending.length >= frameHeader && pending.readUInt32BE(0) > maxFrame)
|
|
6025
|
+
return void client.destroy();
|
|
6026
|
+
while (pending.length >= frameHeader && pending.length >= frameHeader + pending.readUInt32BE(0)) {
|
|
6027
|
+
const length = pending.readUInt32BE(0);
|
|
6028
|
+
if (length === 0)
|
|
6029
|
+
upstream.end();
|
|
6030
|
+
else
|
|
6031
|
+
keepUp(client, upstream, upstream.write(pending.subarray(frameHeader, frameHeader + length)));
|
|
6032
|
+
pending = pending.subarray(frameHeader + length);
|
|
6033
|
+
}
|
|
6034
|
+
});
|
|
6035
|
+
client.on("end", () => upstream.end());
|
|
6036
|
+
upstream.pipe(client);
|
|
6037
|
+
closeTogether(client, upstream);
|
|
6038
|
+
}), input.port);
|
|
6039
|
+
if (false)
|
|
6040
|
+
;
|
|
6041
|
+
|
|
5797
6042
|
// packages/agent-runtime/src/verification-executor.ts
|
|
6043
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
5798
6044
|
var checkTimeout = "30 minutes";
|
|
5799
6045
|
var checkFilesPerVerification = 50;
|
|
6046
|
+
var halfCloseEncoder = [process.execPath, fileURLToPath3(import.meta.resolve("./half-close-relay.ts"))];
|
|
6047
|
+
var srtDockerTunnel = (relayPort) => srtLoopbackTunnel({
|
|
6048
|
+
service: "Docker daemon",
|
|
6049
|
+
hostPort: relayPort,
|
|
6050
|
+
listenPort: checkDockerPort,
|
|
6051
|
+
listener: `${halfCloseEncoder.map(quoted).join(" ")} ${checkDockerPort} "$HTTP_PROXY" ${relayPort}`,
|
|
6052
|
+
probe: String.raw`printf 'GET /_ping HTTP/1.0\r\n\r\n'`,
|
|
6053
|
+
read: "tail -c 2",
|
|
6054
|
+
answers: "OK"
|
|
6055
|
+
});
|
|
6056
|
+
var pipelineEnvironment = (docker, dockerHost) => ({
|
|
6057
|
+
DOCKER_HOST: dockerHost,
|
|
6058
|
+
DOCKER_CONFIG: `${docker.clientDir}/docker`,
|
|
6059
|
+
GCL_HOME: `${docker.clientDir}/gitlab-ci-local-home`
|
|
6060
|
+
});
|
|
6061
|
+
var dockerRoute = (backend, docker) => {
|
|
6062
|
+
if (docker === undefined)
|
|
6063
|
+
return { env: {}, hosts: [], unixSockets: [], services: [], tunnel: undefined };
|
|
6064
|
+
if (backend === "docker")
|
|
6065
|
+
return {
|
|
6066
|
+
env: {
|
|
6067
|
+
...pipelineEnvironment(docker, `tcp://${checkDockerAlias}:${checkDockerPort}`),
|
|
6068
|
+
NO_PROXY: `localhost,127.0.0.1,${checkDockerAlias}`,
|
|
6069
|
+
no_proxy: `localhost,127.0.0.1,${checkDockerAlias}`
|
|
6070
|
+
},
|
|
6071
|
+
hosts: [],
|
|
6072
|
+
unixSockets: [],
|
|
6073
|
+
services: [{ container: docker.container, alias: checkDockerAlias }],
|
|
6074
|
+
tunnel: undefined
|
|
6075
|
+
};
|
|
6076
|
+
switch (docker.route.kind) {
|
|
6077
|
+
case "socket":
|
|
6078
|
+
return {
|
|
6079
|
+
env: pipelineEnvironment(docker, `unix://${docker.route.dir}/docker.sock`),
|
|
6080
|
+
hosts: [],
|
|
6081
|
+
unixSockets: [docker.route.dir],
|
|
6082
|
+
services: [],
|
|
6083
|
+
tunnel: undefined
|
|
6084
|
+
};
|
|
6085
|
+
case "relay":
|
|
6086
|
+
return {
|
|
6087
|
+
env: pipelineEnvironment(docker, `tcp://127.0.0.1:${checkDockerPort}`),
|
|
6088
|
+
hosts: [`127.0.0.1:${docker.route.port}`],
|
|
6089
|
+
unixSockets: [],
|
|
6090
|
+
services: [],
|
|
6091
|
+
tunnel: srtDockerTunnel(docker.route.port)
|
|
6092
|
+
};
|
|
6093
|
+
case "direct":
|
|
6094
|
+
throw new Error("A pipeline check in srt reaches its daemon through a socket or the relay, never directly.");
|
|
6095
|
+
}
|
|
6096
|
+
};
|
|
6097
|
+
var hostCheckEnvironment = (docker) => {
|
|
6098
|
+
const { POSTGRES_URL: _runnerDatabase, ...runner } = withoutPreloads2();
|
|
6099
|
+
if (docker === undefined)
|
|
6100
|
+
return runner;
|
|
6101
|
+
return {
|
|
6102
|
+
...Object.fromEntries(Object.entries(runner).filter(([name]) => !name.startsWith("DOCKER_") && !name.startsWith("GCL_") && name !== "SSH_AUTH_SOCK")),
|
|
6103
|
+
...pipelineEnvironment(docker, `tcp://127.0.0.1:${docker.hostPort}`)
|
|
6104
|
+
};
|
|
6105
|
+
};
|
|
5800
6106
|
var postgresRoute = (backend, postgres) => {
|
|
5801
6107
|
if (postgres === undefined)
|
|
5802
6108
|
return { url: undefined, hosts: [], unixSockets: [], services: [], tunnel: undefined };
|
|
@@ -5824,12 +6130,13 @@ var postgresRoute = (backend, postgres) => {
|
|
|
5824
6130
|
tunnel: srtPostgresTunnel(postgres.hostPort)
|
|
5825
6131
|
};
|
|
5826
6132
|
};
|
|
5827
|
-
var checkSandboxPolicy = (work, sandbox, checkout, cache, filesDir, postgresHosts) => ({
|
|
6133
|
+
var checkSandboxPolicy = (work, sandbox, checkout, cache, filesDir, postgresHosts, clientDir) => ({
|
|
5828
6134
|
...runnerStateSandboxPolicy(sandbox.runner, [
|
|
5829
6135
|
repositoryClone(sandbox.runner.workRoot, work.repoBinding.id),
|
|
5830
|
-
checkout
|
|
6136
|
+
checkout,
|
|
6137
|
+
...clientDir === undefined ? [] : [clientDir, ...halfCloseEncoder]
|
|
5831
6138
|
]),
|
|
5832
|
-
writablePaths: [checkout, cache, filesDir],
|
|
6139
|
+
writablePaths: [checkout, cache, filesDir, ...clientDir === undefined ? [] : [clientDir]],
|
|
5833
6140
|
network: verificationNetworkAllowlist({
|
|
5834
6141
|
apiUrl: sandbox.apiUrl,
|
|
5835
6142
|
storageOrigin: work.storageOrigin,
|
|
@@ -5839,16 +6146,15 @@ var checkSandboxPolicy = (work, sandbox, checkout, cache, filesDir, postgresHost
|
|
|
5839
6146
|
environment: srtCacheEnvironment2(cache)
|
|
5840
6147
|
});
|
|
5841
6148
|
var checkGitEnvironment = { GIT_CONFIG_GLOBAL: "/dev/null", GIT_TERMINAL_PROMPT: "0" };
|
|
5842
|
-
var checkRunner = (work, sandbox, cache, inherited) => (checkout) => ({ command, cwd, filesDir, onOutput, postgres }) => {
|
|
6149
|
+
var checkRunner = (work, sandbox, cache, inherited) => (checkout) => ({ command, cwd, filesDir, onOutput, postgres, docker }) => {
|
|
5843
6150
|
const env = { ...checkGitEnvironment, KITMAN_RUN_FILES_DIR: filesDir };
|
|
5844
6151
|
const output = onOutput === undefined ? undefined : { onOutput };
|
|
5845
6152
|
if (sandbox === undefined || sandbox.backend === undefined) {
|
|
5846
|
-
const { POSTGRES_URL: _runnerDatabase, ...runner } = withoutPreloads2();
|
|
5847
6153
|
return runShell2(command, {
|
|
5848
6154
|
cwd,
|
|
5849
6155
|
extendEnv: false,
|
|
5850
6156
|
env: {
|
|
5851
|
-
...
|
|
6157
|
+
...hostCheckEnvironment(docker),
|
|
5852
6158
|
...env,
|
|
5853
6159
|
...postgres === undefined ? undefined : { POSTGRES_URL: checkPostgresUrl(postgres, "127.0.0.1", postgres.hostPort) }
|
|
5854
6160
|
},
|
|
@@ -5857,14 +6163,18 @@ var checkRunner = (work, sandbox, cache, inherited) => (checkout) => ({ command,
|
|
|
5857
6163
|
});
|
|
5858
6164
|
}
|
|
5859
6165
|
const route = postgresRoute(sandbox.backend, postgres);
|
|
5860
|
-
const
|
|
6166
|
+
const daemon = dockerRoute(sandbox.backend, docker);
|
|
6167
|
+
const policy = checkSandboxPolicy(work, sandbox, checkout, cache, filesDir, [...route.hosts, ...daemon.hosts], docker?.clientDir);
|
|
6168
|
+
const [tunnel, ...innerTunnels] = [route.tunnel, daemon.tunnel].filter((script) => script !== undefined);
|
|
6169
|
+
const tunnelled = innerTunnels.reduceRight((inner, script) => `sh -c ${quoted(script)} sh ${quoted(inner)}`, command);
|
|
5861
6170
|
const sandboxCommand = {
|
|
5862
|
-
argv:
|
|
6171
|
+
argv: tunnel === undefined ? ["sh", "-c", command] : ["sh", "-c", tunnel, "sh", tunnelled],
|
|
5863
6172
|
cwd,
|
|
5864
6173
|
env: {
|
|
5865
6174
|
...env,
|
|
5866
6175
|
KITMAN_CHECK_SANDBOX: sandbox.backend,
|
|
5867
|
-
...route.url === undefined ? undefined : { POSTGRES_URL: route.url }
|
|
6176
|
+
...route.url === undefined ? undefined : { POSTGRES_URL: route.url },
|
|
6177
|
+
...daemon.env
|
|
5868
6178
|
},
|
|
5869
6179
|
timeout: checkTimeout,
|
|
5870
6180
|
...output
|
|
@@ -5873,7 +6183,7 @@ var checkRunner = (work, sandbox, cache, inherited) => (checkout) => ({ command,
|
|
|
5873
6183
|
backend: "srt",
|
|
5874
6184
|
policy,
|
|
5875
6185
|
command: { ...sandboxCommand, env: { ...inherited, ...sandboxCommand.env }, extendEnv: false },
|
|
5876
|
-
unixSockets: route.unixSockets,
|
|
6186
|
+
unixSockets: [...route.unixSockets, ...daemon.unixSockets],
|
|
5877
6187
|
...sandbox.srtCommand === undefined ? undefined : { srtCommand: sandbox.srtCommand }
|
|
5878
6188
|
}) : runSandboxCommand({
|
|
5879
6189
|
backend: "docker",
|
|
@@ -5883,7 +6193,7 @@ var checkRunner = (work, sandbox, cache, inherited) => (checkout) => ({ command,
|
|
|
5883
6193
|
readOnlyPaths: [repositoryClone(sandbox.runner.workRoot, work.repoBinding.id)],
|
|
5884
6194
|
policy,
|
|
5885
6195
|
command: sandboxCommand,
|
|
5886
|
-
services: route.services
|
|
6196
|
+
services: [...route.services, ...daemon.services]
|
|
5887
6197
|
});
|
|
5888
6198
|
};
|
|
5889
6199
|
var regularFiles = (dir) => gen(function* () {
|
|
@@ -5937,6 +6247,38 @@ var checkoutPostgres = (worktree, services) => gen(function* () {
|
|
|
5937
6247
|
});
|
|
5938
6248
|
return yield* services.postgres(services.id).pipe(mapError2((error) => new ServicesUnavailable({ reason: error.message })));
|
|
5939
6249
|
});
|
|
6250
|
+
var decodeListeningPort = decodeUnknownEffect2(Struct({ port: Finite }));
|
|
6251
|
+
var pipelineDaemon = (services, check) => gen(function* () {
|
|
6252
|
+
if (services.docker === undefined)
|
|
6253
|
+
return yield* new ServicesUnavailable({
|
|
6254
|
+
reason: `The pipeline check ${check} runs with a Docker daemon of its own, and this Runner cannot start one: set KITMAN_RUNNER_CHECK_DOCKER_IMAGE to a docker:*-dind-rootless image and KITMAN_RUNNER_CHECK_REGISTRY_IMAGE to a registry:3 image, each pinned by digest.`
|
|
6255
|
+
});
|
|
6256
|
+
const fs = yield* FileSystem;
|
|
6257
|
+
const started = yield* services.docker({ id: services.id, network: services.network }).pipe(mapError2((error) => new ServicesUnavailable({ reason: error.message })));
|
|
6258
|
+
const clientDir = yield* fs.makeTempDirectoryScoped({ prefix: "kitman-check-docker-" });
|
|
6259
|
+
yield* fs.makeDirectory(`${clientDir}/docker`);
|
|
6260
|
+
yield* fs.makeDirectory(`${clientDir}/gitlab-ci-local-home`);
|
|
6261
|
+
yield* fs.writeFileString(`${clientDir}/docker/config.json`, encodeUnknownJsonStringSync(checkDockerClientConfig(started.proxy)));
|
|
6262
|
+
if (services.backend !== "srt")
|
|
6263
|
+
return { ...started, clientDir, route: { kind: "direct" } };
|
|
6264
|
+
if (services.platform !== "darwin") {
|
|
6265
|
+
const relay = yield* acquireRelease(promise(() => startHalfCloseDecoder({ port: 0, targetPort: started.hostPort })), (server) => sync2(() => server.close()));
|
|
6266
|
+
const { port } = yield* decodeListeningPort(relay.address()).pipe(mapError2(() => new ServicesUnavailable({
|
|
6267
|
+
reason: `The pipeline check ${check} cannot reach its Docker daemon: its relay has no port`
|
|
6268
|
+
})));
|
|
6269
|
+
return { ...started, clientDir, route: { kind: "relay", port } };
|
|
6270
|
+
}
|
|
6271
|
+
return {
|
|
6272
|
+
...started,
|
|
6273
|
+
clientDir,
|
|
6274
|
+
route: {
|
|
6275
|
+
kind: "socket",
|
|
6276
|
+
dir: yield* loopbackSocketForwarder(started.hostPort, "docker.sock").pipe(mapError2((error) => new ServicesUnavailable({
|
|
6277
|
+
reason: `The pipeline check ${check} cannot reach its Docker daemon: ${error.reason}`
|
|
6278
|
+
})))
|
|
6279
|
+
}
|
|
6280
|
+
};
|
|
6281
|
+
});
|
|
5940
6282
|
var reachCheckPostgres = (postgres, services, worktree, run) => gen(function* () {
|
|
5941
6283
|
if (services.backend !== "srt")
|
|
5942
6284
|
return postgres;
|
|
@@ -5985,11 +6327,13 @@ var runChecks = (worktree, checks, tree, known, run, keeper, services) => gen(fu
|
|
|
5985
6327
|
const filesDir = yield* fs.makeTempDirectoryScoped({ prefix: "kitman-check-files-" });
|
|
5986
6328
|
const output = path.join(yield* fs.makeTempDirectoryScoped({ prefix: "kitman-check-output-" }), "output");
|
|
5987
6329
|
const sink = keeper === undefined ? undefined : yield* fs.open(output, { flag: "w" });
|
|
6330
|
+
const docker = check.pipeline === true ? yield* pipelineDaemon(services, check.name) : undefined;
|
|
5988
6331
|
const ran = yield* run({
|
|
5989
6332
|
command: check.command,
|
|
5990
6333
|
cwd: path.join(worktree, check.workingDirectory ?? "."),
|
|
5991
6334
|
filesDir,
|
|
5992
6335
|
...postgres === undefined ? undefined : { postgres },
|
|
6336
|
+
...docker === undefined ? undefined : { docker },
|
|
5993
6337
|
...sink === undefined ? undefined : { onOutput: (chunk) => sink.writeAll(chunk) }
|
|
5994
6338
|
});
|
|
5995
6339
|
const durationMs = (yield* currentTimeMillis) - startedAt;
|
|
@@ -6088,6 +6432,27 @@ var measureRevisions = (work, measure, checkouts, runIn) => gen(function* () {
|
|
|
6088
6432
|
}
|
|
6089
6433
|
};
|
|
6090
6434
|
});
|
|
6435
|
+
var ownRepository = (worktree, clone, targetBranch) => gen(function* () {
|
|
6436
|
+
const fs = yield* FileSystem;
|
|
6437
|
+
const path = yield* Path;
|
|
6438
|
+
const scratch = yield* fs.makeTempDirectoryScoped({
|
|
6439
|
+
directory: path.dirname(worktree),
|
|
6440
|
+
prefix: "kitman-check-git-"
|
|
6441
|
+
});
|
|
6442
|
+
const gitDir = path.join(scratch, "git");
|
|
6443
|
+
const git = (...args) => runCommand2("git", ["--git-dir", gitDir, ...args]);
|
|
6444
|
+
const revision = (yield* runCommand2("git", ["-C", worktree, "rev-parse", "HEAD"])).trim();
|
|
6445
|
+
const originUrl = (yield* runCommand2("git", ["-C", clone, "config", "--get", "remote.origin.url"])).trim();
|
|
6446
|
+
yield* runCommand2("git", ["clone", "--quiet", "--bare", "--no-hardlinks", clone, gitDir]);
|
|
6447
|
+
yield* git("fetch", "--quiet", clone, "+refs/remotes/origin/*:refs/remotes/origin/*");
|
|
6448
|
+
yield* git("config", "core.bare", "false");
|
|
6449
|
+
yield* git("remote", "set-url", "origin", originUrl);
|
|
6450
|
+
yield* git("symbolic-ref", "refs/remotes/origin/HEAD", `refs/remotes/origin/${targetBranch}`);
|
|
6451
|
+
yield* git("update-ref", "--no-deref", "HEAD", revision);
|
|
6452
|
+
yield* fs.remove(path.join(worktree, ".git"));
|
|
6453
|
+
yield* fs.rename(gitDir, path.join(worktree, ".git"));
|
|
6454
|
+
yield* runCommand2("git", ["-C", worktree, "reset", "--quiet"]);
|
|
6455
|
+
}).pipe(scoped);
|
|
6091
6456
|
var treeOf = (worktree) => map8(runCommand2("git", ["-C", worktree, "rev-parse", "HEAD^{tree}"]), (tree) => tree.trim());
|
|
6092
6457
|
var verificationExecutor = (input) => (work) => gen(function* () {
|
|
6093
6458
|
if (input.sandbox !== undefined && input.sandbox.backend === undefined)
|
|
@@ -6113,15 +6478,22 @@ var verificationExecutor = (input) => (work) => gen(function* () {
|
|
|
6113
6478
|
}, work.repoBinding);
|
|
6114
6479
|
const pipelineOnly = work.checks.every((check) => check.pipeline === true);
|
|
6115
6480
|
const baseline = pipelineOnly ? head : yield* baselineAt(work.baseline).pipe(catch_(() => baselineAt()));
|
|
6481
|
+
if (work.checks.some((check) => check.pipeline === true))
|
|
6482
|
+
yield* ownRepository(head, repositoryClone(input.rootDir, work.repoBinding.id), work.repoBinding.defaultTargetBranch);
|
|
6116
6483
|
const headTree = yield* treeOf(head);
|
|
6117
6484
|
const baselineTree = yield* treeOf(baseline);
|
|
6118
6485
|
const trees = headTree === baselineTree ? [headTree] : [headTree, baselineTree];
|
|
6119
6486
|
const known = yield* input.known(work, trees);
|
|
6487
|
+
const sandbox = input.sandbox;
|
|
6488
|
+
const checkNetwork = sandbox === undefined || sandbox.backend === undefined ? "open" : checkSandboxPolicy(work, sandbox, head, cache, cache, []).network;
|
|
6489
|
+
const daemonNetwork = checkNetwork === "open" ? "open" : { hosts: checkNetwork.hosts };
|
|
6120
6490
|
const services = (id) => ({
|
|
6121
6491
|
id,
|
|
6122
6492
|
backend: input.sandbox?.backend,
|
|
6123
6493
|
platform: (input.sandbox?.backend === "srt" ? input.sandbox.platform : undefined) ?? process.platform,
|
|
6124
|
-
postgres: input.postgres
|
|
6494
|
+
postgres: input.postgres,
|
|
6495
|
+
docker: input.docker,
|
|
6496
|
+
network: daemonNetwork
|
|
6125
6497
|
});
|
|
6126
6498
|
const headChecks = yield* runChecks(head, work.checks, headTree, known, runIn(head), keeper(""), services(work.id)).pipe(scoped);
|
|
6127
6499
|
return {
|
|
@@ -6136,7 +6508,7 @@ var verificationExecutor = (input) => (work) => gen(function* () {
|
|
|
6136
6508
|
baseline: {
|
|
6137
6509
|
revision: (yield* runCommand2("git", ["-C", baseline, "rev-parse", "HEAD"])).trim(),
|
|
6138
6510
|
tree: baselineTree,
|
|
6139
|
-
checks: yield* runChecks(baseline, work.checks, baselineTree, known, runIn(baseline), keeper("baseline-"), services(`${work.id}-baseline`)).pipe(scoped)
|
|
6511
|
+
checks: yield* runChecks(baseline, work.checks.filter((check) => check.pipeline !== true), baselineTree, known, runIn(baseline), keeper("baseline-"), services(`${work.id}-baseline`)).pipe(scoped)
|
|
6140
6512
|
}
|
|
6141
6513
|
}
|
|
6142
6514
|
}
|
|
@@ -6160,11 +6532,13 @@ export {
|
|
|
6160
6532
|
AgentRuntimePackageName,
|
|
6161
6533
|
AgentStepFailure,
|
|
6162
6534
|
BundleRefused,
|
|
6535
|
+
CheckDockerUnavailable,
|
|
6163
6536
|
CheckPostgresImage,
|
|
6164
6537
|
CheckPostgresUnavailable,
|
|
6165
6538
|
CommandFailed2 as CommandFailed,
|
|
6166
6539
|
CommandRunner2 as CommandRunner,
|
|
6167
6540
|
ContainerError,
|
|
6541
|
+
DockerHubRegistry,
|
|
6168
6542
|
EgressProxyImage,
|
|
6169
6543
|
HERDR_CORE_GENERATION2 as HERDR_CORE_GENERATION,
|
|
6170
6544
|
HERDR_CORE_INSTALL_COMMAND2 as HERDR_CORE_INSTALL_COMMAND,
|
|
@@ -6188,11 +6562,18 @@ export {
|
|
|
6188
6562
|
applyAgentEnvironmentMaterialization2 as applyAgentEnvironmentMaterialization,
|
|
6189
6563
|
applyAgentEnvironmentPlans,
|
|
6190
6564
|
assertHerdrRunnerCapability2 as assertHerdrRunnerCapability,
|
|
6565
|
+
checkDockerAlias,
|
|
6566
|
+
checkDockerClientConfig,
|
|
6567
|
+
checkDockerDaemonArgs,
|
|
6568
|
+
checkDockerPort,
|
|
6569
|
+
checkDockerProxyEnvironment,
|
|
6191
6570
|
checkPostgresAlias,
|
|
6192
6571
|
checkPostgresSandboxPort,
|
|
6193
6572
|
checkPostgresSocketUrl,
|
|
6194
6573
|
checkPostgresSweepInterval,
|
|
6195
6574
|
checkPostgresUrl,
|
|
6575
|
+
checkRegistryCacheVolume,
|
|
6576
|
+
checkRegistryMirrorArgs,
|
|
6196
6577
|
clearSettledCheckpoints,
|
|
6197
6578
|
commandHostProbe,
|
|
6198
6579
|
compiled2 as compiled,
|
|
@@ -6200,6 +6581,7 @@ export {
|
|
|
6200
6581
|
controlFailureKind,
|
|
6201
6582
|
defaultAgentRuntimeUserConfigPath,
|
|
6202
6583
|
detectHarnesses2 as detectHarnesses,
|
|
6584
|
+
dockerCheckDaemon,
|
|
6203
6585
|
dockerCheckPostgres,
|
|
6204
6586
|
dockerContainerRunner,
|
|
6205
6587
|
dockerExecArgv,
|
|
@@ -6237,6 +6619,7 @@ export {
|
|
|
6237
6619
|
liveReplicaIds,
|
|
6238
6620
|
loadAgentRuntimeConfig,
|
|
6239
6621
|
makeReplicaSignals,
|
|
6622
|
+
orphanedCheckDocker,
|
|
6240
6623
|
orphanedCheckPostgres,
|
|
6241
6624
|
pinnedSrtCommand2 as pinnedSrtCommand,
|
|
6242
6625
|
pinnedSrtStepCommand2 as pinnedSrtStepCommand,
|
|
@@ -6251,6 +6634,7 @@ export {
|
|
|
6251
6634
|
publishedPort,
|
|
6252
6635
|
readManifestSkillPaths2 as readManifestSkillPaths,
|
|
6253
6636
|
refuseAgentRequest,
|
|
6637
|
+
removeCheckDocker,
|
|
6254
6638
|
removeCheckPostgres,
|
|
6255
6639
|
removeRunnerContainers,
|
|
6256
6640
|
removeRunnerTerminals,
|
|
@@ -496,6 +496,12 @@ var sandboxMissing = (sandbox, missing) => ({
|
|
|
496
496
|
});
|
|
497
497
|
var personalCheckBackend = (agentSandbox, dockerAvailable) => agentSandbox === "srt" || agentSandbox === "docker" && dockerAvailable ? agentSandbox : undefined;
|
|
498
498
|
var checkHostsSetting = (value) => (value ?? "").split(",").map((host) => host.trim()).filter((host) => host !== "");
|
|
499
|
+
var pinnedImage = (value) => {
|
|
500
|
+
const image = value?.trim();
|
|
501
|
+
return image !== undefined && /@sha256:[a-f0-9]{64}$/.test(image) ? image : undefined;
|
|
502
|
+
};
|
|
503
|
+
var checkDockerImageSetting = pinnedImage;
|
|
504
|
+
var checkRegistryImageSetting = pinnedImage;
|
|
499
505
|
var herdrCoreSession = (runnerId, personal) => personal ? undefined : `kitman-${sha2562(runnerId).slice(0, 16)}`;
|
|
500
506
|
var OpenHerdrCore = Reference("kitman/cli/OpenHerdrCore", {
|
|
501
507
|
defaultValue: () => (options) => flatMap3(promise(() => import("./chunk-mtgkvbk4.js")), ({ HerdrCore, herdrCoreLayer }) => provide3(service(HerdrCore), herdrCoreLayer(options)))
|
|
@@ -1645,7 +1651,7 @@ var package_default = {
|
|
|
1645
1651
|
};
|
|
1646
1652
|
|
|
1647
1653
|
// apps/cli/src/scaffold.ts
|
|
1648
|
-
var cliVersion = "0.10.
|
|
1654
|
+
var cliVersion = "0.10.55";
|
|
1649
1655
|
var packageJson = (name, scaffold) => `${JSON.stringify({
|
|
1650
1656
|
name,
|
|
1651
1657
|
private: true,
|
|
@@ -1894,7 +1900,7 @@ var Usage = [
|
|
|
1894
1900
|
"Every run, loop, agent, verify, constraint, secret, claim, deploy, dev and kit command takes --json."
|
|
1895
1901
|
].join(`
|
|
1896
1902
|
`);
|
|
1897
|
-
var agentRuntime = promise(() => import("./chunk-
|
|
1903
|
+
var agentRuntime = promise(() => import("./chunk-dgb6s5vq.js"));
|
|
1898
1904
|
var TuiLauncher = Reference("kitman/cli/TuiLauncher", {
|
|
1899
1905
|
defaultValue: () => gen(function* () {
|
|
1900
1906
|
const tui = yield* tryPromise({
|
|
@@ -2762,7 +2768,9 @@ var runnerSettings = gen(function* () {
|
|
|
2762
2768
|
sshAgentAvailable: (yield* optional("SSH_AUTH_SOCK")) !== undefined,
|
|
2763
2769
|
sandbox,
|
|
2764
2770
|
dockerImage: yield* optional("KITMAN_RUNNER_DOCKER_IMAGE"),
|
|
2765
|
-
checkHosts: checkHostsSetting(yield* optional("KITMAN_RUNNER_CHECK_HOSTS"))
|
|
2771
|
+
checkHosts: checkHostsSetting(yield* optional("KITMAN_RUNNER_CHECK_HOSTS")),
|
|
2772
|
+
checkDockerImage: checkDockerImageSetting(yield* optional("KITMAN_RUNNER_CHECK_DOCKER_IMAGE")),
|
|
2773
|
+
checkRegistryImage: checkRegistryImageSetting(yield* optional("KITMAN_RUNNER_CHECK_REGISTRY_IMAGE"))
|
|
2766
2774
|
};
|
|
2767
2775
|
});
|
|
2768
2776
|
var herdrRunnerRootDir = gen(function* () {
|
|
@@ -2787,8 +2795,10 @@ function startCanonicalHerdrRunnerCommand(input) {
|
|
|
2787
2795
|
HerdrCore,
|
|
2788
2796
|
herdrCoreAgentBackendLayer,
|
|
2789
2797
|
detectHarnesses,
|
|
2798
|
+
dockerCheckDaemon,
|
|
2790
2799
|
dockerCheckPostgres,
|
|
2791
2800
|
liveReplicaIds,
|
|
2801
|
+
removeCheckDocker,
|
|
2792
2802
|
removeCheckPostgres,
|
|
2793
2803
|
checkPostgresSweepInterval,
|
|
2794
2804
|
herdrRunnerCapabilities,
|
|
@@ -3016,7 +3026,9 @@ function startCanonicalHerdrRunnerCommand(input) {
|
|
|
3016
3026
|
const removeOrphanedCheckPostgres = gen(function* () {
|
|
3017
3027
|
const now = yield* currentTimeMillis;
|
|
3018
3028
|
const { items } = yield* controlPlane.client.listRunnerReplicas(input.runnerId);
|
|
3019
|
-
|
|
3029
|
+
const live = liveReplicaIds(items, replica.id, now);
|
|
3030
|
+
yield* removeCheckPostgres(input.runnerId, live);
|
|
3031
|
+
yield* removeCheckDocker(input.runnerId, live);
|
|
3020
3032
|
}).pipe(ignore);
|
|
3021
3033
|
yield* removeOrphanedCheckPostgres;
|
|
3022
3034
|
yield* removeOrphanedCheckPostgres.pipe(delay(checkPostgresSweepInterval), forever, forkScoped);
|
|
@@ -3026,6 +3038,14 @@ function startCanonicalHerdrRunnerCommand(input) {
|
|
|
3026
3038
|
gitCredentials: rpc.gitCredential,
|
|
3027
3039
|
runFiles: { client: rpc, storage },
|
|
3028
3040
|
postgres: dockerCheckPostgres(input.runnerId, replica.id),
|
|
3041
|
+
...settings.checkDockerImage === undefined || settings.checkRegistryImage === undefined ? undefined : {
|
|
3042
|
+
docker: dockerCheckDaemon({
|
|
3043
|
+
runnerId: input.runnerId,
|
|
3044
|
+
replicaId: replica.id,
|
|
3045
|
+
image: settings.checkDockerImage,
|
|
3046
|
+
registryImage: settings.checkRegistryImage
|
|
3047
|
+
})
|
|
3048
|
+
},
|
|
3029
3049
|
...checkSandbox === undefined ? undefined : { sandbox: { ...checkSandbox, apiUrl: input.apiUrl, hosts: settings.checkHosts } }
|
|
3030
3050
|
});
|
|
3031
3051
|
const control = (capacitySlot) => {
|
|
@@ -4312,9 +4332,9 @@ function claimCommand(args) {
|
|
|
4312
4332
|
}
|
|
4313
4333
|
});
|
|
4314
4334
|
}
|
|
4315
|
-
var KitWriter = map8(promise(() => import("./chunk-
|
|
4335
|
+
var KitWriter = map8(promise(() => import("./chunk-axdmbr61.js")), (build) => build.writeKit);
|
|
4316
4336
|
var KitBuilder = Reference("kitman/cli/KitBuilder", {
|
|
4317
|
-
defaultValue: () => (directory) => flatMap3(promise(() => import("./chunk-
|
|
4337
|
+
defaultValue: () => (directory) => flatMap3(promise(() => import("./chunk-axdmbr61.js")), (build) => build.buildKit(directory))
|
|
4318
4338
|
});
|
|
4319
4339
|
var uploadKit = (client, workspaceId, built) => gen(function* () {
|
|
4320
4340
|
yield* client.uploadKitBundle(workspaceId, built.digest, { bundle: encode2(built.bundle) });
|
package/dist/egress-proxy.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Runs alone inside the egress proxy container, so it imports nothing outside Node.
|
|
2
2
|
import { once } from 'node:events'
|
|
3
|
-
import { connect, createServer, type Server, type Socket } from 'node:net'
|
|
3
|
+
import { connect, createServer, type LookupFunction, type Server, type Socket } from 'node:net'
|
|
4
4
|
|
|
5
5
|
/** Whether the allowlist takes `host:port`: an entry `host` takes any port, `host:port` one, `*.domain` subdomains. */
|
|
6
6
|
export const egressAllowed = (hosts: readonly string[], host: string, port: number) => {
|
|
@@ -39,7 +39,93 @@ const httpTarget = (absolute: string) => {
|
|
|
39
39
|
|
|
40
40
|
const answer = (socket: Socket, status: string) => socket.end(`HTTP/1.1 ${status}\r\nContent-Length: 0\r\n\r\n`)
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
/** Takes the next bytes a client sends: how many of them belong to the request body, and whether the body has ended. */
|
|
43
|
+
type BodyFraming = (chunk: Buffer) => { readonly used: number; readonly done: boolean } | 'malformed'
|
|
44
|
+
|
|
45
|
+
const countedBody = (length: number): BodyFraming => {
|
|
46
|
+
let remaining = length
|
|
47
|
+
|
|
48
|
+
return chunk => {
|
|
49
|
+
const used = Math.min(remaining, chunk.length)
|
|
50
|
+
remaining -= used
|
|
51
|
+
|
|
52
|
+
return { used, done: remaining === 0 }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const chunkedBody = (): BodyFraming => {
|
|
57
|
+
let state: 'size' | 'data' | 'trailer' | 'done' = 'size'
|
|
58
|
+
let line = ''
|
|
59
|
+
// The chunk's data and the CRLF after it.
|
|
60
|
+
let remaining = 0
|
|
61
|
+
|
|
62
|
+
return chunk => {
|
|
63
|
+
let index = 0
|
|
64
|
+
|
|
65
|
+
while (index < chunk.length && state !== 'done') {
|
|
66
|
+
if (state === 'data') {
|
|
67
|
+
const used = Math.min(remaining, chunk.length - index)
|
|
68
|
+
index += used
|
|
69
|
+
remaining -= used
|
|
70
|
+
|
|
71
|
+
if (remaining === 0) state = 'size'
|
|
72
|
+
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const byte = chunk[index++]
|
|
77
|
+
|
|
78
|
+
if (byte !== 0x0a) {
|
|
79
|
+
line += String.fromCharCode(byte ?? 0)
|
|
80
|
+
|
|
81
|
+
if (line.length > 4_096) return 'malformed'
|
|
82
|
+
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const content = line.trim()
|
|
87
|
+
line = ''
|
|
88
|
+
|
|
89
|
+
if (state === 'trailer') {
|
|
90
|
+
if (content === '') state = 'done'
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hex = content.split(';')[0]?.trim() ?? ''
|
|
95
|
+
|
|
96
|
+
if (!/^[0-9a-f]+$/i.test(hex)) return 'malformed'
|
|
97
|
+
|
|
98
|
+
const size = Number.parseInt(hex, 16)
|
|
99
|
+
|
|
100
|
+
if (size === 0) state = 'trailer'
|
|
101
|
+
else {
|
|
102
|
+
remaining = size + 2
|
|
103
|
+
state = 'data'
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { used: index, done: state === 'done' }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const requestBody = (headers: readonly string[]): BodyFraming | 'malformed' => {
|
|
112
|
+
const value = (name: string) =>
|
|
113
|
+
headers.flatMap(line => (line.toLowerCase().startsWith(`${name}:`) ? [line.slice(name.length + 1).trim()] : []))
|
|
114
|
+
|
|
115
|
+
const transferEncoding = value('transfer-encoding').join(',').toLowerCase()
|
|
116
|
+
|
|
117
|
+
if (transferEncoding !== '') return /(^|,)\s*chunked\s*$/.test(transferEncoding) ? chunkedBody() : 'malformed'
|
|
118
|
+
|
|
119
|
+
const lengths = [...new Set(value('content-length'))]
|
|
120
|
+
|
|
121
|
+
if (lengths.length === 0) return countedBody(0)
|
|
122
|
+
|
|
123
|
+
const [length = ''] = lengths
|
|
124
|
+
|
|
125
|
+
return lengths.length === 1 && /^\d+$/.test(length) ? countedBody(Number(length)) : 'malformed'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const handle = (hosts: readonly string[], lookup: LookupFunction | undefined) => (client: Socket) => {
|
|
43
129
|
let head = Buffer.alloc(0)
|
|
44
130
|
|
|
45
131
|
const onData = (chunk: Buffer) => {
|
|
@@ -48,6 +134,8 @@ const handle = (hosts: readonly string[]) => (client: Socket) => {
|
|
|
48
134
|
|
|
49
135
|
if (end === -1) return head.length > 16_384 ? answer(client, '431 Request Header Fields Too Large') : undefined
|
|
50
136
|
client.off('data', onData)
|
|
137
|
+
// What a pipelining client sends next arrives while the proxy connects, and a flowing socket would drop it.
|
|
138
|
+
client.pause()
|
|
51
139
|
|
|
52
140
|
const [requestLine = '', ...headers] = head.subarray(0, end).toString().split('\r\n')
|
|
53
141
|
const [method = '', target = '', version = 'HTTP/1.1'] = requestLine.split(' ')
|
|
@@ -57,30 +145,57 @@ const handle = (hosts: readonly string[]) => (client: Socket) => {
|
|
|
57
145
|
|
|
58
146
|
if (!egressAllowed(hosts, destination.host, destination.port)) return answer(client, '403 Forbidden')
|
|
59
147
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
148
|
+
const framing = 'path' in destination ? requestBody(headers) : undefined
|
|
149
|
+
|
|
150
|
+
if (framing === 'malformed') return answer(client, '400 Bad Request')
|
|
151
|
+
|
|
152
|
+
const request = 'path' in destination && framing !== undefined ? { path: destination.path, framing } : undefined
|
|
153
|
+
|
|
154
|
+
const upstream = connect(
|
|
155
|
+
{ port: destination.port, host: destination.host, ...(lookup === undefined ? undefined : { lookup }) },
|
|
156
|
+
() => {
|
|
157
|
+
if (request === undefined) {
|
|
158
|
+
client.write('HTTP/1.1 200 Connection Established\r\n\r\n')
|
|
159
|
+
upstream.write(head.subarray(end + 4))
|
|
160
|
+
client.pipe(upstream)
|
|
161
|
+
upstream.pipe(client)
|
|
162
|
+
client.resume()
|
|
163
|
+
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// One request per connection, and the client's connection ends with its response: a reused one could
|
|
168
|
+
// carry a request for another host to this one, and a pipelined one would mismatch the answers.
|
|
66
169
|
const kept = headers.filter(line => !/^(proxy-)?connection:/i.test(line))
|
|
170
|
+
upstream.write([[method, request.path, version].join(' '), ...kept, 'Connection: close', '', ''].join('\r\n'))
|
|
67
171
|
|
|
68
|
-
|
|
69
|
-
Buffer.concat([
|
|
70
|
-
Buffer.from(
|
|
71
|
-
[[method, destination.path, version].join(' '), ...kept, 'Connection: close', '', ''].join('\r\n')
|
|
72
|
-
),
|
|
73
|
-
head.subarray(end + 4)
|
|
74
|
-
])
|
|
75
|
-
)
|
|
76
|
-
}
|
|
172
|
+
let bodyDone = false
|
|
77
173
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
174
|
+
const forward = (chunk: Buffer) => {
|
|
175
|
+
if (bodyDone) return
|
|
176
|
+
|
|
177
|
+
const framed = request.framing(chunk)
|
|
178
|
+
|
|
179
|
+
if (framed === 'malformed') return client.destroy()
|
|
180
|
+
|
|
181
|
+
bodyDone = framed.done
|
|
182
|
+
|
|
183
|
+
if (upstream.write(chunk.subarray(0, framed.used))) return
|
|
184
|
+
client.pause()
|
|
185
|
+
upstream.once('drain', () => client.resume())
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Pipelined requests after the body are dropped; the client sends them again on a new connection.
|
|
189
|
+
client.on('data', forward)
|
|
190
|
+
upstream.pipe(client)
|
|
191
|
+
client.resume()
|
|
192
|
+
forward(head.subarray(end + 4))
|
|
193
|
+
}
|
|
194
|
+
)
|
|
81
195
|
|
|
82
196
|
upstream.on('error', () => (client.writable ? answer(client, '502 Bad Gateway') : client.destroy()))
|
|
83
197
|
client.on('error', () => upstream.destroy())
|
|
198
|
+
client.on('close', () => upstream.destroy())
|
|
84
199
|
}
|
|
85
200
|
|
|
86
201
|
client.on('data', onData)
|
|
@@ -92,8 +207,10 @@ export const startEgressProxy = (input: {
|
|
|
92
207
|
readonly hosts: readonly string[]
|
|
93
208
|
readonly port: number
|
|
94
209
|
readonly hostname: string
|
|
210
|
+
/** How it resolves a host, if not as the system does. */
|
|
211
|
+
readonly lookup?: LookupFunction
|
|
95
212
|
}): Promise<Server> => {
|
|
96
|
-
const server = createServer(handle(input.hosts)).listen(input.port, input.hostname)
|
|
213
|
+
const server = createServer(handle(input.hosts, input.lookup)).listen(input.port, input.hostname)
|
|
97
214
|
|
|
98
215
|
return once(server, 'listening').then(() => server)
|
|
99
216
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Runs alone inside a pipeline check's sandbox too, so it imports nothing outside Node.
|
|
2
|
+
import { once } from 'node:events'
|
|
3
|
+
import { connect, createServer, type Server, type Socket } from 'node:net'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* srt's proxy closes a connection once its client stops sending, and a Docker client attached to a job stops before
|
|
7
|
+
* the job prints anything. So the encoder inside the sandbox never stops sending: it sends what its client sends as
|
|
8
|
+
* frames, each a 4-byte big-endian length and that many bytes, and the client's end as a frame of length 0. The
|
|
9
|
+
* decoder outside the sandbox turns that frame back into the end of what it sends on.
|
|
10
|
+
*/
|
|
11
|
+
const frameHeader = 4
|
|
12
|
+
|
|
13
|
+
const frame = (chunk: Buffer) => {
|
|
14
|
+
const header = Buffer.alloc(frameHeader)
|
|
15
|
+
|
|
16
|
+
header.writeUInt32BE(chunk.length)
|
|
17
|
+
|
|
18
|
+
return Buffer.concat([header, chunk])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const endOfInput = Buffer.alloc(frameHeader)
|
|
22
|
+
|
|
23
|
+
/** Far more than the one socket chunk the encoder frames; a longer frame is not the encoder's. */
|
|
24
|
+
export const maxFrame = 1024 * 1024
|
|
25
|
+
|
|
26
|
+
/** Holds `from` until `to` has caught up, when a write to `to` was `accepted` only into its buffer. */
|
|
27
|
+
const keepUp = (from: Socket, to: Socket, accepted: boolean) => {
|
|
28
|
+
if (accepted) return
|
|
29
|
+
from.pause()
|
|
30
|
+
to.once('drain', () => from.resume())
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Ends each of `sockets` once another has closed, after what it holds is sent, and destroys them all if one fails. */
|
|
34
|
+
const closeTogether = (...sockets: readonly Socket[]) =>
|
|
35
|
+
sockets.forEach(socket => {
|
|
36
|
+
socket.on('error', () => sockets.forEach(other => other.destroy()))
|
|
37
|
+
socket.on('close', () => sockets.forEach(other => other.end()))
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const listening = (server: Server, port: number) =>
|
|
41
|
+
once(server.listen(port, '127.0.0.1'), 'listening').then(() => server)
|
|
42
|
+
|
|
43
|
+
/** Serves `port` outside the sandbox, sending each connection's frames on to `127.0.0.1:<targetPort>`. */
|
|
44
|
+
export const startHalfCloseDecoder = (input: { readonly port: number; readonly targetPort: number }) =>
|
|
45
|
+
listening(
|
|
46
|
+
createServer({ allowHalfOpen: true }, client => {
|
|
47
|
+
const upstream = connect({ port: input.targetPort, host: '127.0.0.1', allowHalfOpen: true })
|
|
48
|
+
let pending: Buffer = Buffer.alloc(0)
|
|
49
|
+
|
|
50
|
+
client.on('data', (chunk: Buffer) => {
|
|
51
|
+
pending = Buffer.concat([pending, chunk])
|
|
52
|
+
|
|
53
|
+
if (pending.length >= frameHeader && pending.readUInt32BE(0) > maxFrame) return void client.destroy()
|
|
54
|
+
|
|
55
|
+
while (pending.length >= frameHeader && pending.length >= frameHeader + pending.readUInt32BE(0)) {
|
|
56
|
+
const length = pending.readUInt32BE(0)
|
|
57
|
+
|
|
58
|
+
if (length === 0) upstream.end()
|
|
59
|
+
else keepUp(client, upstream, upstream.write(pending.subarray(frameHeader, frameHeader + length)))
|
|
60
|
+
pending = pending.subarray(frameHeader + length)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
client.on('end', () => upstream.end())
|
|
64
|
+
upstream.pipe(client)
|
|
65
|
+
closeTogether(client, upstream)
|
|
66
|
+
}),
|
|
67
|
+
input.port
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
/** `host` as `net.connect` takes it: a URL writes an IPv6 address in brackets. */
|
|
71
|
+
const unbracketed = (host: string) => host.replace(/^\[(.*)\]$/, '$1')
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Serves `port` inside the sandbox, sending each connection through `proxy` to the decoder on the Runner's
|
|
75
|
+
* `127.0.0.1:<targetPort>`, as frames.
|
|
76
|
+
*/
|
|
77
|
+
export const startHalfCloseEncoder = (input: {
|
|
78
|
+
readonly port: number
|
|
79
|
+
readonly proxy: string
|
|
80
|
+
readonly targetPort: number
|
|
81
|
+
}) => {
|
|
82
|
+
const proxy = new URL(input.proxy)
|
|
83
|
+
const target = `127.0.0.1:${input.targetPort}`
|
|
84
|
+
|
|
85
|
+
const credential =
|
|
86
|
+
proxy.username === ''
|
|
87
|
+
? ''
|
|
88
|
+
: `Proxy-Authorization: Basic ${Buffer.from(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`).toString('base64')}\r\n`
|
|
89
|
+
|
|
90
|
+
return listening(
|
|
91
|
+
createServer({ allowHalfOpen: true }, client => {
|
|
92
|
+
client.pause()
|
|
93
|
+
const upstream = connect({ port: Number(proxy.port === '' ? 80 : proxy.port), host: unbracketed(proxy.hostname) })
|
|
94
|
+
let head = Buffer.alloc(0)
|
|
95
|
+
|
|
96
|
+
const onHead = (chunk: Buffer) => {
|
|
97
|
+
head = Buffer.concat([head, chunk])
|
|
98
|
+
const end = head.indexOf('\r\n\r\n')
|
|
99
|
+
|
|
100
|
+
if (end === -1) return
|
|
101
|
+
upstream.off('data', onHead)
|
|
102
|
+
|
|
103
|
+
if (!/^HTTP\/1\.[01] 200/.test(head.toString('latin1', 0, end))) return void upstream.destroy()
|
|
104
|
+
client.write(head.subarray(end + 4))
|
|
105
|
+
upstream.pipe(client)
|
|
106
|
+
client.on('data', (chunk: Buffer) => keepUp(client, upstream, upstream.write(frame(chunk))))
|
|
107
|
+
client.on('end', () => upstream.write(endOfInput))
|
|
108
|
+
client.resume()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
upstream.on('data', onHead)
|
|
112
|
+
upstream.write(`CONNECT ${target} HTTP/1.1\r\nHost: ${target}\r\n${credential}\r\n`)
|
|
113
|
+
closeTogether(client, upstream)
|
|
114
|
+
}),
|
|
115
|
+
input.port
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// `bun half-close-relay.ts <port> <proxy URL> <decoder port>`, inside srt.
|
|
120
|
+
if (import.meta.main)
|
|
121
|
+
await startHalfCloseEncoder({
|
|
122
|
+
port: Number(Bun.argv[2]),
|
|
123
|
+
proxy: Bun.argv[3] ?? '',
|
|
124
|
+
targetPort: Number(Bun.argv[4])
|
|
125
|
+
})
|
package/dist/index.js
CHANGED
|
@@ -8,12 +8,12 @@ if (args[0] === "srt") {
|
|
|
8
8
|
const { runSrtCli } = await import("./chunk-wczs5vrq.js");
|
|
9
9
|
await runSrtCli();
|
|
10
10
|
} else if (args[0] === "srt-step") {
|
|
11
|
-
const { runSrtStep } = await import("./chunk-
|
|
11
|
+
const { runSrtStep } = await import("./chunk-99ver3za.js");
|
|
12
12
|
await runSrtStep(args.slice(1));
|
|
13
13
|
} else if (args[0] === "opencode-read") {
|
|
14
14
|
const { runOpencodeReader } = await import("./chunk-rjfxnk05.js");
|
|
15
15
|
runOpencodeReader();
|
|
16
16
|
} else {
|
|
17
|
-
const { runCli } = await import("./chunk-
|
|
17
|
+
const { runCli } = await import("./chunk-dj7pp4w4.js");
|
|
18
18
|
runCli(args);
|
|
19
19
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Runs alone inside a check Docker daemon's API forwarder container, so it imports nothing outside Node.
|
|
2
|
+
import { once } from 'node:events'
|
|
3
|
+
import { connect, createServer, type Server } from 'node:net'
|
|
4
|
+
|
|
5
|
+
/** Forwards each connection to `port` on to `target:targetPort`, and only there. */
|
|
6
|
+
export const startTcpForwarder = (input: {
|
|
7
|
+
readonly port: number
|
|
8
|
+
readonly target: string
|
|
9
|
+
readonly targetPort: number
|
|
10
|
+
}): Promise<Server> => {
|
|
11
|
+
// A Docker client attached to a container stops sending long before the container's output ends.
|
|
12
|
+
const server = createServer({ allowHalfOpen: true }, client => {
|
|
13
|
+
const upstream = connect({ port: input.targetPort, host: input.target, allowHalfOpen: true })
|
|
14
|
+
|
|
15
|
+
client.pipe(upstream)
|
|
16
|
+
upstream.pipe(client)
|
|
17
|
+
client.on('error', () => upstream.destroy())
|
|
18
|
+
upstream.on('error', () => client.destroy())
|
|
19
|
+
}).listen(input.port, '0.0.0.0')
|
|
20
|
+
|
|
21
|
+
return once(server, 'listening').then(() => server)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (import.meta.main)
|
|
25
|
+
await startTcpForwarder({
|
|
26
|
+
port: Number(Bun.argv[2]),
|
|
27
|
+
target: Bun.argv[3] ?? '',
|
|
28
|
+
targetPort: Number(Bun.argv[4])
|
|
29
|
+
})
|
package/package.json
CHANGED
|
File without changes
|