@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.
@@ -6,7 +6,7 @@ import {
6
6
  decideSecretRequest,
7
7
  readSecretRegistry,
8
8
  appendSecretRefusals
9
- } from "./chunk-3d6abgjh.js";
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 entry = path.join(root, ".kitman-build-entry.ts");
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.readFile(bundleFile).pipe(mapError2((error) => new BundleFailed({ reason: error.message })));
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-3d6abgjh.js";
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-postgres.ts
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
- "proxy=${HTTP_PROXY:?srt set no HTTP_PROXY}",
5416
- "proxy=${proxy#*://}",
5417
- "proxy=${proxy%%/*}",
5418
- "auth=",
5419
- "case $proxy in *@*)",
5420
- ` auth=$(printf %s "\${proxy%@*}" | awk 'BEGIN { for (i = 1; i < 256; i++) hex[sprintf("%02X", i)] = sprintf("%c", i) }
5421
- { 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 }')`,
5422
- " proxy=${proxy##*@} ;;",
5423
- "esac",
5424
- "log=$(mktemp)",
5425
- `socat TCP-LISTEN:${listenPort},bind=127.0.0.1,fork,reuseaddr "PROXY:\${proxy%:*}:127.0.0.1:${hostPort},proxyport=\${proxy##*:}\${auth:+,proxyauth=$auth}" 2>"$log" &`,
5426
- "tunnel=$!",
5427
- 'fail() { echo "Kitman: the Postgres tunnel did not open: $1" >&2; cat "$log" >&2; kill $tunnel 2>/dev/null; rm -f "$log"; exit 1; }',
5428
- "tries=0",
5429
- "while :; do",
5430
- ' kill -0 $tunnel 2>/dev/null || fail "its listener stopped"',
5431
- ` answer=$( (printf '${sslRequestOctal}'; sleep 1) | socat - TCP:127.0.0.1:${listenPort} 2>/dev/null | head -c 1)`,
5432
- " case $answer in N|S) break ;; esac",
5433
- " tries=$((tries + 1))",
5434
- ' [ $tries -lt 30 ] || fail "Postgres did not answer through it"',
5435
- " sleep 0.2",
5436
- "done",
5437
- 'kill -0 $tunnel 2>/dev/null || fail "its listener stopped"',
5438
- 'sh -c "$1"',
5439
- "status=$?",
5440
- "kill $tunnel 2>/dev/null",
5441
- 'rm -f "$log"',
5442
- "exit $status"
5443
- ].join(`
5444
- `);
5445
- var postgresSocketForwarder = (hostPort) => gen(function* () {
5446
- const fs = yield* FileSystem;
5447
- const dir = yield* fs.realPath(yield* fs.makeTempDirectoryScoped({ prefix: "kpg-" }));
5448
- yield* acquireRelease(callback3((resume) => {
5449
- const open = new Set;
5450
- const server = net.createServer((client) => {
5451
- const upstream = net.connect(hostPort, "127.0.0.1");
5452
- for (const socket of [client, upstream]) {
5453
- open.add(socket);
5454
- socket.on("close", () => open.delete(socket));
5455
- }
5456
- client.pipe(upstream);
5457
- upstream.pipe(client);
5458
- client.on("error", () => upstream.destroy());
5459
- upstream.on("error", () => client.destroy());
5460
- });
5461
- server.once("error", (error) => resume(fail7(new CheckPostgresUnavailable({ reason: error.message }))));
5462
- server.listen(`${dir}/.s.PGSQL.${checkPostgresSandboxPort}`, () => resume(succeed6({ server, open })));
5463
- }), ({ server, open }) => callback3((resume) => {
5464
- server.close(() => resume(void_3));
5465
- open.forEach((socket) => socket.destroy());
5466
- }));
5467
- return dir;
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
- ...runner,
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 policy = checkSandboxPolicy(work, sandbox, checkout, cache, filesDir, route.hosts);
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: route.tunnel === undefined ? ["sh", "-c", command] : ["sh", "-c", route.tunnel, "sh", command],
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.54";
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-c0wrcam4.js"));
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
- yield* removeCheckPostgres(input.runnerId, liveReplicaIds(items, replica.id, now));
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-my987gxf.js")), (build) => build.writeKit);
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-my987gxf.js")), (build) => build.buildKit(directory))
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) });
@@ -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
- const handle = (hosts: readonly string[]) => (client: Socket) => {
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 upstream = connect(destination.port, destination.host, () => {
61
- if (!('path' in destination)) {
62
- client.write('HTTP/1.1 200 Connection Established\r\n\r\n')
63
- upstream.write(head.subarray(end + 4))
64
- } else {
65
- // One request per connection: a reused one could carry a request for another host to this one.
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
- upstream.write(
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
- client.pipe(upstream)
79
- upstream.pipe(client)
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-19aqk325.js");
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-y48wz1sa.js");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitman-sh/cli",
3
- "version": "0.10.54",
3
+ "version": "0.10.55",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {
File without changes