Container Compilation
Container Compilation v0.1
Section titled “Container Compilation v0.1”This document specifies how the Spawnfile compiler emits container artifacts alongside runtime-specific config and workspace files.
The goal is simple: spawnfile compile should produce output that can be built and run with docker build and docker run, giving developers and operators a way to verify that compiled output actually works against the real runtime. STATUS.md defines the read-only status layer over detached Docker deployments created from these artifacts.
Core Rule
Section titled “Core Rule”One compile = one container.
The compiler walks the full graph from the root Spawnfile. Everything it resolves — agents, subagents, team members — lands in a single container image. This applies regardless of:
- how many Spawnfiles are in the graph
- how many agents or subagents are resolved
- how many distinct runtimes appear in the compile plan
Output Layout
Section titled “Output Layout”The compiler should emit container artifacts at the compile output root, alongside the existing runtime output:
.spawn/├── Dockerfile├── entrypoint.sh├── .env.example├── container/│ └── rootfs/│ └── var/lib/spawnfile/instances/...├── runtimes/│ ├── openclaw/agents/analyst/...│ └── picoclaw/agents/editor/...└── spawnfile-report.jsonThe Dockerfile and entrypoint.sh are derived from the compile plan. They are not templates chosen by the user — the compiler generates them based on the resolved graph.
runtimes/ is the human-inspectable adapter output. container/rootfs/ is the final container filesystem emitted by the compiler for build-time placement into the runtime’s expected paths.
This is the default hidden output root. --out <dir> may be used to export the same artifacts into a visible directory when needed.
Dockerfile Generation
Section titled “Dockerfile Generation”Base Image
Section titled “Base Image”Each runtime adapter should declare:
- a standalone base image or install strategy aligned with the pinned runtime ref
- system dependencies required
- the expected config and workspace paths inside the container
- the start command and any runtime env it needs
For single-runtime compiles, the Dockerfile uses that runtime’s standalone base image or install strategy directly.
For multi-runtime compiles, the Dockerfile should use a common base and install each runtime.
Runtime Installation
Section titled “Runtime Installation”Each adapter should expose enough information to generate install steps:
interface RuntimeContainerMeta { configFileName: string; configEnvBindings?: Array<{ envName: string; jsonPath: string; }>; configPathEnv?: string; env?: Array<{ description: string; name: string; required: boolean; }>; homeEnv?: string; instancePaths: { configPathTemplate: string; homePathTemplate?: string; sourceWorkspacePathTemplate?: string; workspacePathTemplate: string; }; globalNpmPackages?: string[]; port?: number; portStride?: number; portEnv?: string; postRootfsCommands?: string[]; standaloneBaseImage: string; startCommand: string[]; staticEnv?: Record<string, string>; systemDeps: string[];}
interface ContainerTarget { configEnvBindings?: Array<{ envName: string; jsonPath: string; }>; id: string; files: EmittedFile[]; envFiles?: Array<{ envName: string; relativePath: string; }>; sourceIds?: string[];}The compiler uses this metadata to compose the Dockerfile and entrypoint. Adapters own their runtime’s container story; the compiler just stitches them together.
sourceWorkspacePathTemplate is used by grouped runtimes where one process hosts several concrete source agents but each source still needs its own workspace directory. sourceIds records which compile nodes a grouped container target serves. Files emitted under the reserved runtime/ target path are placed in the adapter’s runtime install root; adapters may use this for generated harness apps that are installed alongside pinned runtime packages.
postRootfsCommands run in the generated Dockerfile after container/rootfs/ has been copied into the image. They are only for adapter-owned install steps that truly depend on generated rootfs files. Runtime package installs SHOULD run before container/rootfs/ is copied so Docker can reuse the expensive dependency layer when only agent prompts, docs, skills, or generated configs change.
Pinned Versions
Section titled “Pinned Versions”The runtime version used in the Dockerfile should match the pinned registry metadata from runtimes.yaml. This keeps the compiled container aligned with the runtime version the adapters were written against.
spawnfile compile itself should not require local runtime clones on the compiler machine. The compile step reads the registry metadata and adapter contracts; the Docker build step is responsible for fetching or installing the pinned runtime artifact.
The v0.1 reference implementation uses pinned compiled runtime artifacts:
- npm packages where the runtime publishes them
- release archives or bundles where the runtime ships them
Generated Dockerfiles must not clone runtime repositories or rebuild runtime sources during image build.
Runtimes MAY provide a reusable artifact image that already contains their pinned runtime package dependencies. Copyable artifact images are preferred for runtimes that may appear in mixed-runtime organizations, because generated Dockerfiles can compose them with COPY --from instead of requiring one base image for every runtime combination.
The Daimon, OpenClaw, and PicoClaw adapters use published runtime artifact images by default. Generated Dockerfiles copy each runtime from /opt/spawnfile/runtime-installs/<runtime> and skip runtime npm/archive installs during organization builds.
Current default images:
noopolis/spawnfile-runtime-daimon:0.1.0noopolis/spawnfile-runtime-openclaw:2026.6.8noopolis/spawnfile-runtime-picoclaw:0.2.9To test a local Daimon runtime artifact instead:
cd daimonnpm run image:runtime:localcd ..SPAWNFILE_DAIMON_RUNTIME_IMAGE=noopolis/spawnfile-runtime-daimon:0.1.0-local spawnfile up ./org --detachOpenClaw and PicoClaw have equivalent overrides:
SPAWNFILE_OPENCLAW_RUNTIME_IMAGE=noopolis/spawnfile-runtime-openclaw:2026.6.8-local spawnfile up ./org --detachSPAWNFILE_PICOCLAW_RUNTIME_IMAGE=noopolis/spawnfile-runtime-picoclaw:0.2.9-local spawnfile up ./org --detachThe Pi compatibility adapter still supports a reusable base image with SPAWNFILE_PI_RUNTIME_BASE_IMAGE; when set, generated Dockerfiles use that image as the base and skip the Pi npm dependency install. Build the reference Pi base image with:
npm run runtime:pi-base -- noopolis/spawnfile-pi-runtime:0.79.9-node24SPAWNFILE_PI_RUNTIME_BASE_IMAGE=noopolis/spawnfile-pi-runtime:0.79.9-node24 spawnfile up ./org --detachDeclared environment.packages are installed into the generated image before runtime startup:
aptpackages are added to the Debianapt-get installlayer;versionis rendered asname=version.npmpackages are installed globally withnpm install -g;versionis rendered asname@version.pipxpackages are installed withPIPX_HOME=/opt/pipxandPIPX_BIN_DIR=/usr/local/bin;versionis rendered asname==version.
Project-declared npm packages override runtime default global npm packages with the same package name, so a project can pin a runtime-adjacent CLI version intentionally.
Entrypoint Generation
Section titled “Entrypoint Generation”The entrypoint script is responsible for:
- Validating required env and required files
- Materializing env-backed secret files when a runtime expects file-based auth
- Materializing env-backed runtime config fields when a runtime stores auth in config
- Preparing workspace resources and managed state before startup
- Starting any managed Moltnet services and runtime process(es)
Single-Runtime
Section titled “Single-Runtime”For a single runtime, the compiler should prefer build-time placement into the runtime’s final config and workspace paths under container/rootfs/.
The entrypoint should then stay minimal:
- validate required env vars
- validate that the compiled config exists at the expected final path
- write env-backed secret files when needed
- patch runtime-native config fields from env when needed
- prepare workspace resources and managed Moltnet services
execthe runtime’s start command
Multi-Runtime
Section titled “Multi-Runtime”For multiple runtimes in one container, the compiler should still pre-place config and workspace files into final paths at build time.
The entrypoint then:
- validates required env and config for each target
- writes env-backed secret files for each target when needed
- patches runtime-native config fields from env when needed
- starts each runtime process
- prepares workspace resources and managed Moltnet services
- traps signals and forwards them to all child processes
- waits for all processes
This follows the pattern used by existing multi-agent deployments (e.g. picoclaw multi-gateway entrypoints that spawn one process per agent and manage the process group).
Workspace Resource Lifecycle
Section titled “Workspace Resource Lifecycle”For each effective workspace.resource attached to a concrete agent lifecycle, startup must enforce mount behavior:
- Resolve the declared
mountto the agent-visible link path:./pathand${workspace}/pathresolve under the concrete runtime workspace./absolute/pathis used as an explicit container path.
- Prepare the resource under Spawnfile-managed backing storage.
- Expose the backing path at the agent-visible link path with a symlink before the runtime starts.
volumeresources: create backing directories and verify ownership/permissions before first launch.gitresources:- clone into empty backing paths using declared selector (
branch,tag, orref) - reuse compatible existing checkouts when present
- fail fast when the backing path contains an incompatible checkout
- clone into empty backing paths using declared selector (
Compatibility uses exact remote URL match (after trim) and exact selector match.
The compiler does not perform git mutation at build time.
sharing: per_agent resources use backing storage scoped to the concrete runtime target. sharing: team volume resources use backing storage scoped to the team where the resource was declared, so all inheriting concrete members see the same files at their own workspace-relative link paths.
Single Agent vs Team vs Subagents
Section titled “Single Agent vs Team vs Subagents”- Single agent: one runtime process, one config
- Agent with subagents: one runtime process — the runtime itself manages subagent delegation internally
- Team with members on one runtime: one runtime process with multi-agent config (if the runtime supports it), or one process per agent
- Team with members on multiple runtimes: one process group, one runtime process per distinct runtime
The entrypoint does not need to understand agent semantics. It only needs to know which runtime processes to start, which env files to materialize, and where the final compiled config already lives.
Environment and Secrets
Section titled “Environment and Secrets”The compiler should emit a .env.example file listing all required and optional environment variables:
- secrets declared in manifests (e.g.
SEARCH_API_KEY) - model auth variables for providers that still use
api_keyauth (e.g.ANTHROPIC_API_KEY) - surface auth variables for declared communication surfaces (e.g.
DISCORD_BOT_TOKEN) - runtime auth variables (e.g.
OPENCLAW_GATEWAY_TOKEN) - Moltnet auth/store variables declared under managed/external server blocks (for example
MOLTNET_STORE_DSNor static attachment token names) - any variables the entrypoint or runtime expects
Actual secret values are never emitted. The .env.example contains variable names with empty values and comments describing their purpose.
At runtime, secrets are injected via:
--env-fileondocker run- environment variable pass-through
- mounted secret files
If a runtime expects secret file references in its config, the adapter should declare those env-to-file bindings and the entrypoint should materialize them before startup.
Model auth intent itself is declared on each source model target under execution.model.primary and execution.model.fallback[*]. The compile output should therefore reflect:
- which provider/runtime instances still require
api_keyenv at run time - which provider/runtime instances expect imported CLI credential stores such as
claude-codeorcodex - which declared communication surfaces require env-backed secrets at run time
Moltnet Storage and Secret Materialization
Section titled “Moltnet Storage and Secret Materialization”Container startup must support Moltnet server and node artifacts emitted from team.networks[].server:
- For
spawnfile build/spawnfile upwith--context, the compiler resolves the docker context architecture before staging Moltnet artifacts and selectsmoltnet_linux_<architecture>.tar.gzfor that architecture. For manual compile without--context, you can force the target withSPAWNFILE_MOLTNET_TARGET_ARCH=amd64|arm64(orx86_64/aarch64aliases). - When Moltnet binaries are not staged from a local release asset, the generated Dockerfile installs Moltnet from
https://moltnet.dev/install.shand includes the GitHub latest-release metadata as a prior Docker layer so Docker cache invalidates when Moltnet releases. server.store.kind: sqliteandserver.store.kind: jsoncreate the configured or default store directory before server start.- Durable
sqliteandjsonstores emitcontainer.persistent_mounts[]entries thatspawnfile runandspawnfile uptranslate into Docker named volumes. server.store.persistence.mode: ephemeralskips persistent mount emission but still creates the in-container store directory before server start.server.store.kind: postgresinjectsserver.store.dsn_secretinto runtime config and skips local path creation.server.store.kind: memorycreates no local persistence directory.
Secret materialization rules:
server.auth.tokens[].secretis never written into source-controlled files.server.auth.tokens[].secretis written into private Moltnet config values at runtime start.server.store.dsn_secretis written asstorage.postgres.dsnin managed server config.server.pairings[].token_secretis written aspairings[].tokenin managed server config.- Generated open-mode token files for attach/self-claiming clients are runtime state files with private permissions (equivalent to
0600), and token directories use private directory mode (equivalent to0700). - Generated open-mode token directories are reported as persistent mounts so claimed agent identities survive container replacement.
Detached Deployment Records
Section titled “Detached Deployment Records”spawnfile run --detach and spawnfile up --detach write deployment records after a container starts successfully. Records live under the selected output directory:
.spawn/deployments/default.json.spawn/deployments/<name>.jsonDeployment names are operator-local kebab-case slugs. They are not declared in Spawnfile source.
The Docker deployment record schema and behavior are defined in STATUS.md. Container compilation must provide the fields that records need:
compile_fingerprintoutput_directory- runtime instance ids and the compile node ids each instance serves
- image tag/id and container name/id after successful start
- Docker target information for context execution
- persistent mounts and published ports
A failed detached start MUST NOT write a record. Redeploying the same deployment name MUST replace the record atomically only after the new detached start succeeds.
spawnfile dev up writes the same record shape under .spawn-dev/deployments/
by default. spawnfile dev apply --agent <id> reads that record to find the
running Docker target and container, recompiles into .spawn-dev without
removing records, and mutates the running development container in place. The
v0.1 hot-apply path is Pi-only: it copies the refreshed Pi app config, the
selected agent workspace, every matching Moltnet node config, and managed
Moltnet server configs into the container, then calls the generated Daimon control
endpoint to load or reload that agent. New-agent Moltnet nodes are started as
that agent is applied. Existing agents and the container are not restarted.
Running managed Moltnet servers keep their current in-memory room membership
until the copied server config is reconciled by an operator-token moltnet apply or a server restart.
spawnfile dev activity uses the same deployment record to read the generated
Pi app’s bounded activity buffer from the running container. It is a runtime
diagnostic surface, not a Moltnet message reader.
Docker Targets
Section titled “Docker Targets”Detached Docker execution may run against the default Docker context, an explicit --context <name>, or DOCKER_HOST. The deployment record must store the Docker target actually used:
{ "kind": "context", "name": "vm1", "endpoint_fingerprint": "sha256:4be91d2b0d4f3a7c99e8123400aa55cc" }{ "kind": "host", "value": "ssh://ops@my-vm" }endpoint_fingerprint is a hash of the resolved Docker context endpoint. Status must re-resolve the context and report endpoint drift as an error instead of falling back to the local daemon.
Docker Labels
Section titled “Docker Labels”Detached containers should receive non-secret status/deployment labels:
com.spawnfile.version=0.1com.spawnfile.project=<project-slug>com.spawnfile.deployment=<deployment-name>com.spawnfile.unit=<unit-id>com.spawnfile.compile_fingerprint=<compile-fingerprint>Labels are identifiers only. They must not include absolute paths, usernames, hostnames, auth profile names, env values, generated token values, or secrets.
Adapter Container Contract
Section titled “Adapter Container Contract”Each runtime adapter should expose container metadata as part of its adapter interface, plus optional per-target container overrides such as env-backed secret files.
The compiler calls each relevant adapter for its container metadata and container targets, then composes the Dockerfile and entrypoint from the combined metadata.
Compile Report Extensions
Section titled “Compile Report Extensions”The compile report should include a container section:
{ "container": { "runtimes_installed": ["openclaw", "picoclaw"], "dockerfile": "Dockerfile", "entrypoint": "entrypoint.sh", "env_example": ".env.example", "model_secrets_required": ["ANTHROPIC_API_KEY"], "runtime_secrets_required": ["OPENCLAW_GATEWAY_TOKEN"], "runtime_homes": ["/var/lib/spawnfile/instances/openclaw/agent-analyst/home"], "persistent_mounts": [ { "id": "moltnet-local_lab-store", "mount_path": "/var/lib/spawnfile/moltnet/networks/local_lab", "volume_name": "spawnfile-project-moltnet-local-lab-store-00000000", "reason": "managed Moltnet sqlite store for local_lab" } ], "workspace_resources": [ { "id": "project-repo", "kind": "git", "mount": "./repos/project", "link_path": "/var/lib/spawnfile/instances/openclaw/agent-analyst/home/.openclaw/workspace/repos/project", "backing_path": "/var/lib/spawnfile/resources/instances/agent-analyst-00000000/project-repo-00000000", "mode": "mutable", "sharing": "per_agent" } ], "secrets_required": ["SEARCH_API_KEY", "ANTHROPIC_API_KEY"], "ports": [ { "id": "openclaw-gateway", "internal": 3000, "published": 3000 } ], "moltnet": { "servers": [ { "network_id": "local_lab", "mode": "managed", "auth_mode": "open", "rooms": ["research"], "listen_port": 8787, "published_port": 8787 } ], "nodes": [ { "network_id": "local_lab", "agent_ids": ["analyst"], "rooms": ["research"] } ] }, "runtime_instances": [ { "id": "agent-analyst", "node_ids": ["agent:analyst"], "runtime": "openclaw", "config_path": "/var/lib/spawnfile/instances/openclaw/agent-analyst/home/.openclaw/openclaw.json", "home_path": "/var/lib/spawnfile/instances/openclaw/agent-analyst/home", "workspace_path": "/var/lib/spawnfile/instances/openclaw/agent-analyst/home/.openclaw/workspace", "model_auth_methods": { "anthropic": "claude-code" }, "model_secrets_required": [] } ] }}The moltnet report data is sanitized. It records ids, modes, rooms, and ports only; it must not include operator tokens, agent tokens, pairing tokens, generated token file contents, or secret-bearing config patches.
What This Does Not Cover
Section titled “What This Does Not Cover”These are explicitly out of scope for v0.1 container compilation:
Image publishing and sourceless run are covered separately by the distribution spec.
- Docker Compose generation for multi-container topologies
- Orchestration beyond the Docker detached record model (Kubernetes, ECS, Fly, etc.)
- Runtime-native auth bootstrap (onboarding flows stay manual)
- emitted Docker
HEALTHCHECKinstructions or richer readiness contracts beyond adapter-owned status probes - General volume orchestration beyond compiler-reported persistent mounts
- Network topology between containers
- CI/CD integration
Spawnfile-managed auth profile storage and spawnfile run orchestration are adjacent UX layers, not part of compile output itself, but the compile output does include the metadata needed for run to validate declared model auth and mount the right credential material.
Validation
Section titled “Validation”The compiler should verify at compile time:
- every runtime in the compile plan has container metadata
- all declared secrets are listed in the
.env.example - all runtime bins are installed in the Dockerfile
- config and workspace paths do not collide across runtimes
At build/run time, validation is the container’s responsibility — the entrypoint should fail fast with clear errors if required config or secrets are missing.
Adapter verification at the pinned ref should include:
spawnfile compilespawnfile builddocker builddocker run- a host-side smoke check against the runtime’s exposed health or API endpoint when the runtime exposes network services
Developer Workflow
Section titled “Developer Workflow”The intended workflow for testing compiled output:
# sync declared model auth and project secrets into a local profilespawnfile auth sync fixtures/single-agent --profile dev --env-file ./.env
# compile and build the containerspawnfile build fixtures/single-agent --out ./bundle/single-agent --tag my-agent
# run with the local auth profile and inspect live statusspawnfile run fixtures/single-agent --out ./bundle/single-agent --tag my-agent --auth-profile dev --detachspawnfile status fixtures/single-agent --out ./bundle/single-agent --liveFor teams:
spawnfile auth sync fixtures/multi-runtime-team --profile dev --env-file ./.envspawnfile build fixtures/multi-runtime-team --out ./bundle/team --tag my-teamspawnfile run fixtures/multi-runtime-team --out ./bundle/team --tag my-team --auth-profile dev --detach --deployment devspawnfile status fixtures/multi-runtime-team --out ./bundle/team --live --deployment devSame flow regardless of project complexity. One compile, one build, one detached run, one live status check.
For interactive Pi org development, use the dev loop:
spawnfile auth sync fixtures/e2e/pi-harness-org --profile dev --env-file ./.envspawnfile dev up fixtures/e2e/pi-harness-org --auth-profile dev --deployment devspawnfile dev apply fixtures/e2e/pi-harness-org --agent new-agent --deployment devspawnfile dev activity fixtures/e2e/pi-harness-org --agent new-agent --deployment devDev mode uses .spawn-dev by default and keeps the deployment record there.
dev apply is intentionally source-backed and Pi-specific in v0.1. It does not
rebuild the image or restart the container; it updates one generated Daimon agent in
the running container and starts that agent’s Moltnet bridges only when the
agent is new.
spawnfile compile still emits a standard Docker build context, so manual docker build remains supported when developers want to inspect or tweak the emitted output before building the image.
The intended auth split is:
Spawnfiledeclares model auth intent on each model target viaauth, plusendpointforcustomandlocalbackendsSpawnfiledeclares runtime/project secret requirements throughenvironment.secretsand teamshared.environment.secretsspawnfile auth syncmaterializes matching local auth and declared secret values into a profilespawnfile buildstays secrets-freespawnfile run --auth-profile ...injects only the auth material required by the declared methods and secretsspawnfile run --env-file ...MAY inject external env values directly for a single run without first storing them in an auth profile
For repository-level verification, an opt-in Docker auth E2E harness SHOULD exist outside the normal unit-test flow. That harness SHOULD:
- build generated images from compiled output
- start containers with a local Spawnfile auth profile
- wait for host-reachable runtime readiness
- send real prompts through each supported runtime path
- fail unless the expected sentinel reply is observed
This harness is intentionally separate from npm test because it requires Docker, network access, and real credentials.