Skip to content

feat(docker): declare the minimal CogOS workspace as a container image - #574

Draft
chazmaniandinkle wants to merge 1 commit into
mainfrom
feat/workspace-base-image
Draft

feat(docker): declare the minimal CogOS workspace as a container image#574
chazmaniandinkle wants to merge 1 commit into
mainfrom
feat/workspace-base-image

Conversation

@chazmaniandinkle

Copy link
Copy Markdown
Contributor

What this declares

Dockerfile.workspace is the first official declaration of a CogOS workspace as an image — as distinct from the existing Dockerfile, which containerizes only the kernel binary. Every existing container artifact in this repo (Dockerfile, Dockerfile.e2e, docker-compose.node.yml) mounts a workspace in from the host; nothing declares what a fresh workspace actually is. This does.

Two things make it trustworthy rather than a hand-copied file list:

  1. Scaffold via the real code path. The scaffold build stage runs the actual cogos init --workspace /workspace (internal/engine/init.go's RunInit) against a downloaded release binary — not a manually-copied directory tree. If init.go changes what init produces, this image changes with it the next time it's rebuilt against a release containing that change.
  2. Release verified the way self-update verifies it. internal/providers/selfupdate/github.go resolves cogos-<goos>-<goarch>, checksums.txt, checksums.txt.sig, checksums.txt.pem for a release; docs/release-signing.md documents the consumer recipe (cosign verify-blob against this repo's release.yml CI identity, then sha256sum -c). The fetch stage does exactly that, and fails the build (REQUIRE_SIGNATURE_VERIFICATION=1, the default) if either check doesn't pass.

cogos init currently scaffolds (from reading internal/engine/init.go directly):

  • Directories: .cog/config, .cog/agents/identities, .cog/mem/{semantic,episodic,procedural,reflective,working}, .cog/ledger, .cog/run, .cog/blobs
  • Files (skipped if already present — init is idempotent): .cog/config/kernel.yaml, .cog/config/identity.yaml, .cog/config/providers.yaml, .cog/agents/identities/identity_cogos.md
  • A .cog/VERSION marker (currently 3.0.0)

Component seams (build ARGs)

  • COGOS_VERSION — pinned release tag (default v0.16.31, the latest published tag as of this writing; not resolved dynamically, so builds stay reproducible)
  • WORKSPACE_NAME, COGOS_PORT — cosmetic identity / port, stamped as labels/env
  • SKILLS_OVERLAY, CONFIG_OVERLAY — build-context-relative directories COPYed onto .claude/skills/ and .cog/config/ after scaffolding. Default to empty placeholders checked in at docker/workspace-overlay/{skills,config}/, so a plain docker build produces zero overlay. No ONBUILD; a distribution just points these ARGs at its own directories inside its build context.
  • REQUIRE_SIGNATURE_VERIFICATION — fail-closed by default; 0 only for pre-signing releases (< v0.16.20) or air-gapped builds.
  • COGOS_WORKSPACE_SCHEMA (default 3.0.0) — the cog.workspace.schema OCI label. The scaffold stage asserts this equals the actual .cog/VERSION cogos init wrote, so the label can never silently drift from reality — bump the ARG when init.go's VERSION marker changes, or the build fails loudly.

What is deliberately NOT in this image

No ANTHROPIC_API_KEY or any other secret, no provider config beyond shipped defaults, no identity beyond the generic default identity card, no skills, no CLAUDE.md/SOUL.md/USER.md. A workspace is a substrate, not a persona — those are supplied by whoever runs the image, same as the kernel Dockerfile's external workspace volume mount today.

Composition

docker-compose.workspace.yml is a new, separate compose file (not an addition to docker-compose.node.yml): a workspace-init service builds the workspace image, materializes its scaffolded /workspace into a named volume, and exits; the existing kernel Dockerfile then mounts that volume once workspace-init completes successfully. docker-compose.node.yml's primary/secondary topology already assumes an externally bind-mounted workspace across several services (vaultwarden, gateway, bridge, tailscale siblings) — threading a build-time-scaffolded workspace through that would mean touching every node's volume wiring for a concern this file demonstrates cleanly in two services, with zero risk to the existing topology. docker-compose.node.yml is untouched.

Docs

Added a "Workspace base image" subsection to README.md's existing ### Docker section (where the repo already documents docker-compose.node.yml): states plainly that Dockerfile.workspace is the declared minimal starting condition of a workspace, cog doctor validates it, this image materializes it, and distributions layer overlays on top.

Verification actually performed

No Docker daemon was available on this host (docker infoCannot connect to the Docker daemon at unix:///Users/slowbro/.docker/run/docker.sock), so the image was not built or run. docker buildx build --check was attempted and failed for the same reason (needs a builder backed by a running daemon). What was done instead:

  • Careful manual review against the existing Dockerfile's working multi-stage patterns (builder/runtime split, apk add --no-cache, non-root cogos user via addgroup -S/adduser -S, HEALTHCHECK shape).
  • Read internal/engine/init.go, internal/engine/cli.go (command dispatch, runHealthCheckCmd, runInitCmd), internal/providers/selfupdate/github.go and resolve.go, docs/release-signing.md, and .github/workflows/release.yml directly, rather than assuming their behavior.
  • Resolved alpine:3.21's current multi-arch manifest-list digest live against the Docker Hub registry API (sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d) to pin the base image by digest while keeping multi-platform resolution — this is a real, current digest as of 2026-08-21, not fabricated, but it will need periodic bumping like any digest-pinned base.
  • Confirmed v0.16.31 is the current latest published cogos release (gh release list) and that it postdates FirstSignedRelease = v0.16.20, so signature verification is exercised by default with the chosen COGOS_VERSION default.
  • Confirmed cosign v3.1.3 is current and that sigstore/cosign publishes a cosign_checksums.txt its own binary can be checksummed against.
  • Traced the ARG-vs-ENV-vs-runtime-shell-expansion semantics by hand for HEALTHCHECK/CMD (ARGs don't survive into the running container; EXPOSE/LABEL/COPY get Dockerfile-level substitution, CMD/HEALTHCHECK don't — they need ENV plus shell-form to pick up $COGOS_PORT at container start) since this class of bug is easy to introduce and silent until runtime.

I did not get to execute a real build, so there is real risk of a typo-class Dockerfile bug (a missing stage reference, a shell quoting slip) that only surfaces in an actual docker build. Please build it before merging, or ping me to arrange a host with a running daemon.

Design decisions the operator should ratify

  1. Binary name: cogos, not cog. The existing kernel Dockerfile builds from source and names its entrypoint binary cog (COPY --from=builder /cog /usr/local/bin/cog). This image installs the published release asset, which GitHub Actions names cogos-<goos>-<goarch> and which self-update/install docs refer to as cogos (e.g. ~/.cog/bin/cogos). I kept the published name (/usr/local/bin/cogos, ENTRYPOINT-equivalent CMD cogos ...) rather than renaming it to match the kernel image's cog convention. Flag if the two images should agree on one binary name.
  2. Separate compose file instead of extending docker-compose.node.yml. Explained above under Composition — open to revisiting if a tighter integration is wanted later.
  3. Overlay mechanism is build-context-relative COPY behind ARGs, not ONBUILD or a templated Dockerfile. This satisfies "ONBUILD-free template hooks" literally, but it does mean a distribution's overlay directory must live inside this build's context (or the distribution builds from a docker build -f path/to/Dockerfile.workspace .. pointed at a context that contains both). An alternative (not implemented) is a distribution doing FROM ghcr.io/myrgic/cogos-workspace:TAG in its own Dockerfile and COPYing on top of the published image instead of rebuilding from source with different ARGs — probably the more common real-world pattern once this image is actually published to ghcr.io. Worth deciding which pattern is canonical.
  4. cog.workspace.schema is pinned to the literal string .cog/VERSION currently holds (3.0.0), asserted at build time. If init.go's VERSION marker is meant to mean something more specific (a scaffold-shape version distinct from a general workspace format version), the assertion still holds but the semantic label content may need a different source of truth.
  5. workspace-init in the compose file runs as root and chmod -R a+rwXs the shared volume rather than trying to coordinate matching UIDs between two independently-built Alpine images. Documented inline as a compose-demo simplification, not a production pattern.
  6. HEALTHCHECK/CMD use shell-form and an ENV mirror of COGOS_PORT (not ENTRYPOINT + exec-form CMD like the existing kernel Dockerfile) because exec-form JSON arrays never expand $COGOS_PORT — Docker only substitutes ARGs into LABEL/EXPOSE/COPY/etc., not into CMD/ENTRYPOINT/HEALTHCHECK content. Flagging since it's a real behavioral divergence from the existing Dockerfile's style, made necessary by wanting the port to be genuinely build-configurable.

Refs #571 — this is the materialize-side counterpart to cog doctor (#570)'s declared/validated surface: doctor checks an existing install/workspace against what healthy looks like; this Dockerfile is the thing that produces a workspace doctor should find healthy.

Dockerfile.workspace is the first official declaration of what a CogOS
workspace IS, not just how the kernel runs. It fetches a checksum- and
Sigstore-verified published release the same way self-update does, then
scaffolds the workspace by running the real `cogos init` (not a hand-copied
file list), so the image can never drift from what init.go actually
produces. A build-time assertion checks the cog.workspace.schema label
against .cog/VERSION so the label cannot silently lie.

docker-compose.workspace.yml composes this image with the existing kernel
Dockerfile as a separate file rather than an addition to
docker-compose.node.yml, to avoid touching that file's multi-node bind-mount
topology for an unrelated concern.

Refs #571.
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