Skip to content

feat(apple-container): Apple Container runtime + remote OneCLI gateway#2809

Open
hidenwalker wants to merge 2 commits into
nanocoai:mainfrom
hidenwalker:feat/apple-container-onecli
Open

feat(apple-container): Apple Container runtime + remote OneCLI gateway#2809
hidenwalker wants to merge 2 commits into
nanocoai:mainfrom
hidenwalker:feat/apple-container-onecli

Conversation

@hidenwalker

Copy link
Copy Markdown

What

Adds an env-gated CONTAINER_RUNTIME (docker default | container = Apple Container, macOS) and first-class support for pointing NanoClaw at a remote OneCLI gateway.

CONTAINER_RUNTIME=docker is the default and is byte-identical to today's behavior — existing installs are unaffected (the new code paths are all gated behind IS_APPLE_CONTAINER / a non-loopback ONECLI_URL).

Apple Container (macOS)

  • container-runtime.ts: runtime-binary switch, bridge100 host-gateway autodetect (192.168.64.0/24), ls --format json orphan cleanup scoped by install label, system + builder start.
  • No --add-host under Apple Container, so the host.docker.internal literal OneCLI bakes into its injected proxy env is rewritten to the bridge gateway IP (value-driven, on -e values only; mount args untouched). The gateway is resolved lazily at spawn time, after the bridge is up.
  • onecli-forwarder.ts: host-side TCP relay (bridge IP:10255 → 127.0.0.1) so the VM can reach a local OneCLI gateway in Docker Desktop. Binds the host-only bridge IP only — never 0.0.0.0 — rejects off-bridge clients, and refuses to start if the unauthenticated control API is reachable off-loopback.
  • index.ts: asserts the install lives under $HOME and pins TMPDIR there (Apple Container only shares mount sources under HOME — fail fast instead of spawning with silently-missing mounts).
  • Egress lockdown is hard-gated off (Apple Container has no Docker bridge network).

Remote OneCLI gateway (works under either runtime)

  • A non-loopback ONECLI_URL skips the local forwarder; the agent reaches the remote gateway over the LAN.
  • The proxy host OneCLI injects (the literal host.docker.internal, which assumes a Docker-Desktop-local agent) is retargeted to the remote gateway's host, derived automatically from ONECLI_URL — so remote OneCLI usually needs only ONECLI_URL set.
  • ONECLI_GATEWAY_REWRITE="from=to" is an explicit override for when the gateway is reachable at a different address than the control API host (e.g. a Tailscale IP); validated eagerly so a malformed value fails fast.

setup/service.ts emits CONTAINER_RUNTIME into the launchd/systemd service env so the runtime flip survives a service reload. .env.example documents every knob and the security model (the proxy port carries vault credentials — keep it off the open LAN).

Why

Lets a macOS host run agent containers on Apple's lightweight container runtime, and — paired with a remote OneCLI gateway on another host — drop Docker Desktop entirely for a meaningful RAM saving, while keeping the default Docker path unchanged.

Testing

  • New src/apple-container.test.ts: host-gateway fallback (regression guard for the empty-string env default), bridge-subnet guard, and remote-host derivation including the bracketed IPv6 loopback [::1] case.
  • Full host test suite green; tsc clean; Prettier clean.
  • Verified end-to-end on a Mac mini: Apple Container runtime + a remote OneCLI gateway on another LAN host — agent containers spawned, vault credentials injected via the remote gateway, real Claude responses received, with Docker Desktop fully down.

🤖 Generated with Claude Code

Env-gated CONTAINER_RUNTIME=container selects Apple's `container` CLI
instead of Docker. Defaults to docker, with a byte-identical service env,
so existing installs are unaffected.

Apple Container (macOS):
- container-runtime.ts: runtime-binary switch, bridge100 host-gateway
  autodetect (192.168.64.0/24), `ls --format json` orphan cleanup scoped
  by install label, system + builder start.
- No --add-host under Apple Container, so rewrite the host.docker.internal
  literal that OneCLI bakes into its injected proxy env to the bridge
  gateway IP (value-driven; -v mount args untouched).
- onecli-forwarder.ts: host-side TCP relay (bridge IP:10255 -> 127.0.0.1)
  so the VM can reach a LOCAL OneCLI gateway running in Docker Desktop.
  Binds the host-only bridge IP only, never 0.0.0.0; refuses to start if
  the unauthenticated control API is reachable off-loopback.
- index.ts: assert the install lives under HOME and pin TMPDIR under HOME
  (Apple Container only shares mount sources under HOME; fail fast instead
  of spawning with silently-missing mounts).
- egress-lockdown hard-gated off (Apple Container has no Docker bridge net).

Remote OneCLI gateway (works under either runtime):
- A non-loopback ONECLI_URL skips the local forwarder; the agent reaches
  the remote gateway directly over the LAN.
- ONECLI_GATEWAY_REWRITE="from=to" rewrites the gateway host OneCLI bakes
  into the proxy env (its own container-bridge IP) to a LAN-reachable
  address, so a remote agent can reach the proxy.

setup/service.ts emits CONTAINER_RUNTIME into the launchd/systemd env so
the runtime flip survives a service reload. .env.example documents every
knob, including the requirement that Docker Desktop stay running to host a
local OneCLI gateway.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mksocial19-code

Copy link
Copy Markdown

Triage verdict: defer until one blocker is fixed, then this is still high-risk and should get maintainer macOS/Apple Container E2E before merge.

Verified on a fresh clone merged onto current main:

  • merge: clean
  • pnpm test: 58 files / 528 tests pass
  • pnpm typecheck: pass
  • pnpm format:check: pass
  • pnpm lint: fails, but only with existing baseline errors; this PR adds 2 warnings in changed files, not new errors

Blocker:

  • NANOCLAW_HOST_GATEWAY_IP is documented as an override, but detectHostGateway() returns the detected bridge100/bridge0 address before the override:
    return addr || NANOCLAW_HOST_GATEWAY_IP || '192.168.64.1';
    This makes the override ineffective whenever a bridge address exists. Please swap precedence to NANOCLAW_HOST_GATEWAY_IP || addr || ... and add a unit test for override-wins.

Risk notes after that fix:

  • This changes core container spawn/runtime, service env, egress behavior, and OneCLI proxy routing. Unit coverage is good, but I would not merge without a real macOS Apple Container smoke using local and remote OneCLI modes.
  • I did not open a smaller replacement PR because the safe fix is inside this PR's new runtime abstraction and does not stand alone against main.

… autodetect

detectHostGateway() returned `addr || NANOCLAW_HOST_GATEWAY_IP || .1`, so the

documented override was a no-op whenever a bridge100 address existed. Swap to

`NANOCLAW_HOST_GATEWAY_IP || addr || .1` so an explicit override wins, and add a

regression test that mocks config + re-imports the runtime to exercise that path.

Reported by mksocial19-code on PR nanocoai#2809 (blocker).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_014uQUtemkjFQGeg3BEhDFiB
@hidenwalker

Copy link
Copy Markdown
Author

Thanks for the careful triage, @mksocial19-code — the blocker is a real bug and you're right about the precedence.

Fixed in d950a51:

  • detectHostGateway() now returns NANOCLAW_HOST_GATEWAY_IP || addr || '192.168.64.1', so an explicit override wins over a detected bridge address (the previous order made the documented override a no-op whenever bridge100 was up).
  • Added a regression test (override-wins) that mocks config and re-imports the runtime module to exercise the override path, since NANOCLAW_HOST_GATEWAY_IP is frozen at import time. src/apple-container.test.ts now has 13 tests.
  • Updated the function's doc comment to state the precedence explicitly.

Full suite green on a clean clone: 558/558 tests, typecheck pass.

On the risk note: agreed this touches core spawn/runtime and warrants a maintainer macOS Apple Container E2E before merge. For what it's worth, this exact branch has been running live on a Mac mini (Apple Container + a remote OneCLI gateway over LAN) since submission — local and remote OneCLI modes both exercised end-to-end. Happy to provide repro steps or logs if that helps a maintainer reproduce.

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.

2 participants