Skip to content

feat: auto-detect container runtime, promote Apple container as default - #179

Merged
capotej merged 4 commits into
mainfrom
feat/auto-detect-container-runtime-v2
Sep 2, 2026
Merged

capotej merged 4 commits into
mainfrom
feat/auto-detect-container-runtime-v2

Conversation

@capotej

@capotej capotej commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Supersedes #114 (same change, rebased onto current main + review fixes; the original branch lived in a fork whose permissions blocked the rebase push).

Summary

Promotes Apple's container CLI as the default container runtime via auto-detection. HARNESS_CONTAINER_RUNTIME=apple is no longer needed — on macOS, harness auto-detects whether container is on PATH and prefers it over docker. Set HARNESS_CONTAINER_RUNTIME=docker to force docker.

What changed

src/harness.ts

  • selectRuntime() auto-detects: on macOS (process.platform === "darwin"), prefers Apple's container CLI if on PATH via new whichSync(), falls back to docker. The darwin gate exists because apple/container ships for macOS only — a same-named binary on a Linux PATH is an unrelated tool and must not hijack selection (docker stays default everywhere else)
  • HARNESS_CONTAINER_RUNTIME=apple is kept as a deprecated alias instead of hard-failing: it still selects the apple runtime but prints a deprecation warning, so existing dotfiles keep working across the upgrade
  • HARNESS_CONTAINER_RUNTIME=auto (or unset) triggers auto-detection; docker forces docker; any other value is a hard error
  • Updated AppleContainerRuntime.ensureReady() error message

Tests

  • Rewrote runtime selection tests for auto-detection: darwin-gated preference test + its non-macOS complement (PATH-collision guard)
  • New test: =apple deprecated alias warns but still selects the apple runtime
  • Added runtimeArgsAny() helper to helpers.mjs for runtime-agnostic assertions; docker-specific tests (hardening flags, --port, repeated --env-file, home-guard mounts) pin HARNESS_CONTAINER_RUNTIME=docker explicitly so they stay deterministic on macOS dev machines
  • Apple-argv tests (tty flags, caps, cosign cache, --platform pull) pin =apple explicitly, which also exercises the deprecated alias end-to-end

Docs

  • README, AGENTS.md, USAGE help text, CHANGELOG updated
  • RFC 2026-06-20_container_runtime.md amended with a superseding note documenting the auto-detect design change

Test plan

Check Status
All 131 e2e tests pass (rebased onto main, incl. new --port and DNS tests) ✅
Build passes (pnpm build) ✅
Lint passes (biome + markdownlint) ✅
Manual smoke: unset→docker on linux, =apple warns+container, =docker, garbage value errors ✅
CI on PR ⏳

hermclaw and others added 2 commits September 2, 2026 04:11
- Remove HARNESS_CONTAINER_RUNTIME=apple; instead auto-detect whether
  Apple's container CLI is on PATH, preferring it over docker
- Add whichSync() helper for synchronous PATH lookup
- HARNESS_CONTAINER_RUNTIME=docker still supported to force docker
- Accept 'auto' as explicit no-op value (same as unset)
- Update error messages, USAGE help text, README, AGENTS.md, CHANGELOG
- Update all e2e tests to use runtime-agnostic assertions via
  runtimeArgsAny() helper; only docker-specific tests force docker
  via HARNESS_CONTAINER_RUNTIME=docker
…macOS

Review feedback on the auto-detect change:

- HARNESS_CONTAINER_RUNTIME=apple no longer hard-fails. It is now a
  deprecated explicit alias: still selects the apple runtime, prints a
  one-time deprecation warning, so existing dotfiles keep working across
  the upgrade.
- Auto-detection is gated to process.platform === "darwin". apple/container
  ships for macOS only, so a binary named `container` on a Linux PATH is an
  unrelated tool that must not hijack runtime selection; docker stays the
  default everywhere else.
- ensureReady() error message no longer assumes the auto-detected path.
- Tests: apple-argv tests pin =apple explicitly (deterministic on all
  platforms), new tests for the deprecation alias and the non-macOS
  PATH-collision guard.
- Docs: README/AGENTS.md/CHANGELOG updated; CHANGELOG #TBD replaced with
  #114; RFC amended with a superseding note.

@BoldBlackBot BoldBlackBot left a comment

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.

Review: auto-detect container runtime

The design is right — darwin-gated auto-detect, warn-not-fail =apple alias, deliberate test pinning (=docker for docker-shape assertions, runtimeArgsAny for runtime-agnostic ones), RFC amendment. Two functional items and two nits inline.

On the "standard way" question: there is none in core — no util.which or any binary-lookup API exists as of Node 26 (checked current docs; CI matrix is 22/24). npm which is real but not worth breaking the single-runtime-dep posture for ~15 lines. The standard pattern is EAFP: don't sniff PATH, just try executing the binary — which ensureReady() already does. Details inline.

Design consideration (non-blocking): an auto-detected apple runtime that half-works (version probe passes but container system start / kernel never configured) hard-fails at run, where plain docker "worked yesterday". If that bites users, one mitigation: when selection came from auto-detect (not explicit =apple), let ensureReady() failure fall back to docker with a warning instead of exit(1) — keep the hard fail for explicit selection. Note only; the RFC's trade-off is accepted.

Comment thread src/harness.ts Outdated
}

/** Synchronous `which` — returns true if the binary is found on PATH. */
function whichSync(name: string): boolean {

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.

More standard than shelling out to which: drop the PATH pre-check and reuse the probe ensureReady() already runs. execFileSync("container", ["--version"]) succeeding is strictly stronger evidence than which container — it proves the binary actually executes (broken install, quarantine attribute, wrong-arch build all fail here), and execution is the only thing selection depends on:

function appleContainerAvailable(): boolean {
  try {
    execFileSync("container", ["--version"], { stdio: "ignore", timeout: 5000 });
    return true;
  } catch {
    return false;
  }
}

Then in selectRuntime(): if (process.platform === "darwin" && appleContainerAvailable()).

This deletes whichSync entirely, including its dead win32/where branch (unreachable behind the darwin gate), plus the corner cases (2s timeout, which itself missing). Cost: the probe runs again in ensureReady() right after — either accept the ~ms double-run or hoist the result into the instance. For context: Node core has no util.which/binary-lookup API (checked Node 26 docs), and npm which isn't worth a new runtime dep here.

Comment thread tests/e2e/runtime.test.mjs Outdated
cwd: WORK_DIR,
env: {
...process.env,
PATH: `${dockerOnlyDir}:${process.env.PATH}`,

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.

This prepends dockerOnlyDir but keeps the runner's full process.env.PATH, so it doesn't actually create a docker-only PATH. On a macOS dev machine with the real container installed, auto-detect selects the real binary → no DOCKER_INVOKED line → this test fails (and it execs the real CLI against a nonexistent image tag along the way). The old version of this test filtered container-containing dirs out of PATH — that filtering is what made the premise true. Suggest restoring it, or building PATH as dockerOnlyDir + only the dirs node itself needs.

Comment thread README.md Outdated
export HARNESS_CONTAINER_RUNTIME=docker
```

`HARNESS_CONTAINER_RUNTIME=apple` (the pre-auto-detect opt-in) still works but is deprecated: it selects the apple runtime explicitly, prints a one-time deprecation warning, and will be removed in a future release.

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.

Nit: "prints a one-time deprecation warning" — it warns on every invocation; there's no state. Drop "one-time".

Comment thread rfcs/2026-06-20_container_runtime.md Outdated
entry under Key subsystems, and the architecture overview's spawn step now
reads `<runtime> run`.

## Amendment: auto-detection supersedes explicit opt-in (2026-09-02, PR #114)

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.

Nit: the amendment header cites PR #114, but #114 is the superseded fork-branch PR — this lands as #179. Reference both ("drafted in #114, landed as #179") so the archaeology auto-links to the merged PR.

- Replace the whichSync PATH pre-check with appleContainerAvailable(), which
  reuses the same `container --version` exec probe ensureReady() runs.
  Strictly stronger evidence: proves the binary executes (broken install,
  quarantine attribute, wrong-arch build all fail), which is what selection
  depends on. Deletes whichSync entirely, including its dead win32 branch.
  The probe runs once more in ensureReady() right after — accepted cost,
  bounded by the same 5s timeout.
- docker-only auto-detect test: filter container-containing dirs out of PATH
  instead of merely prepending the docker shim dir, so the test's premise
  holds on macOS dev machines with the real container CLI installed.
- README: drop 'one-time' from the =apple deprecation warning description.
- RFC amendment header: reference both #114 (draft) and #179 (landed).
@capotej

capotej commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed all four BoldBlackBot comments in 30210ef:

  1. whichSync → exec probe: replaced with appleContainerAvailable() reusing the exact container --version probe ensureReady() runs — execution is strictly stronger evidence than a PATH lookup. whichSync and its dead win32 branch are deleted. Accepted the double-run cost in the apple case (bounded by the same 5s timeout); on timeout the first probe already falls back to docker.
  2. docker-only test PATH: restored the container-dir filtering from main's original version so the premise holds on macOS machines with the real CLI installed.
  3. 'one-time' warning wording: dropped.
  4. RFC archaeology: amendment header now reads 'drafted in feat: auto-detect container runtime, promote Apple container as default #114, landed as feat: auto-detect container runtime, promote Apple container as default #179'.

Local: 131/131 e2e, biome + markdownlint clean.

selectRuntime() and AppleContainerRuntime.ensureReady() both needed the
same `container --version` evidence; appleContainerAvailable() now caches
its result in a module-level memo so the exec runs once per process.
ensureReady() consumes the memo instead of re-probing. The cache lives
only for the process lifetime — never persisted — since the CLI's install
state can change between harness invocations.
@capotej
capotej merged commit 79c5722 into main Sep 2, 2026
9 checks passed
@capotej
capotej deleted the feat/auto-detect-container-runtime-v2 branch September 2, 2026 12:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants