Skip to content

fix(docker): self-dropping netclaw CLI launcher so root exec works (0.23.0-beta.3)#1322

Merged
Aaronontheweb merged 2 commits into
devfrom
fix/docker-nonroot-cli-launcher
Jun 4, 2026
Merged

fix(docker): self-dropping netclaw CLI launcher so root exec works (0.23.0-beta.3)#1322
Aaronontheweb merged 2 commits into
devfrom
fix/docker-nonroot-cli-launcher

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

Summary

Makes the non-root container model usable for the operator CLI, and locks it in with a standalone-docker regression test. Bumps to 0.23.0-beta.3.

Problem

The daemon runs as the unprivileged netclaw user (good — it executes model-chosen shell commands). But docker exec/kubectl exec default to the image's USER root, and netclaw init — the documented first-run setup — is invoked exactly that way.

A root-context netclaw invocation breaks the agent two ways:

  1. Bundle extraction (EACCES). The CLI is a .NET single-file binary; it self-extracts into $HOME/.net/netclaw/<hash>/, which the runtime locks to the invoking user at mode 700. Extracted by root, the non-root daemon can no longer extract its own CLI:
    Failure processing application bundle.
    Failed to create directory [/home/netclaw/.net/netclaw/<hash>] ... Error code: 13
    
    (EACCES, exit 160).
  2. Root-owned config. netclaw init writes identity/config/secrets under NETCLAW_HOME as root; the non-root daemon can't read them.

Seen in production: operators inspecting/setting model assignments via kubectl exec left the agents unable to run their own CLI.

Fix

/usr/local/bin/netclaw becomes a self-dropping launcher (docker/netclaw-cli-launcher.sh): if invoked as root it re-execs as netclaw via gosu; if already netclaw it execs directly (so it composes with runAsUser: 1654 / docker exec -u netclaw). netclawd stays a plain symlink — it's only ever launched by the already-dropped entrypoint, so it never hits the root path.

Rationale and alternatives (why not USER netclaw, runAsUser, DOTNET_BUNDLE_EXTRACT_BASE_DIR, or docs-only) are in ADR-004.

Regression test

scripts/docker/test-nonroot-cli.sh (wired into validate_docker_image.yml) reproduces the original break with standalone docker and asserts:

  • root docker exec -- netclaw --version exits 0, prints a version (no EACCES signature), and emits the launcher's drop breadcrumb;
  • nothing under /home/netclaw/.net or /home/netclaw/.netclaw is owned by uid 0 after root-context CLI runs (the guard — without the launcher the extraction dir is root:root and this fails);
  • the non-root path (docker exec -u netclaw) execs directly with no second drop;
  • the daemon stays healthy throughout.

Changes

  • docker/netclaw-cli-launcher.sh (new) + docker/Dockerfile (CLI → launcher; netclawd symlink unchanged)
  • scripts/docker/test-nonroot-cli.sh (new) + validate_docker_image.yml (run it; path trigger; cleanup)
  • docs/adr/ADR-004-non-root-cli-self-drop.md (new)
  • Directory.Build.props + RELEASE_NOTES.md0.23.0-beta.3

Release

Tag 0.23.0-beta.3 after merge to fire publish_release_binaries.yml.

….23.0-beta.3)

The daemon runs as the non-root netclaw user, but docker exec / kubectl
exec default to the image's root user, and 'netclaw init' is invoked
that way. A root-context CLI run extracted the .NET single-file bundle
into a per-$HOME dir the runtime locks root:root (and wrote config
root-owned), after which the non-root daemon could no longer run its own
CLI (Failed to create directory ... Error code: 13 / EACCES, exit 160).

/usr/local/bin/netclaw is now a self-dropping launcher that re-execs as
the netclaw user when invoked as root; when already netclaw it execs
directly (composes with runAsUser / -u netclaw). netclawd stays a plain
symlink (only ever launched by the already-dropped entrypoint).

Adds ADR-004 and a standalone-docker regression test
(scripts/docker/test-nonroot-cli.sh) wired into validate_docker_image.

Bumps to 0.23.0-beta.3.
@Aaronontheweb Aaronontheweb force-pushed the fix/docker-nonroot-cli-launcher branch from f39dcfd to 7ff3efb Compare June 4, 2026 06:45
@Aaronontheweb Aaronontheweb merged commit 6561c49 into dev Jun 4, 2026
21 checks passed
@Aaronontheweb Aaronontheweb deleted the fix/docker-nonroot-cli-launcher branch June 4, 2026 07:02
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.

1 participant