Skip to content

Deliver sidecar secrets as environment without exposing them#14

Merged
CrypticSwarm merged 2 commits into
masterfrom
tongs-phase-7d-secret-delivery
Jun 19, 2026
Merged

Deliver sidecar secrets as environment without exposing them#14
CrypticSwarm merged 2 commits into
masterfrom
tongs-phase-7d-secret-delivery

Conversation

@CrypticSwarm

Copy link
Copy Markdown
Owner

Summary

A sidecar definition can hold credentials in its env: as
${secret:<provider>:<ref>} references. This teaches the host launcher
(scripts/run_anvil.py) to resolve those references and hand the resolved values
to the sidecar as ordinary environment variables -- so an off-the-shelf server
that reads them from its environment at startup works unmodified -- without ever
exposing them to the harness, to docker inspect, or to disk.

What this does

  • Resolves secrets on the host. The launcher shells out to the provider CLIs
    declared in the user-layer table (--providers, defaulting to
    ~/.swarmforge/secret-providers.yaml), so interactive unlocks happen in the
    user's terminal before the harness starts. Plain (non-secret) env keeps flowing
    through -e.
  • Delivers secret env over a FIFO, never -e. A resolved secret must never be
    a docker -e value (anything holding the docker socket could read it back via
    docker inspect), a command-line argument, or a file on disk. The launcher
    creates a host FIFO, bind-mounts it read-only into the sidecar, and overrides
    the entrypoint with a /bin/sh wrapper that reads the FIFO, exports each
    NAME=value into its environment, then execs the image's real
    entrypoint+command (read via docker inspect, or declared on the sidecar as
    entrypoint:/command:). The read and eval are guarded, so the real process
    never starts if delivery fails rather than running without its secrets. The
    wrapper blocks on the read, so the env is set before the real process
    starts -- no startup race -- and the secret bytes only ever live in the kernel
    pipe buffer.
  • Fails safe. The write end is opened non-blocking and times out if no reader
    attaches; the payload is written in a loop so a value larger than the pipe
    buffer is delivered whole rather than truncated; a reader that closes early
    fails the launch. A delivery that fails (docker error or Ctrl-C) removes the
    just-started container before raising, so a long-lived sidecar is not cached and
    reused while missing its secret, and the FIFO is always cleaned up.
  • Makefile. run_opencode / run_claude pass the provider-table path to the
    launcher.

The credential tongs we want to run are off-the-shelf servers that read their
secrets from the process environment at startup, so they must work unmodified.
A resolved secret still must never reach a tong as a docker -e value (anything
holding the docker socket could read it back via docker inspect), a command
argument, or a file on disk.

So a tong whose env references ${secret:...} is now started behind a /bin/sh
wrapper: the launcher creates a host FIFO, bind-mounts it read-only into the
tong, and overrides the entrypoint with a wrapper that reads the FIFO, exports
each NAME=value into the environment, then execs the image's real
entrypoint+command (read via docker inspect, or declared on the tong as
entrypoint:/command:). The read and eval are guarded (secret_env=$(cat ...) ||
exit 1; eval "$secret_env" || exit 1) so the real process never starts if
delivery fails -- a bare eval "$(cat ...)" would succeed on a failed or empty
read and run the target without its secrets. The launcher resolves the secret
env and writes export lines into the FIFO once the wrapper has opened the read
end. The values arrive as ordinary environment variables -- set before the real
process starts, since the wrapper blocks on the FIFO until delivery -- while the
bytes only ever live in the kernel pipe buffer, never an -e value, an argv, or
disk.

The write end is opened non-blocking and times out if no reader attaches, so a
tong that never starts does not hang the launch; the payload is written in a
loop so a value larger than the pipe buffer is delivered whole rather than
truncated, and a reader that closes early fails the launch rather than silently
dropping bytes. A delivery that fails (docker error or Ctrl-C) removes the
just-started container before re-raising, so a half-configured shared tong is
not cached and reused while missing its secret, and the FIFO is always cleaned
up. A tong with secret env needs a /bin/sh in its image; one without secrets
runs its image entrypoint unchanged.

Plain (non-secret) env keeps flowing through -e. mcp and volume interfaces, and
a shared tong that mounts the workspace, are still refused.
run_opencode / run_claude now hand the launcher the user-layer
secret-providers.yaml path so a tong's ${secret:...} references resolve
through the configured provider CLIs. The path is a launcher option consumed
before the anvil command, so with no tongs discovered the file is never read
and the anvil argv is forwarded unchanged.
@CrypticSwarm CrypticSwarm merged commit 02efa27 into master Jun 19, 2026
1 check passed
@CrypticSwarm CrypticSwarm deleted the tongs-phase-7d-secret-delivery branch June 19, 2026 02:50
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