@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 +106 -8
- package/dist/agent.js +8 -2
- package/dist/docker-capability.d.ts +158 -0
- package/dist/docker-capability.js +510 -0
- package/dist/docker-command.d.ts +30 -0
- package/dist/docker-command.js +91 -0
- package/dist/docker-probe-image.d.ts +97 -0
- package/dist/docker-probe-image.js +283 -0
- package/dist/docker-status.d.ts +99 -19
- package/dist/docker-status.js +93 -36
- package/dist/environment-inventory.d.ts +57 -6
- package/dist/environment-inventory.js +149 -17
- package/dist/harness-server.js +7 -1
- package/dist/infra-standup.d.ts +15 -12
- package/dist/infra-standup.js +73 -23
- package/dist/job.d.ts +20 -0
- package/dist/redact.d.ts +15 -0
- package/dist/redact.js +18 -0
- package/package.json +5 -5
- package/src/agent.ts +8 -2
- package/src/docker-capability.ts +767 -0
- package/src/docker-command.ts +117 -0
- package/src/docker-probe-image.ts +328 -0
- package/src/docker-status.ts +148 -38
- package/src/environment-inventory.ts +216 -30
- package/src/harness-server.ts +7 -1
- package/src/infra-standup.ts +77 -23
- package/src/job.ts +20 -0
- package/src/redact.ts +19 -0
package/src/infra-standup.ts
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
import { execFile } from 'node:child_process'
|
|
7
7
|
import { promisify } from 'node:util'
|
|
8
8
|
import type { AgentInfraSpec, InfraSetupRecord, ServiceInfraSpec } from './job.js'
|
|
9
|
+
import type { DockerWorkload } from './docker-capability.js'
|
|
9
10
|
import {
|
|
10
11
|
type DockerProbe,
|
|
11
|
-
|
|
12
|
+
probeLiveDockerCapability,
|
|
12
13
|
readDockerStatus,
|
|
13
14
|
resolveDockerVerdict,
|
|
14
15
|
} from './docker-status.js'
|
|
@@ -26,19 +27,22 @@ const exec = promisify(execFile)
|
|
|
26
27
|
* still run unit-level tests and report what it could. A no-op for ephemeral / no-infra /
|
|
27
28
|
* no-compose-path runs.
|
|
28
29
|
*
|
|
29
|
-
* A CONFIRMED absence of a Docker daemon short-circuits it: the container
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* looking like a Tester that simply chose not to. Anything OTHER than a confirmed
|
|
35
|
-
* attempts as before (`DockerStatus.available` in docker-status.ts states why
|
|
36
|
-
* own value).
|
|
30
|
+
* A CONFIRMED absence of a USABLE Docker daemon short-circuits it: the container already knows
|
|
31
|
+
* compose cannot work, so running it would only turn a fact this container holds into an error
|
|
32
|
+
* the agent has to interpret. The record then carries the stated cause plus the two facts that
|
|
33
|
+
* decide where a human should look (`dockerAvailable`: was anything answering, `dockerWorkload`:
|
|
34
|
+
* what a container did on it), which is what makes the Tester step say why it ran no infra
|
|
35
|
+
* instead of looking like a Tester that simply chose not to. Anything OTHER than a confirmed
|
|
36
|
+
* negative attempts as before (`DockerStatus.available` in docker-status.ts states why
|
|
37
|
+
* "undecided" is its own value).
|
|
37
38
|
*
|
|
38
|
-
* "Confirmed", not merely recorded: {@link resolveDockerVerdict} re-checks
|
|
39
|
-
* against a live daemon first, so a warm-pool container whose sidecar came up late is not
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* "Confirmed", not merely recorded: {@link resolveDockerVerdict} re-checks the boot record
|
|
40
|
+
* against a live daemon first, so a warm-pool container whose sidecar came up late is not latched
|
|
41
|
+
* into refusing infra that works. It re-checks a recorded PRESENCE too, by running an actual
|
|
42
|
+
* container: a rootless daemon in a sandbox answers `docker version` while being unable to mount
|
|
43
|
+
* an image, and compose against that one died on a mount error inside the very mechanism that
|
|
44
|
+
* exists to explain why infra did not come up. `probe` is that check, injected so the unit suite
|
|
45
|
+
* can state every answer on a machine that has its own daemon either way.
|
|
42
46
|
*
|
|
43
47
|
* Whether it succeeds or fails, the (redacted, bounded) command output is captured into a
|
|
44
48
|
* {@link InfraSetupRecord} returned alongside the prompt `note`, so the backend can surface
|
|
@@ -54,27 +58,46 @@ export async function standUpInfra(
|
|
|
54
58
|
infra: ServiceInfraSpec,
|
|
55
59
|
signal: AbortSignal | undefined,
|
|
56
60
|
logger: Logger,
|
|
57
|
-
probe: DockerProbe =
|
|
61
|
+
probe: DockerProbe = probeLiveDockerCapability,
|
|
58
62
|
): Promise<{ started: boolean; note?: string; record?: InfraSetupRecord }> {
|
|
59
63
|
if (infra.environment !== 'local' || infra.noInfraDependencies || !infra.composePath) {
|
|
60
64
|
return { started: false }
|
|
61
65
|
}
|
|
62
66
|
const startedAt = Date.now()
|
|
63
67
|
const recorded = await readDockerStatus()
|
|
64
|
-
const docker = await resolveDockerVerdict(recorded,
|
|
68
|
+
const docker = await resolveDockerVerdict(recorded, {
|
|
69
|
+
probe,
|
|
70
|
+
...(signal ? { signal } : {}),
|
|
71
|
+
logger,
|
|
72
|
+
})
|
|
65
73
|
if (docker.refusal) {
|
|
66
74
|
const note = `the dependencies could not be started: ${docker.refusal}`
|
|
67
|
-
logger.warn('agent(explore): infra stand-up refused, no docker daemon', {
|
|
75
|
+
logger.warn('agent(explore): infra stand-up refused, no usable docker daemon', {
|
|
68
76
|
composePath: infra.composePath,
|
|
69
77
|
dockerSource: recorded.source,
|
|
70
78
|
dockerReason: recorded.reason,
|
|
79
|
+
// What the LIVE check found, which is the only place the second refusal cause exists: the
|
|
80
|
+
// boot record's own words for a daemon that answers and cannot run anything are `serving`
|
|
81
|
+
// and nothing else, so a log line carrying the record alone describes the wrong failure.
|
|
82
|
+
dockerWorkload: docker.workload?.status ?? 'unmeasured',
|
|
83
|
+
...(docker.workload?.status === 'usable'
|
|
84
|
+
? { dockerEgress: docker.workload.egress.status }
|
|
85
|
+
: {}),
|
|
86
|
+
...(docker.workload?.status === 'unusable' ? { dockerDetail: docker.workload.detail } : {}),
|
|
71
87
|
})
|
|
72
88
|
return {
|
|
73
89
|
started: false,
|
|
74
90
|
note,
|
|
75
91
|
record: {
|
|
76
92
|
started: false,
|
|
77
|
-
|
|
93
|
+
// NOT a flat `false`. Two refusals reach this branch and they have opposite fixes: with
|
|
94
|
+
// nothing answering, the executor image or the sandbox running it is what to go and look
|
|
95
|
+
// at; with a daemon that answers and cannot run a container, that daemon is up and an
|
|
96
|
+
// operator sent to restart it finds nothing wrong. `dockerAvailable` answers only the
|
|
97
|
+
// first question and `dockerWorkload` the second, so neither has to carry the other's
|
|
98
|
+
// fact (the same rule the compose-failure branch below states for its own `false`).
|
|
99
|
+
dockerAvailable: docker.daemon === true,
|
|
100
|
+
...workloadRecord(docker.workload),
|
|
78
101
|
composePath: infra.composePath,
|
|
79
102
|
at: Date.now(),
|
|
80
103
|
durationMs: Date.now() - startedAt,
|
|
@@ -97,6 +120,7 @@ export async function standUpInfra(
|
|
|
97
120
|
record: {
|
|
98
121
|
started: true,
|
|
99
122
|
dockerAvailable: true,
|
|
123
|
+
...workloadRecord(docker.workload),
|
|
100
124
|
composePath: infra.composePath,
|
|
101
125
|
at: Date.now(),
|
|
102
126
|
durationMs: Date.now() - startedAt,
|
|
@@ -116,12 +140,13 @@ export async function standUpInfra(
|
|
|
116
140
|
note,
|
|
117
141
|
record: {
|
|
118
142
|
started: false,
|
|
119
|
-
// A compose failure with a REACHABLE daemon
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
...(docker.
|
|
143
|
+
// A compose failure with a REACHABLE daemon, which is a third diagnosis again: the stack
|
|
144
|
+
// itself did not come up. Read off the RESOLVED verdict, so a container whose daemon came
|
|
145
|
+
// up after boot claims the daemon it actually reached rather than the one its boot record
|
|
146
|
+
// still denies, and OMITTED rather than `false` when nothing answered the live check,
|
|
147
|
+
// because the boot record's word for that is a hypothesis and not a measurement.
|
|
148
|
+
...(docker.daemon === true ? { dockerAvailable: true } : {}),
|
|
149
|
+
...workloadRecord(docker.workload),
|
|
125
150
|
composePath: infra.composePath,
|
|
126
151
|
at: Date.now(),
|
|
127
152
|
durationMs: Date.now() - startedAt,
|
|
@@ -132,6 +157,35 @@ export async function standUpInfra(
|
|
|
132
157
|
}
|
|
133
158
|
}
|
|
134
159
|
|
|
160
|
+
/**
|
|
161
|
+
* What the live check measured, for the record the Tester step shows.
|
|
162
|
+
*
|
|
163
|
+
* Its own field beside `dockerAvailable` because the two answer different questions and only one
|
|
164
|
+
* of them has a boolean's worth of answers: a daemon either answered or it did not, while what a
|
|
165
|
+
* container DID on it is `usable`, `unusable`, or a check that could not be carried out. Absent
|
|
166
|
+
* when nothing was measured at all (an undecided boot record probes nothing), which is not the
|
|
167
|
+
* same as a check that ran and could not tell.
|
|
168
|
+
*
|
|
169
|
+
* THREE fields rather than two, because `usable` is not one fact. A daemon running with
|
|
170
|
+
* `--iptables=false` runs containers perfectly and gives them no network, so it is `usable` and
|
|
171
|
+
* every `docker build` that fetches anything on it is guaranteed to fail. The agent's prompt and
|
|
172
|
+
* `GET /health` both learn that; without `dockerEgress` the record a human reads on the Tester
|
|
173
|
+
* step, and every backend consumer of it, sees the same undifferentiated `usable` as a sandbox
|
|
174
|
+
* where the stack actually works. Present only on `usable`, which is the one verdict that has an
|
|
175
|
+
* egress half at all: on any other, a word here would report "no measurement" and "measured, and
|
|
176
|
+
* it cannot get out" in the same field.
|
|
177
|
+
*/
|
|
178
|
+
function workloadRecord(workload: DockerWorkload | undefined): {
|
|
179
|
+
dockerWorkload?: InfraSetupRecord['dockerWorkload']
|
|
180
|
+
dockerEgress?: InfraSetupRecord['dockerEgress']
|
|
181
|
+
} {
|
|
182
|
+
if (!workload) return {}
|
|
183
|
+
if (workload.status === 'usable') {
|
|
184
|
+
return { dockerWorkload: 'usable', dockerEgress: workload.egress.status }
|
|
185
|
+
}
|
|
186
|
+
return { dockerWorkload: workload.status === 'unknown' ? 'undetermined' : workload.status }
|
|
187
|
+
}
|
|
188
|
+
|
|
135
189
|
/**
|
|
136
190
|
* Stand the run's infra up and return a single cleanup handle, dispatching on the spec's
|
|
137
191
|
* `kind`: the frontend UI-test flow (`kind: 'frontend'`) builds/serves the app + WireMock as
|
package/src/job.ts
CHANGED
|
@@ -955,6 +955,26 @@ export interface InfraSetupRecord {
|
|
|
955
955
|
* mistake that let a daemon-less image read as an ordinary infra failure for months.
|
|
956
956
|
*/
|
|
957
957
|
dockerAvailable?: boolean
|
|
958
|
+
/**
|
|
959
|
+
* What a real container DID on that daemon, when the platform measured it.
|
|
960
|
+
*
|
|
961
|
+
* The third diagnosis, and the one `dockerAvailable` structurally cannot carry: a rootless
|
|
962
|
+
* daemon nested in a sandbox answers throughout while unable to mount any image layer, so it is
|
|
963
|
+
* `dockerAvailable: true` and no stack can come up on it (issue #2120). Reporting that as an
|
|
964
|
+
* absent daemon sends a human to restart one that is already up. `undetermined` is a check that
|
|
965
|
+
* ran and could not tell; ABSENT means nothing was measured at all.
|
|
966
|
+
*/
|
|
967
|
+
dockerWorkload?: 'usable' | 'unusable' | 'undetermined'
|
|
968
|
+
/**
|
|
969
|
+
* What a container started ON that daemon could REACH, when the platform measured it.
|
|
970
|
+
*
|
|
971
|
+
* The fourth diagnosis, and the one `dockerWorkload: 'usable'` structurally cannot carry: a
|
|
972
|
+
* rootless daemon started with `--iptables=false` installs no MASQUERADE rule for its bridge,
|
|
973
|
+
* so it runs containers perfectly and none of them has a route out. The stack comes up and
|
|
974
|
+
* every `docker build` that fetches a dependency fails, slowly. Present only alongside
|
|
975
|
+
* `usable`, which is the one verdict with an egress half; absent means nothing measured it.
|
|
976
|
+
*/
|
|
977
|
+
dockerEgress?: 'reachable' | 'blocked' | 'undetermined'
|
|
958
978
|
/** The repo-relative compose file that was stood up. */
|
|
959
979
|
composePath?: string
|
|
960
980
|
/** Epoch ms the stand-up attempt finished. */
|
package/src/redact.ts
CHANGED
|
@@ -72,6 +72,25 @@ export function redactSecrets(input: string): string {
|
|
|
72
72
|
return redact(input)
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* A scrubbed, length-bounded excerpt of a string that is about to be QUOTED at a human or a
|
|
77
|
+
* model: a failing command's output, a rejected setting, a thrown message.
|
|
78
|
+
*
|
|
79
|
+
* One helper rather than a private `bounded()` per module, which is what this replaced. Two of
|
|
80
|
+
* them had drifted: `docker-probe-image.ts` echoed a rejected `HARNESS_DOCKER_EGRESS_TARGET`
|
|
81
|
+
* verbatim into a string that reaches every agent's system prompt and `GET /health`, while its
|
|
82
|
+
* same-named neighbour in `docker-capability.ts` scrubbed first. A proxy URL with an embedded
|
|
83
|
+
* token in that setting is the ordinary way that becomes a leak, and two helpers with one name
|
|
84
|
+
* in sibling files is what kept the divergence invisible.
|
|
85
|
+
*
|
|
86
|
+
* Scrub BEFORE bounding, so the cut cannot land inside a credential and leave half of it
|
|
87
|
+
* quotable, and mark a trimmed value so a reader never takes the head for the whole.
|
|
88
|
+
*/
|
|
89
|
+
export function scrubbedExcerpt(text: string, maxChars: number): string {
|
|
90
|
+
const scrubbed = redactSecrets(text)
|
|
91
|
+
return scrubbed.length > maxChars ? `${scrubbed.slice(0, maxChars)}…` : scrubbed
|
|
92
|
+
}
|
|
93
|
+
|
|
75
94
|
/** Cap on captured command output kept on an infra record (tail-biased — failures show last). */
|
|
76
95
|
export const MAX_CAPTURED_OUTPUT_CHARS = 16_000
|
|
77
96
|
|