Skip to content
Merged
21 changes: 11 additions & 10 deletions docs/specs/2026-06-17-m3-persistent-channel-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ M3 closes all three, **harness-side only** (`pi-fork` untouched, as in M2):
2. **Env injection** — Pi's per-command `env` (passed only to the bash tool) is applied
inside the pod.
3. **find ignore-list** — find honours the `ignore` patterns (negated globs) AND `.gitignore`
for ignored **directories** (verified on the pod's ripgrep 14.1.0). Minor divergence from
Pi's `fd`: an individually-gitignored _file_ matching the positive `-g <pattern>` is
re-included by the glob whitelist — see D5 caveat below.
for ignored **directories** (verified on the pod's ripgrep 14.1.0; re-verified on 15.2.0,
which the leaf sandbox images vendor). Minor divergence from Pi's `fd`: an
individually-gitignored _file_ matching the positive `-g <pattern>` is re-included by the
glob whitelist — see D5 caveat below.

M3 is **done** when: a burst of fast ops in one agent turn is served by a single reused
kubectl process (proven on a real kind cluster); an env var set on a bash tool call is
Expand Down Expand Up @@ -252,13 +253,13 @@ glob: async (pattern, cwd, { ignore, limit }) => {
```

`rg --files` lists files under cwd honouring `.gitignore`; `--hidden` keeps dotfiles in
view; each `ignore` pattern becomes a negated glob. **Verified nuance (rg 14.1.0):**
gitignored _directories_ (e.g. `node_modules/`, `dist/`) are pruned and stay excluded even
when `-g` matches files inside; but an individually-gitignored _file_ matching the positive
`-g <pattern>` is re-included (the glob whitelist-overrides a file-level ignore) — a minor
divergence from Pi's `fd --glob`. The `ignore`-list negated globs (`-g '!<ig>'`) always
exclude their entries. Output shape (relative paths, `./` stripped, `limit`-capped) matches
M2. `exists` is unchanged.
view; each `ignore` pattern becomes a negated glob. **Verified nuance (rg 14.1.0, re-verified
on 15.2.0):** gitignored _directories_ (e.g. `node_modules/`, `dist/`) are pruned and stay
excluded even when `-g` matches files inside; but an individually-gitignored _file_ matching
the positive `-g <pattern>` is re-included (the glob whitelist-overrides a file-level ignore)
— a minor divergence from Pi's `fd --glob`. The `ignore`-list negated globs (`-g '!<ig>'`)
always exclude their entries. Output shape (relative paths, `./` stripped, `limit`-capped)
matches M2. `exists` is unchanged.

---

Expand Down
15 changes: 8 additions & 7 deletions packages/k8s-sandbox/src/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,14 @@ export function createPodFindOps(exec: ExecInPod, cfg: K8sSandboxConfig): FindOp
const q = mapper(cfg);
return {
exists: async (p) => (await exec(`test -e ${q(p)}`)).exitCode === 0,
// `rg --files --hidden` lists files under cwd, honouring .gitignore (verified
// on the pod's ripgrep 14.1.0). Nuance: gitignored DIRECTORIES (e.g. node_modules/,
// dist/) are pruned and stay excluded even though an explicit -g matches files
// inside them; but an individually-gitignored FILE matching the positive -g
// <pattern> IS re-included (the glob whitelist-overrides a file-level ignore) —
// a minor divergence from Pi's `fd --glob`. Pi's `ignore` list is applied as
// negated globs (-g '!<ig>') and always excludes its entries. --hidden keeps
// `rg --files --hidden` lists files under cwd, honouring .gitignore (verified on the
// pod's ripgrep 14.1.0, and re-verified on 15.2.0 — the version the leaf sandbox images
// vendor — where both nuances below hold unchanged). Nuance: gitignored DIRECTORIES
// (e.g. node_modules/, dist/) are pruned and stay excluded even though an explicit -g
// matches files inside them; but an individually-gitignored FILE matching the positive
// -g <pattern> IS re-included (the glob whitelist-overrides a file-level ignore) — a
// minor divergence from Pi's `fd --glob`. Pi's `ignore` list is applied as negated
// globs (-g '!<ig>') and always excludes its entries. --hidden keeps
// dotfiles in view. Paths come back relative to cwd; strip any leading "./".
glob: async (pattern, cwd, { ignore, limit }) => {
const globs = [`-g ${shQuote(pattern)}`, ...ignore.map((ig) => `-g ${shQuote(`!${ig}`)}`)];
Expand Down
91 changes: 90 additions & 1 deletion remote-worker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,65 @@ COPY gen/go/ ./gen/go/
COPY remote-worker/ ./remote-worker/
WORKDIR /src/remote-worker
RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /out/remote-worker ./cmd/worker

# ripgrep is vendored because it is the IMPLEMENTATION of two registered tools, not a scheduling
# label. The Find/Glob tool runs `rg --files --hidden` (k8s-sandbox `createPodFindOps`.glob) and the
# Grep tool runs `rg` directly (`createPodGrepTool`), and both land in THIS container whenever a
# leased gRPC presence record supplies a relay transport: extension.ts's `opts?.transport ??`
# overrides the fast AND the stream transport, so every pod operation runs over the relay. Without
# `rg` on PATH the model's Glob returns "glob failed in pod (rg exited 127)" and its Grep "rg failed
# in pod (exit 127)" -- the same attached-and-healthy-yet-every-tool-fails signature as the
# /workspace bug below. Both other sandbox images in the repo install it (sandbox.Dockerfile,
# k8s-sandbox/deploy/sandbox.yaml).
#
# It is genuinely not in the UBI 9 repositories (verified: `microdnf install ripgrep` -> "No package
# matches"), and EPEL would add a third-party repository to an image that executes model-authored
# commands. So: the upstream static musl build, pinned by version and checked against the sha256 the
# release publishes. Static, so it needs no libc from this base. Fetched in its own stage, so the
# tar/gzip needed to unpack it never reach the runtime image.
#
# The version is not free to choose. `createPodFindOps`.glob leans on two documented .gitignore
# nuances -- gitignored DIRECTORIES stay pruned even when `-g` matches inside them, while an
# individually-gitignored FILE matching a positive `-g` IS re-included -- and those were originally
# characterised on rg 14.1.0, the version alpine:3.20 ships and therefore the one both other sandbox
# images and the k8s-sandbox SMOKE.md run used. 15.2.0 crosses a major boundary, so if it diverged
# there, Glob's ignore semantics would differ by which sandbox image an operator happened to build --
# silently, and only for gitignored paths. Both nuances were re-verified against these exact pinned
# tarballs, on both arches, before pinning: they hold unchanged. operations.ts and the M3 spec's D5
# note record that.
#
# Pinning back to 14.1.0 instead is NOT available: that release publishes no
# aarch64-unknown-linux-musl asset (only -gnu), so it would either break the arm64 build or give up
# the static property this stage exists for.
#
# To bump: change RG_VERSION and BOTH digests together, AND re-check those two nuances on the new
# version. The digests are per-arch and `sha256sum -c` fails the build closed if either is wrong.
# Arch comes from `uname -m` rather than TARGETARCH because buildah / `oc new-build
# --strategy=docker` do not populate BuildKit's automatic args.
# hadolint ignore=DL3007
FROM registry.access.redhat.com/ubi9/ubi-minimal:latest AS rg
ARG RG_VERSION=15.2.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion — version pinned here is two majors ahead of the one the dependent semantics were verified on.

This vendoring exists to serve Find/Glob, but those semantics were characterised against a different ripgrep major:

  • packages/k8s-sandbox/src/operations.ts:161 — "rg --files --hidden lists files under cwd, honouring .gitignore (verified on the pod's ripgrep 14.1.0)", followed by the gitignored-DIRECTORY-vs-FILE nuance
  • docs/specs/2026-06-17-m3-persistent-channel-design.md:28 and :255 — the same nuance, recorded as a "Verified nuance (rg 14.1.0)"

15.0.0 was a major release, and after this PR the leaf is a sandbox that rg --files --hidden runs inside, so that nuance is unverified on the version actually being shipped. If it changed, Glob's ignore semantics differ by which sandbox image an operator built — silently, and only for gitignored paths.

Either pin to the version the semantics were verified on, or re-verify on 15.2.0 and update both the comment and the spec note.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-verified on 15.2.0 and updated both the comment and the spec notes — e47e70d. I took your second option, because the first turned out not to be available.

Pinning back to 14.1.0 is not possible without giving something up. That release publishes no aarch64-unknown-linux-musl asset — its only aarch64 Linux build is -gnu. So pinning to the version the semantics were characterised on would either break the arm64 path (which README-worker.md makes a real one, since these images are built for the host) or trade the static property this fetch stage exists for. That is worth recording, because "just pin to the verified version" is the obvious reading of your finding and it does not survive contact with the asset list.

So I verified the version actually being shipped, against the exact pinned tarballs rather than a convenient local build:

rg 14.1.0 (alpine:3.20) rg 15.2.0 (pinned tarball)
gitignored DIRECTORY pruned even though -g matches inside excluded excluded
individually-gitignored FILE re-included by positive -g re-included re-included

Both nuances hold unchanged. Method, since a check like this is easy to do vacuously:

  • fixture is a git repo with .gitignore covering node_modules/ and secret.txt, then rg --files --hidden -g '*.js' (directory case) and -g 'secret.txt' (file case)
  • run under ubi9/ubi-minimal with the tarball fetched and sha256sum -c verified exactly as the Dockerfile does it, on aarch64-unknown-linux-musl natively and x86_64-unknown-linux-musl emulated. Both digests matched the published .sha256.
  • the baseline was run first, on alpine:3.20's rg 14.1.0, and reproduces the documented behaviour. That ordering is the point: it shows the fixture can actually detect the nuance instead of passing for some unrelated reason. An early version of the script reported "nuance CHANGED" when the binary was simply missing, so it now refuses to report a result unless rg --version runs.

On 14.1.0 not being an arbitrary baseline — it is what alpine:3.20 ships, which is why both other sandbox images and the SMOKE.md run observed it. I left SMOKE.md alone: it is a dated result record, not a live claim.

The Dockerfiles now say the version is not free to choose and that a bump has to re-check those two nuances, not just the digests — the failure mode you named (semantics differing by which image an operator built, silently, and only for gitignored paths) is not something a digest check would catch.

ARG RG_SHA256_X86_64=33e15bcf1624b25cdd2a55813a47a2f95dbe126268203e76aa6a585d1e7b149c
ARG RG_SHA256_AARCH64=800b1e7206afe799dfb5a6901f23147cfaabe0e52210538100f61e86e1740915
# hadolint ignore=DL3041
RUN microdnf install -y --nodocs tar gzip \
&& microdnf clean all
RUN set -eux; \
case "$(uname -m)" in \
x86_64) target=x86_64-unknown-linux-musl; sha="$RG_SHA256_X86_64" ;; \
aarch64) target=aarch64-unknown-linux-musl; sha="$RG_SHA256_AARCH64" ;; \
*) echo "no pinned ripgrep build for $(uname -m)" >&2; exit 1 ;; \
esac; \
tarball="ripgrep-${RG_VERSION}-${target}.tar.gz"; \
curl -fsSL -o "/tmp/${tarball}" \
"https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${tarball}"; \
printf '%s %s\n' "${sha}" "/tmp/${tarball}" > /tmp/rg.sha256; \
sha256sum -c /tmp/rg.sha256; \
tar -xzf "/tmp/${tarball}" -C /tmp; \
install -m 755 "/tmp/ripgrep-${RG_VERSION}-${target}/rg" /usr/local/bin/rg; \
rm -rf "/tmp/${tarball}" /tmp/rg.sha256 "/tmp/ripgrep-${RG_VERSION}-${target}"; \
/usr/local/bin/rg --version

# The worker execs `bash -c <command>`, so the runtime image needs a shell and the
# coreutils the harness's ops call: base64 (writeFile), file (image mime sniffing).
# distroless/static ships none of them — every exec would fail with ENOENT.
Expand All @@ -26,10 +85,40 @@ RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /out/remote-worker ./cm
# bumps for the same reason .hadolint.yaml already ignores DL3018 for Alpine.
# hadolint ignore=DL3007
FROM registry.access.redhat.com/ubi9/ubi-minimal:latest
# git and python3 are here because cmd/worker/main.go's `probed` list advertises
# bash, rg, base64, file, python3, git
# as this worker's Hello.capabilities, and its comment says "the pool will eventually match on
# these, so they must be true". Three of the six were not: on a real VM the leaf reported
# `caps=[bash base64 file]`, so it advertised a set it could not honour. All six are now installed
# (`rg` from the stage above), and dockerfile_parity_test.go fails if that stops being true.
#
# git also matters for measurement. Spec §2.3's duty bases were derived from workloads whose git
# operations cost ~470ms; without git in the sandbox, an E8 tool call can only be a ~0ms no-op, so
# the hands tier is exercised structurally but carries no load and the measured duty cycle describes
# a cheaper workload than the basis it is compared against.
# hadolint ignore=DL3041
RUN microdnf install -y --nodocs bash coreutils-single findutils file \
RUN microdnf install -y --nodocs bash coreutils-single findutils file git python3 \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion — nothing in CI builds either of these Dockerfiles, so the package-availability judgements this PR turns on are unverified, and the drift it fixes can recur silently.

The whole diff rests on claims about what UBI 9 provides — git and python3 resolve, rg does not. Those are exactly the claims no job checks:

  • build.yaml's matrix is three images — serverless-harness (./Dockerfile), serverless-harness-sandbox (deploy/knative/sandbox.Dockerfile) and echo-target. Neither remote-worker/Dockerfile nor remote-worker/Dockerfile.runtime is in it, and the workflow only triggers on push to main and v* tags, so it would not run on this PR regardless.
  • ci.yml:170's "Build and test remote-worker" is the Go build, not an image build.
  • hadolint parses these files but does not resolve packages; Trivy scans the repo, not an image built from them.

So a package name that is absent or renamed in a future UBI 9 minor surfaces to whoever next runs setup-vm.sh's require_build by hand, not to CI. And more to the point, the specific mismatch this PR exists to fix — probed naming a tool the image does not install — has no guard, which is precisely why it survived to be found on a real VM.

The cheap guard is a parity test, and the repo already has the pattern. packages/knative-server/test/authbridge-manifests.test.ts parses deployment files and asserts invariants across them; its line 262 even enumerates the baked-in tool set in prose. The same technique applied here: parse the microdnf install line out of both Dockerfiles, parse probed out of main.go, and assert every probed tool is installed except an explicit, named exclusion list. That test fails today with rg — which is the point — and after this PR it holds the two Dockerfiles in sync with each other and with the Go list, none of which any test currently relates.

Adding the two files to build.yaml's matrix would additionally prove the packages resolve, and would publish images that setup-vm.sh currently requires an operator to build locally. Worth it independently, though it is the larger change of the two.

Keeping both Dockerfiles textually in step is a real maintenance cost now: they carry the same install line and the same three-command /workspace block, related only by a "see the same block in ./Dockerfile" comment. A test is what makes that relationship enforceable rather than aspirational.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added as a Go test — remote-worker/cmd/worker/dockerfile_parity_test.go in 86bf033.

I went with Go rather than the vitest parse you suggested, for one reason that I think makes it strictly better: in-package it can reference probed directly instead of regex-matching main.go. The list the test checks is therefore the same variable the worker advertises, so those two cannot drift — which removes one of the two drift surfaces rather than adding a parser for it. It also lands in the go test -race ./... step ci.yml:170 already runs, so it needs no new job, no daemon and no network.

Three properties, and I negative-tested each rather than trusting a green run:

Property Negative test Result
every probed tool is installed by both files checked out the pre-fix tree exit 1, naming rg, git, python3
the two Dockerfiles install the same set dropped git from Dockerfile.runtime only exit 1, install different package sets
the vendored binary stays pinned and verified removed the sha256sum step exit 1

All three pass on the branch. Your framing was the useful part here — a test that could not fail would have been worse than none, and verify-sandbox-inventory.sh documents that exact trap in its own comments, so the regex asserts a match exists and fails loudly if the pattern goes stale rather than passing vacuously.

Two details worth calling out because they are where this kind of test usually rots:

  • provides maps tool → what puts it on PATH, not tool → itself. base64 comes from coreutils-single and rg from the tarball, so a test grepping for the tool's own name would have passed on the broken image. That indirection is the whole content of the test.
  • knownGap exists but is empty. With rg vendored there is no gap left; keeping the mechanism means a future omission has to be written down in one place, and an unmapped tool errors rather than being silently skipped.

On the two larger items you raised: I did not add these files to build.yaml's matrix. Static analysis cannot prove a package still resolves in a future UBI 9 minor — only building can — so the gap you identified is narrowed, not closed. I did build both files on both architectures by hand for this change (caps=[bash rg base64 file python3 git], rg running in-image), but you are right that nothing repeats that automatically. The reason I left it out is that build.yaml only fires on push to main, so it would not have guarded this PR either; the honest fix is a PR-time build, and publishing these images has consumer implications that deserve their own PR rather than riding along here. verify-sandbox-inventory.sh is the pattern to follow when it happens, and the test comment says so.

&& microdnf clean all
COPY --from=build /out/remote-worker /usr/local/bin/remote-worker
COPY --from=rg /usr/local/bin/rg /usr/local/bin/rg
# The harness execs every tool call from its sandbox working directory, which defaults to
# /workspace (KAGENTI_SANDBOX_CWD, select-sandbox.ts). Without this directory the worker's
# `bash -c` fails before running anything the caller asked for:
# bash: line 1: cd: /workspace: No such file or directory
# Command exited with code 1
# and the harness reports a tool error for a sandbox that is otherwise healthy and attached --
# observed on a real VM run, where the exec reached the leaf correctly and died on the cd. Owned by
# 1001:0 because that is the uid this image runs as (gid 0, matching the nonroot-v2 SCC below), so
# the agent can actually write in its own workspace. One `install -d` states owner, group and mode in
# one place, as build-swebench-sandbox.sh's generated Dockerfile already does.
#
# WORKDIR because BashRunner's "every command the harness sends is self-contained (`cd 'cwd' &&
# ...`)" is today the only thing making the process cwd irrelevant. Any exec path that ever sends a
# bare command -- or an operator running `podman exec` to debug a sandbox -- would otherwise land in
# /, which is a second round of the bug documented directly above. Both sandbox images that came
# before this one set it.
RUN install -d -o 1001 -g 0 -m 775 /workspace
WORKDIR /workspace
# Matches the nonroot-v2 SCC deploy-incluster.sh applies.
USER 1001
ENTRYPOINT ["/usr/local/bin/remote-worker"]
77 changes: 76 additions & 1 deletion remote-worker/Dockerfile.runtime
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,92 @@
# (produced by build-image.sh). No Go toolchain or module fetch happens here, so
# the in-cluster OpenShift build needs no egress to module proxies.
#
# It DOES now need egress to github.com, for the pinned ripgrep tarball the `rg` stage below
# fetches. That is a new requirement on the in-cluster build path and would fail in a disconnected
# cluster; the alternative was leaving the agent's Grep and Find tools broken in every sandbox built
# from this file. If a disconnected build is ever needed, have build-image.sh place `rg` in dist/
# alongside the worker binary and COPY it, the way the binary itself already arrives.
#
# The base must provide bash + coreutils: the worker runs `bash -c <command>` and
# the harness's ops call base64 and file (spec §3.3).
#
# ripgrep is vendored because it is the IMPLEMENTATION of two registered tools, not a scheduling
# label. The Find/Glob tool runs `rg --files --hidden` (k8s-sandbox `createPodFindOps`.glob) and the
# Grep tool runs `rg` directly (`createPodGrepTool`), and both land in THIS container whenever a
# leased gRPC presence record supplies a relay transport: extension.ts's `opts?.transport ??`
# overrides the fast AND the stream transport, so every pod operation runs over the relay. Without
# `rg` on PATH the model's Glob returns "glob failed in pod (rg exited 127)" and its Grep "rg failed
# in pod (exit 127)" -- the same attached-and-healthy-yet-every-tool-fails signature as the
# /workspace bug below. Both other sandbox images in the repo install it (sandbox.Dockerfile,
# k8s-sandbox/deploy/sandbox.yaml).
#
# It is genuinely not in the UBI 9 repositories (verified: `microdnf install ripgrep` -> "No package
# matches"), and EPEL would add a third-party repository to an image that executes model-authored
# commands. So: the upstream static musl build, pinned by version and checked against the sha256 the
# release publishes. Static, so it needs no libc from this base. Fetched in its own stage, so the
# tar/gzip needed to unpack it never reach the runtime image.
#
# The version is not free to choose. `createPodFindOps`.glob leans on two documented .gitignore
# nuances -- gitignored DIRECTORIES stay pruned even when `-g` matches inside them, while an
# individually-gitignored FILE matching a positive `-g` IS re-included -- and those were originally
# characterised on rg 14.1.0, the version alpine:3.20 ships and therefore the one both other sandbox
# images and the k8s-sandbox SMOKE.md run used. 15.2.0 crosses a major boundary, so if it diverged
# there, Glob's ignore semantics would differ by which sandbox image an operator happened to build --
# silently, and only for gitignored paths. Both nuances were re-verified against these exact pinned
# tarballs, on both arches, before pinning: they hold unchanged. operations.ts and the M3 spec's D5
# note record that.
#
# Pinning back to 14.1.0 instead is NOT available: that release publishes no
# aarch64-unknown-linux-musl asset (only -gnu), so it would either break the arm64 build or give up
# the static property this stage exists for.
#
# To bump: change RG_VERSION and BOTH digests together, AND re-check those two nuances on the new
# version. The digests are per-arch and `sha256sum -c` fails the build closed if either is wrong.
# Arch comes from `uname -m` rather than TARGETARCH because buildah / `oc new-build
# --strategy=docker` do not populate BuildKit's automatic args.
# hadolint ignore=DL3007
FROM registry.access.redhat.com/ubi9/ubi-minimal:latest AS rg
ARG RG_VERSION=15.2.0
ARG RG_SHA256_X86_64=33e15bcf1624b25cdd2a55813a47a2f95dbe126268203e76aa6a585d1e7b149c
ARG RG_SHA256_AARCH64=800b1e7206afe799dfb5a6901f23147cfaabe0e52210538100f61e86e1740915
# hadolint ignore=DL3041
RUN microdnf install -y --nodocs tar gzip \
&& microdnf clean all
RUN set -eux; \
case "$(uname -m)" in \
x86_64) target=x86_64-unknown-linux-musl; sha="$RG_SHA256_X86_64" ;; \
aarch64) target=aarch64-unknown-linux-musl; sha="$RG_SHA256_AARCH64" ;; \
*) echo "no pinned ripgrep build for $(uname -m)" >&2; exit 1 ;; \
esac; \
tarball="ripgrep-${RG_VERSION}-${target}.tar.gz"; \
curl -fsSL -o "/tmp/${tarball}" \
"https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${tarball}"; \
printf '%s %s\n' "${sha}" "/tmp/${tarball}" > /tmp/rg.sha256; \
sha256sum -c /tmp/rg.sha256; \
tar -xzf "/tmp/${tarball}" -C /tmp; \
install -m 755 "/tmp/ripgrep-${RG_VERSION}-${target}/rg" /usr/local/bin/rg; \
rm -rf "/tmp/${tarball}" /tmp/rg.sha256 "/tmp/ripgrep-${RG_VERSION}-${target}"; \
/usr/local/bin/rg --version

# This is a standalone demo image that tracks the current UBI 9 minor; a pinned
# digest would go stale silently, and microdnf package pinning breaks on minor
# bumps for the same reason .hadolint.yaml already ignores DL3018 for Alpine.
# hadolint ignore=DL3007
FROM registry.access.redhat.com/ubi9/ubi-minimal:latest
# git and python3: see the same block in ./Dockerfile. cmd/worker/main.go's `probed` list advertises
# them as this worker's capabilities and they were not present, and a sandbox without git cannot run
# a tool call resembling the workloads spec §2.3's duty bases were measured from. With `rg` from the
# stage above, all six probed tools now resolve; dockerfile_parity_test.go fails if that regresses.
# hadolint ignore=DL3041
RUN microdnf install -y --nodocs bash coreutils-single findutils file \
RUN microdnf install -y --nodocs bash coreutils-single findutils file git python3 \
&& microdnf clean all
COPY remote-worker /usr/local/bin/remote-worker
COPY --from=rg /usr/local/bin/rg /usr/local/bin/rg
# See the same block in ./Dockerfile: the harness execs tool calls from /workspace (the default
# KAGENTI_SANDBOX_CWD), and without it every exec dies on `cd` before running the command, which
# surfaces as a tool error from a sandbox that is attached and healthy. WORKDIR for the same reason
# as there -- so a bare command, or an operator's `podman exec`, does not land in /.
RUN install -d -o 1001 -g 0 -m 775 /workspace
WORKDIR /workspace
USER 1001
ENTRYPOINT ["/usr/local/bin/remote-worker"]
Loading
Loading