@cat-factory/executor-harness 1.145.1 → 1.149.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -142,6 +142,34 @@ Three rules bind anything added to it:
142
142
  installed in this image unconditionally, `entrypoint.sh` starts the rootless daemon best-effort
143
143
  and execs the server without waiting for it, so at job start this probe is the only thing that
144
144
  knows how that went.
145
+ - **A daemon that ANSWERS is not a daemon that WORKS**, which is the same mistake one level in and
146
+ the one this block used to make. A rootless daemon nested inside a sandbox serves throughout
147
+ while its snapshotter cannot mount an image layer, so `docker info` succeeds and `docker build`,
148
+ `docker run` and `docker pull` of anything multi-layer all fail on one EINVAL. Issue #2120 is
149
+ three agents in a single run each paying to disprove the claim, against a block that also tells
150
+ them not to re-check it. Only a container that RAN settles it, so the reachable case is split by
151
+ a real workload (`src/docker-capability.ts`) into `usable`, `unusable` and a daemon that answered
152
+ while the check could not be carried out. **Only `usable` may say the commands work**, and the
153
+ asymmetry runs the other way too: a failure of the platform's own machinery reports that it
154
+ could not tell, never that the daemon is broken. That covers every step before the run (no probe
155
+ payload on this machine, a daemon whose architecture the payload is not built for, `docker load`
156
+ refusing the archive) AND the halves of a failed run that are ours rather than the daemon's,
157
+ which is what docker's exit 126/127, a tag that did not resolve and an unexecutable payload are.
158
+ - **A daemon that runs containers is not a daemon whose containers have a NETWORK**, which is the
159
+ same mistake one level in again. Loading and running a local image needs no network at all, so the
160
+ workload check above passes identically on a daemon whose nested containers are cut off: every
161
+ published image ran its rootless daemon with `--iptables=false`, which drops the MASQUERADE rule
162
+ for its bridge, and the harness reported `usable` while every `docker build` that fetched a
163
+ dependency was guaranteed to fail (issue #2174). So `usable` carries its own egress verdict,
164
+ measured from INSIDE a nested container (a TCP connect to a raw address, and a name to resolve),
165
+ and the rendered line says something different for each. The `blocked` wording is precise about
166
+ which commands break, because "docker has no network" is false and would have an agent skip work
167
+ it could do: the daemon pulls base images and `docker compose up` of pre-built images works, while
168
+ every `RUN` line that fetches anything fails, SLOWLY, since npm reports `EAI_AGAIN` only once its
169
+ retry backoff gives up and the build reads as a hang. `reachable` needs the connect AND the
170
+ lookup, since nothing an agent installs is fetched by address; a resolved name with a refused
171
+ connect is undetermined rather than blocked, because the resolution proves a path out exists and
172
+ the likelier cause is a deployment that filters the address the check was pointed at.
145
173
  - **A daemon that is STARTING is not a daemon that is absent.** Because the entrypoint does not
146
174
  wait, the backend dispatches seconds before there is a socket, and `docker info` is then refused
147
175
  at once rather than slowly. So a refusal is read against `DOCKER_HOST`, which the entrypoint sets
@@ -374,24 +402,88 @@ Docker socket would hand the container root on the host.
374
402
 
375
403
  - `GET /health` reports it, so an operator (and a boot-time probe) can see what the container
376
404
  concluded about itself.
377
- - The compose stand-up REFUSES on a decided absence and says why, instead of running compose
405
+ - The compose stand-up REFUSES on a decided negative and says why, instead of running compose
378
406
  against nothing and handing the agent a connection error to interpret. The refusal rides back on
379
- the Tester step as `infraSetup.dockerAvailable: false` with the cause.
407
+ the Tester step as the cause plus the three facts that decide where a human should look:
408
+ `infraSetup.dockerAvailable` (was anything answering), `infraSetup.dockerWorkload` (what a
409
+ container did on it) and `infraSetup.dockerEgress` (what that container could REACH). Three
410
+ fields rather than one, because the daemon has three ways to stop the work: nothing to talk to, a
411
+ daemon that answers and cannot run a container, and a daemon that runs containers and gives them
412
+ no network. Flattening the second onto `dockerAvailable: false` renders as "no Docker daemon in
413
+ the executor" and sends an operator to restart a daemon that is already up; flattening the third
414
+ onto `dockerWorkload: 'usable'` renders as a sandbox where the stack works, which is the reading
415
+ that let every `docker build` in a `--iptables=false` container fail unexplained for months.
416
+
417
+ **What the entrypoint probes for is a SOCKET, and serving is not usable.** That is the whole of
418
+ what a boot record can know, and it is weaker than what either consumer wants: a rootless daemon in
419
+ a sandbox answers `docker version` while being unable to mount an image, so compose ran and died on
420
+ a mount error inside the one mechanism whose job is to explain why the dependencies did not come up
421
+ (issue #2120). The live half of the verdict is therefore a real workload (load a one-layer image
422
+ and run a container from it, `src/docker-capability.ts`), and `resolveDockerVerdict` consults it in
423
+ BOTH directions: a recorded absence a working daemon contradicts, and a recorded presence that
424
+ cannot run anything. `GET /health` reports the last measurement beside the boot record under
425
+ `docker.workload` and never takes one itself, since it is polled; `unmeasured` is one of its answers.
426
+
427
+ **The weaker fact did not stop mattering, though, and it is what a stale record is read against.**
428
+ A workload check can come back undeterminable for reasons that have nothing to do with whether a
429
+ daemon is up (no probe payload in a deployment's own image variant, an architecture it is not built
430
+ for, a `docker load` the engine refuses, a timeout), so falling straight back to the boot record
431
+ there would re-latch the very refusal the paragraph below rules out. The check therefore reports
432
+ `daemonAnswered` alongside its `unknown`, established on its way past at no extra cost, and a
433
+ daemon that merely ANSWERED overrules a recorded absence exactly as the old `docker version` probe
434
+ did. Only a check that never reached a daemon at all leaves the record to decide.
435
+
436
+ The workload check is memoised per container for a POSITIVE answer only. A daemon that has run a
437
+ container proved something that does not stop being true; a negative is re-measured for the same
438
+ reason a recorded absence is, and it fails fast anyway.
380
439
 
381
440
  The verdict is three-valued, and that is the point. `false` is a decided absence. `undefined` is
382
441
  "nothing decided" — the probe is still in flight, or nothing recorded anything at all, which is the
383
442
  normal state under the native host transport (`LOCAL_NATIVE_AGENTS`) where the harness runs on a
384
- developer's machine with no entrypoint. Undecided attempts the stand-up; only a decided absence
385
- refuses it.
443
+ developer's machine with no entrypoint. Undecided attempts the stand-up, and nothing probes it into
444
+ a refusal; only a decided negative refuses.
386
445
 
387
446
  What is recorded describes BOOT, and a container outlives its boot: a warm pool serves many jobs
388
447
  from one, and a sidecar daemon that took longer to come up than the entrypoint's bounded wait
389
- allows is serving perfectly well by the second job. So a recorded absence is a hypothesis, not the
448
+ allows is serving perfectly well by the second job. So a recorded verdict is a hypothesis, not the
390
449
  refusal: `resolveDockerVerdict` re-checks it against a live daemon at the moment a stand-up is
391
450
  about to run, and the record supplies what only the record holds, the cause and the daemon's own
392
451
  log tail. `GET /health` deliberately keeps reporting the boot record rather than probing per poll,
393
452
  since it is not the surface that acts on the answer.
394
453
 
454
+ **Which daemon the container ends up with is a choice made on evidence.** `--iptables=false`
455
+ arrived because the daemon could not start at all without it: a sandbox like Cloudflare Containers
456
+ gives it no way to install its firewall rules, and it refuses to start. What went unnoticed is what
457
+ the flag costs once the daemon DOES start. The rule it drops is the MASQUERADE for the bridge, so a
458
+ nested container is never NATed and has no egress whatsoever, no DNS and no raw IP either. The
459
+ daemon's own `docker pull` keeps working, which is most of why it stayed hidden for so long, and the
460
+ cost lands on a `docker build` whose `RUN npm ci` sits in npm's retry backoff for some seven minutes
461
+ before failing (issue #2173). So the entrypoint starts the daemon that manages its own rules first
462
+ and falls back to `--iptables=false` only when that one EXITS without serving.
463
+
464
+ The death, and not a clock, is what decides. A sandbox with no iptables binary and no NAT module
465
+ does not make the daemon slow, it makes `dockerd` exit at once with the reason on its log, so an
466
+ exit is the one observation here that is actually about the flags. A first arm still starting when
467
+ the budget runs out is a cold sandbox as often as a wedged one, and swapping there would take a
468
+ capable daemon away for the container's whole life on a guess; it is recorded as undecided and LEFT
469
+ RUNNING instead, so `resolveDockerVerdict`'s live re-probe can still find it with its NAT intact. A
470
+ sandbox that genuinely cannot do iptables ends up exactly where it was; a privileged Docker or
471
+ Podman host, which is what local mode runs on, gets working nested networking.
472
+
473
+ Each arm gets its OWN rootlesskit state directory, image store and pid file, and the abandon path
474
+ waits for the process to be gone after `kill -9`. SIGKILL is not propagated, so a launcher that had
475
+ already forked the real `dockerd` dies while its child holds an exclusive lock on the data root and
476
+ a live pid file: sharing either would make the fallback fail with `pid file found` for a reason that
477
+ has nothing to do with why it was started, which is the worst outcome available here (both arms
478
+ record `failed` and the container ends up with no daemon at all). The socket is the one thing that
479
+ cannot be per-arm, since `DOCKER_HOST` names it, and dockerd unlinks and rebinds it.
480
+
481
+ The two arms record different `reason` words (`serving` and `serving-without-nat`) with a detail
482
+ that names the CONSEQUENCE, which the workload check measures from inside a nested container without
483
+ ever learning why. What the detail does NOT do is name a cause: "iptables is unavailable here" is
484
+ not something the entrypoint measures, and the real cause is the daemon's own log tail, which rides
485
+ the stderr line announcing the switch.
486
+
395
487
  Why it is written down at all: the image shipped for months with `docker-ce-rootless-extras` (the
396
488
  wrappers that START a daemon) and no `docker-ce` (the daemon itself), and no `iproute2` for the
397
489
  network rootlesskit builds. The entrypoint backgrounded the start in a subshell where its exit
@@ -438,10 +530,13 @@ verdict is stated.
438
530
  | `src/bootstrap-mode.ts` | The repo-bootstrap MODE: clone-a-reference-or-scaffold → run the agent → refuse to push an empty tree → reinit + force-push to the pre-created target repo. |
439
531
  | `src/artifact-upload.ts` | The OUTBOUND half of the artifact seam: parses the body's `artifactUpload` and projects it onto the agent's env as `ARTIFACT_UPLOAD_URL` / `ARTIFACT_UPLOAD_TOKEN`, registering the token for redaction first. Passes through what the body carries and decides nothing: which kinds get the seam is the backend's call. |
440
532
  | `src/codex-images.ts` | Codex's own `image_gen` output, staged where the agent can reach it: creates `$CODEX_HOME/generated_images` as a symlink into `.cat-context/binary-output/generated/` before the CLI starts, sweeps anything a failed redirect left behind, and unlinks (never follows) the redirect at teardown — a failed unlink is REPORTED, because that unlink is what stops the recursive delete reaching the checkout. Exists because codex exposes no path for what it generated AND `$CODEX_HOME` holds the run's decrypted credential, so neither asking the agent nor sending it there is available. |
441
- | `src/environment-inventory.ts` | What the MACHINE holds, probed once per job and appended to the agent's system prompt as an ENVIRONMENT INVENTORY block. The only layer that can state it: the backend composes its prompt before a transport is chosen, and the same body serves this image, a deployment's own variant and the developer's laptop under `LOCAL_NATIVE_AGENTS`. Three-valued on purpose, so a probe that failed renders as unknown rather than as an absence, and the Docker DAEMON is answered by running `docker info` rather than by finding the CLI, which is installed here either way. See [The environment is probed once, not by the agent](#the-environment-is-probed-once-not-by-the-agent). |
533
+ | `src/environment-inventory.ts` | What the MACHINE holds, probed once per job and appended to the agent's system prompt as an ENVIRONMENT INVENTORY block. The only layer that can state it: the backend composes its prompt before a transport is chosen, and the same body serves this image, a deployment's own variant and the developer's laptop under `LOCAL_NATIVE_AGENTS`. Three-valued on purpose, so a probe that failed renders as unknown rather than as an absence; Docker gets FIVE, because a daemon that answers `docker info` is not a daemon that can run a container. See [The environment is probed once, not by the agent](#the-environment-is-probed-once-not-by-the-agent). |
442
534
  | `src/agent-shared.ts` | The few helpers every agent MODE shares (effort-report folding, the capability fields forwarded to `runAgentInWorkspace`). |
443
535
  | `src/logger.ts` | Structured logging. |
444
- | `src/docker-status.ts` | This container's own verdict about its Docker daemon, as recorded by `entrypoint.sh`. Three-valued on purpose: a daemon that FAILED and a daemon nobody asked about are different facts, and only a DECIDED absence refuses a stand-up. See [Local infra: the container's Docker daemon](#local-infra-the-containers-docker-daemon). |
536
+ | `src/docker-status.ts` | This container's own verdict about its Docker daemon, as recorded by `entrypoint.sh`. Three-valued on purpose: a daemon that FAILED and a daemon nobody asked about are different facts, and only a DECIDED negative refuses a stand-up. See [Local infra: the container's Docker daemon](#local-infra-the-containers-docker-daemon). |
537
+ | `src/docker-capability.ts` | Whether the daemon can RUN A CONTAINER, which is the fact every caller wanted and `docker info` does not answer. Loads a one-layer image built in-process and runs it; `usable` / `unusable` / `unknown`, and only the container RUN may produce the middle one, AND only where the daemon is what refused it (docker's 126/127, a tag that did not resolve and an unexecutable payload are the platform's own machinery, so they say "could not tell"). `usable` then carries what a SECOND container, on the default network, could reach: `reachable` / `blocked` / `undetermined`, on the same asymmetry (a missing busybox applet is the platform's gap, never an absent network). Total: it answers even if it throws. One budget for the whole pass plus a separate one for the egress container, cancelled when the last caller abandons it, memoised per container once SETTLED: any positive, plus every negative whose cause cannot change under a running container (a rejected target, a payload with no `nc`, a filtered address). Only a genuinely transient failure is re-measured, because re-measuring is two container starts and an image load per job on the critical path. |
538
+ | `src/docker-probe-image.ts` | The one-layer docker-archive that check loads, assembled here from a statically linked binary already in the image, so the whole thing is local: no registry, no network, no second image. Pure, byte-stable, and it names the architecture the DAEMON reported rather than this process's. Also builds the egress container's argv, and validates the address it aims at (an IPv4 literal, never a name: a connect that has to resolve first cannot separate a broken route from broken DNS). The connect uses busybox `nc -z` where the payload has it and no `-w` where it does not, both under `busybox timeout`, because `-w` covers the FINAL NET READ: on a TLS port, a connect that succeeded then exits non-zero and reads as a route that is not there. |
539
+ | `src/docker-command.ts` | How the harness runs one `docker …` command on its own behalf: an argv (no shell), a stdin body, stdout kept apart from stderr, one required timeout, the job's signal, and `killChildProcess` for the kill. Not a second `captured-command.ts`, which stays the one way a DECLARED shell command runs; the header says which of its choices this needs to differ on and why. |
445
540
  | `src/agent-env.ts` | The env for anything the harness spawns into the agent's CHECKOUT: its own environment minus the variables that are facts about the HARNESS. Today that is `NODE_ENV` (the harness runs in production mode, and an inherited `NODE_ENV=production` makes npm omit devDependencies in a checkout that never asked for it) and `PORT` (the port this harness is listening on, so a service that reads it would bind the one address in the container's network namespace that is already taken). |
446
541
 
447
542
  ## Runner lifecycle knobs
@@ -466,8 +561,11 @@ runner):
466
561
  | `REPRODUCTION_TOTAL_BUDGET_MS` | `2700000` (45m) | Wall-clock ceiling on the WHOLE proof phase (every attempt, both trees, setup included). Attempts multiply two full tree runs each and the heartbeat above deliberately stops the inactivity watchdog from firing, so this is what bounds the phase. Checked at phase boundaries; exceeding it settles `inconclusive`, never a run failure. |
467
562
  | `HARNESS_TRANSCRIPT_TTL_MS` | `259200000` (3d) | How long lifted subscription-CLI session transcripts are kept before the retention sweep prunes them. |
468
563
  | `HARNESS_TRANSCRIPT_ROOT` | `<tmpdir>/cf-agent-transcripts` | Where retained session transcripts are moved to (one dir per run). Meaningful only on a reused (warm-pool) container; a per-run container is torn down with the job. The TTL sweep deletes only dirs it created (each carries a `.cf-retained` marker), so pointing this at a shared directory never touches unrelated content, though a dedicated dir is still recommended. An override on a different filesystem than the config home falls back to copy-then-remove. |
469
- | `HARNESS_DOCKER_READY_TIMEOUT_SECONDS` | `60` | How long `entrypoint.sh` waits for the container's Docker daemon before recording it unavailable. Only a HUNG daemon pays this in full: the wait ends early both when the socket answers and when the daemon process is gone. It runs in the BACKGROUND, so it never delays the container's boot. |
564
+ | `HARNESS_DOCKER_READY_TIMEOUT_SECONDS` | `60` | How long `entrypoint.sh` spends GETTING a Docker daemon to answer, in total, before recording it unavailable. One budget for the whole sequence, not one per arm: the rootless path may start a second daemon after the first exits, and the fallback gets what is left of this (floored at 10s, the only thing that can push the sequence past it, and only when a daemon took the whole window to die). Only a HUNG daemon pays it in full: the wait ends early both when the socket answers and when the daemon process is gone. It runs in the BACKGROUND, so it never delays the container's boot. |
470
565
  | `HARNESS_DOCKER_STATUS_FILE` | `/tmp/harness-docker-status.json` | Where that verdict is recorded. `entrypoint.sh` writes it and the harness reads it, so an override must be set for BOTH (they share one process env). |
566
+ | `HARNESS_DOCKER_EGRESS_TARGET` | `1.1.1.1:443` | Where the egress check connects FROM INSIDE a nested container, to find out whether this daemon's containers have a route out. An `IPv4:port`, never a name (a connect that has to resolve first cannot separate a broken route from broken DNS). The default aims at the PUBLIC internet: a deployment that deliberately has none should point this, and the DNS name below, at what it does run, or the verdict is an honest `blocked` about two addresses that were never the ones that matter. A value that is not an address is REPORTED as a check that could not be carried out, never silently replaced. |
567
+ | `HARNESS_DOCKER_EGRESS_DNS_NAME` | `registry.npmjs.org` | The name that same container resolves, which is the other half of egress: nothing an agent installs is fetched by address, so a working route with broken DNS is still reported as blocked, with the detail naming DNS as the half to fix. |
568
+ | `HARNESS_DOCKER_PROBE_BINARY` | `/bin/busybox` | The statically linked binary the platform builds its one-layer probe image from, for the check that answers whether this daemon can RUN a container. Only an image variant that ships it elsewhere needs this. Absent is a supported answer, not a failure: the check then reports that it could not be carried out, which is what the native host transport gets on a developer laptop. |
471
569
 
472
570
  ## Build / test
473
571
 
package/dist/agent.js CHANGED
@@ -137,8 +137,14 @@ export async function handleAgent(job, opts = {}) {
137
137
  // This sits on the critical path AHEAD of the clone, which is the cost of having one
138
138
  // composition point instead of one per mode (each mode owns its own clone, so there is no
139
139
  // single post-clone place to put this). The pass is sized for that: everything in it runs
140
- // concurrently, every probe is a call that answers in milliseconds or is wedged, and the one
141
- // deliberate wait is a single short retry for a daemon that is still starting.
140
+ // concurrently, and every probe either answers in milliseconds or is bounded. Two of them are
141
+ // deliberate waits rather than instant answers: one short retry for a daemon that is still
142
+ // starting, and, only once a daemon has answered, the CONTAINERS the platform runs to find out
143
+ // what this daemon can do (`docker-capability.ts`: one to prove it runs a container at all,
144
+ // then one on the default network to see what that container reaches). Both are budgeted, and
145
+ // the pair is memoised per container once it has SETTLED, which is any positive plus every
146
+ // negative that cannot change under a running container. Both take the job's signal, so an
147
+ // abandoned run stops paying at once.
142
148
  const staged = {
143
149
  ...job,
144
150
  systemPrompt: await appendEnvironmentInventory(job.systemPrompt, {
@@ -0,0 +1,158 @@
1
+ import { type DockerCommandRunner } from './docker-command.js';
2
+ import { type Logger } from './logger.js';
3
+ /**
4
+ * What a NESTED container could reach, measured from inside one.
5
+ *
6
+ * Measured there and nowhere else, which is the whole point. The harness container's own network
7
+ * is fine in both cases: it resolves and fetches normally, and the daemon pulls images
8
+ * successfully, so every check one layer out reports a working network over a daemon whose
9
+ * containers have none.
10
+ *
11
+ * `reachable` needs BOTH halves. A route with no DNS is not a network an agent can use: nothing
12
+ * it installs is fetched by address, so `blocked` is the honest verdict and the detail names DNS
13
+ * as the half to fix. The reverse (a name resolved, the configured address refused) is
14
+ * `undetermined` instead of `blocked`, because a resolved name proves a path out exists and the
15
+ * likeliest cause is that this deployment filters the address the check was pointed at.
16
+ */
17
+ export type ContainerEgress =
18
+ /** A container opened a TCP connection out AND resolved a public name. */
19
+ {
20
+ status: 'reachable';
21
+ }
22
+ /** It could not get out. `detail` names how far it got, since the two have different fixes. */
23
+ | {
24
+ status: 'blocked';
25
+ detail: string;
26
+ }
27
+ /** The check could not be carried out, or could not be read as evidence about the network. */
28
+ | {
29
+ status: 'undetermined';
30
+ reason: string;
31
+ /**
32
+ * Whether asking again could give a different answer.
33
+ *
34
+ * Load-bearing, because the probe re-measures an undetermined egress and re-measuring
35
+ * means two container starts plus an image load, per job, on the critical path ahead of
36
+ * the clone. Most of the ways to land here cannot change while this container lives: a
37
+ * rejected `HARNESS_DOCKER_EGRESS_TARGET`, a payload with no `nc` applet, an address this
38
+ * deployment filters. Latching those is not a stale verdict, it is the same measurement
39
+ * with the same inputs; re-running it forever costs every job about forty seconds for a
40
+ * value that is settled. Only a genuinely transient failure (a timeout, a cancelled job,
41
+ * a daemon that could not attach the bridge yet) is worth asking about again.
42
+ */
43
+ recheck: boolean;
44
+ };
45
+ /** What one measurement concluded. See the three answers above; nothing collapses them. */
46
+ export type DockerWorkload = {
47
+ status: 'usable';
48
+ egress: ContainerEgress;
49
+ } | {
50
+ status: 'unusable';
51
+ detail: string;
52
+ } | {
53
+ status: 'unknown';
54
+ reason: string;
55
+ /**
56
+ * Whether a daemon ANSWERED before the check ran out of things it could do. A weaker fact
57
+ * than the check exists to establish, and the one a stale boot record must be read against.
58
+ */
59
+ daemonAnswered: boolean;
60
+ };
61
+ /**
62
+ * A one-slot memo for the assembled archive.
63
+ *
64
+ * The archive is byte-stable for one `(payload path, architecture)` pair by construction
65
+ * (docker-probe-image.ts pins every timestamp for exactly this reason), and neither half changes
66
+ * within a process. Since a NEGATIVE verdict is re-measured on purpose and two sites ask per job,
67
+ * without this the same two megabytes are re-read and re-sha256'd four times per job for a value
68
+ * that cannot differ. One slot rather than a map: there is only ever one key.
69
+ */
70
+ export interface ProbeArchiveMemo {
71
+ read(key: string): Buffer | undefined;
72
+ write(key: string, archive: Buffer): void;
73
+ }
74
+ /** Build a {@link ProbeArchiveMemo}. Supplied only by {@link realDeps}, so a test memoises nothing. */
75
+ export declare function oneSlotArchiveMemo(): ProbeArchiveMemo;
76
+ /** What a measurement needs from the machine, so a test can supply all of it. */
77
+ export interface DockerWorkloadDeps {
78
+ /** Read the probe payload. Rejecting (ENOENT) is the supported "this machine has none". */
79
+ readPayload: (path: string) => Promise<Buffer>;
80
+ payloadPath: string;
81
+ runDocker: DockerCommandRunner;
82
+ /** This process's architecture, as `process.arch` spells it: the PAYLOAD's, never the daemon's. */
83
+ arch: string;
84
+ /** Where the egress container aims, as configured. Validated at use, never here. */
85
+ egress: {
86
+ target: string;
87
+ dnsName: string;
88
+ };
89
+ logger?: Logger;
90
+ archives?: ProbeArchiveMemo;
91
+ }
92
+ /**
93
+ * Carry out one measurement. Pure of caching, so the suite states every branch directly.
94
+ *
95
+ * TOTAL: it never rejects, whatever happens inside it. The thing it replaced was total by
96
+ * construction (a `try/catch` around one `execFile`), and it is consulted from a stand-up path
97
+ * documented as best-effort, so a throw here would fail a job over a probe whose whole purpose is
98
+ * to make a failure legible. A throw is also, by definition, the platform's own machinery
99
+ * breaking, which is the `unknown` disposition and never the `unusable` one.
100
+ *
101
+ * That asymmetry is the design. Only the RUN produces `unusable`; everything before it produces
102
+ * `unknown`, because everything before it is the platform's own machinery and a bug in it must be
103
+ * able to say "I could not tell" and never "your daemon is broken". The load step in particular is
104
+ * the one this repo wrote itself.
105
+ */
106
+ export declare function measureDockerWorkload(deps?: DockerWorkloadDeps, signal?: AbortSignal): Promise<DockerWorkload>;
107
+ /**
108
+ * A measurement, plus what the last one concluded without taking another.
109
+ *
110
+ * Callable because every caller wants the verdict; `last()` exists for `GET /health`, which is
111
+ * polled and must not spawn a container per poll to answer a question it does not act on.
112
+ */
113
+ export interface DockerWorkloadProbe {
114
+ (signal?: AbortSignal): Promise<DockerWorkload>;
115
+ last(): DockerWorkload | undefined;
116
+ }
117
+ /**
118
+ * Build a probe that measures at most once per container for a POSITIVE answer.
119
+ *
120
+ * A daemon that has run a container has proved something that does not stop being true, so that
121
+ * verdict is kept and every later job reads it for free. A negative is NOT kept, for the reason
122
+ * `resolveDockerVerdict` gives about the boot record: a container outlives its boot, a warm pool
123
+ * serves many jobs from one, and a daemon that was not ready for the first job must not latch
124
+ * the whole container into saying so. Re-measuring a negative is cheap; a daemon that cannot
125
+ * mount fails at once.
126
+ *
127
+ * A `usable` verdict whose EGRESS could not be determined is re-measured on the same rule and for
128
+ * the same reason. Whether the bridge is NATed is settled once and for the daemon's life, so a
129
+ * measured `reachable` or `blocked` is kept; a check that timed out measured nothing, and latching
130
+ * that would leave the container permanently unable to say which of the two it is.
131
+ *
132
+ * But only where asking again could ANSWER differently, which is what `ContainerEgress.recheck`
133
+ * carries. Most ways to reach `undetermined` are standing facts about this container: a rejected
134
+ * target setting, a payload with no `nc`, an address the deployment filters. Re-running the whole
135
+ * measurement on those never converges, and it is not cheap: it is `docker version`, the archive,
136
+ * `docker load`, two container starts and an `image rm`, per job, ahead of the clone.
137
+ *
138
+ * Concurrent callers share one in-flight measurement rather than each starting a container, and
139
+ * the measurement is cancelled when the LAST of them has abandoned it. Neither half is optional:
140
+ * one job's abort may not kill a measurement a sibling job is still waiting on (the local native
141
+ * transport serves every concurrent job from one process), and a measurement nobody is waiting
142
+ * for is a container start no job will read, which is what an abandoned run should stop paying
143
+ * for the moment it is abandoned.
144
+ */
145
+ export declare function createDockerWorkloadProbe(deps?: DockerWorkloadDeps): DockerWorkloadProbe;
146
+ /** The process-wide probe. One per container, which is what makes the positive memo worth having. */
147
+ export declare const probeDockerWorkload: DockerWorkloadProbe;
148
+ /**
149
+ * What `GET /health` reports about the workload check.
150
+ *
151
+ * `unmeasured` is its own word rather than an omitted key or a `null`: this endpoint is polled
152
+ * from boot, so "nothing has needed the daemon yet" is the normal early answer and it must not
153
+ * read as either a broken daemon or a build that cannot report one.
154
+ */
155
+ export declare function reportedDockerWorkload(probe?: DockerWorkloadProbe): DockerWorkload | {
156
+ status: 'unmeasured';
157
+ reason: string;
158
+ };