Skip to content

Minimal Capsem runtime with OCI application images (replaces profiles) #289

Open

Description

@ebursztein

Proposal: a minimal Capsem runtime with OCI application images

Draft for discussion · Revision 4 · 3 October 2026 · No implementation performed

Make the application image the environment users select. Keep one minimal Capsem VM underneath each session. Capsem owns isolation, policy, transport, and lifecycle. Images own applications and their dependencies. A catalog makes images discoverable, and administrator policy determines which images may run.

The purpose is a smaller common runtime that can be qualified thoroughly, less authority for application code, and environments administrators deliver with normal OCI tooling. It builds on the OCI path already in the tree and the Claude/Xpra work on v0.7.

This is forward-only. Profiles are deleted, not deprecated: config/profiles/, --profile, the per-profile rootfs builds, and just release-profile go away. There is no migration path for profile-built sessions.

This lands before the proxy work in #206. #206's standalone capsem proxy was specified against the profile/provider model this proposal deletes, and its proxy extraction should move a proxy that already serves workload MCP rather than move it twice. #206's process-authority and ledger-confinement sections concern host processes and do not depend on this proposal.

The product interface is direct.

capsem images
capsem images pull claude
capsem create codex
capsem create claude --name vm2
capsem create --image registry.company.com/team/dev:latest --name work

codex and claude are catalog names resolving to pinned OCI image digests. There is no other environment concept to choose or build. The CLI, desktop create dialog, SDK, and MCP use one service-owned resolver and one admission check.

capsem images lists the permitted catalog entries with description, supported architectures, and local availability. capsem images pull fetches ahead of time, so a first create need not wait on the network. An approved external reference can be used directly without a catalog entry; registry approval grants permission to use matching references without enumerating repositories.

There are two artifacts, with separate release lifecycles.

Artifact Contents Owner Released by
Minimal VM runtime Kernel/init, guest agent, OCI runtime and the launcher's own tools, network and DNS proxies, port bridge, telemetry, essential diagnostics and storage utilities Capsem release-binaries and the asset manifest, as today
Application image Claude, Codex, or another application with its tools, language runtimes, startup, and optional Xpra/display dependencies Capsem or administrator The independent image CI, replacing release-profile

An application update ships a new image digest and a new catalog version. The runtime is rebuilt only when it needs a new capability.

The catalog is an OCI artifact in our registry, published by the image CI.

The release manifest goes back to naming only the runtime. The catalog is a small document that the image CI pushes to our registry next to the images it describes. Each entry carries a friendly name, description, icon, and its supported versions: every digest still supported, each with its runtime contract version, OBOM digest, and .erofs digest when one exists. The friendly name selects the newest version compatible with the client's runtime. Clients fetch the catalog with the same host-side OCI puller as the images.

A catalog update adds versions; it does not silently remove them. A named session created yesterday restarts tomorrow because its digest is still listed as supported. Dropping a digest from the supported set is an explicit revocation decision, recorded as such, and it is the only way a catalog change stops a pinned session.

Integrity comes from digests plus who can publish. The catalog pins every image by digest, so a catalog entry is exactly as trustworthy as the authority to publish the catalog. The catalog's channel tags (catalog:stable, catalog:nightly) move; each catalog version is itself immutable by digest.

Publication order keeps the existing rule that discovery may not point at unpublished bytes: push and verify the images, their OBOMs, and their .erofs files, then push the catalog that names them. Image release tags are immutable, and every digest a catalog lists as supported stays in the registry.

Capsem publishes to GitHub Container Registry.

Images are packages of google/capsem on GHCR, under ghcr.io/google/capsem/<image>; catalog names keep that path out of everyday commands. Public packages have no storage limit to manage, nothing new to provision, and publication that is audited where the code already is. Every package version is tied to the repository and the workflow run that pushed it, and GitHub's attestations record build provenance against the digest. A second registry would add an account, a bill, and an audit trail in another place for nothing in return.

Publishing authority is the repository's. GHCR grants Actions access to a repository, not to one workflow file, so any workflow on protected main that declares packages: write can publish. That is the same trust as shipping binaries from main, which is the right bar. Default workflow token permissions stay read-only, and pull requests from forks only ever receive read tokens. By convention only the image workflow declares packages: write, and a citadel guard over .github/workflows/ holds that convention; it is not itself the authorization boundary. The debug image package additionally grants maintainers write, so they can push it from their machines.

Clients only pull, anonymously: official images need no GitHub account. Restricted images from other registries use host-side registry authentication, and registry credentials never enter the VM. Official images ship with build provenance attestations from the image workflow, verifiable with gh attestation verify, so administrators who mirror them can check where they came from. Capsem's own clients rely on the catalog digest.

Every published image has an OBOM, pinned by the catalog.

The runtime rootfs already ships a CycloneDX OBOM. cdxgen scans the exported rootfs, then the builder normalizes and validates the result. An image flattens to a rootfs, so the same scanner and validator cover images, giving one scanner and one format for runtime and images. The validator stops assuming a Debian base and names the image by repository and digest instead of capsem-rootfs-<arch>.

The OBOM is attached to its image in the registry as an OCI 1.1 referrer (oras attach), so anyone who pulls or mirrors the image can find it. The catalog records the OBOM's digest next to the image's, so the OBOM is pinned exactly like the image it describes.

An OBOM is an inventory, not an identity. It lists packages and their versions; it does not capture scripts, configuration, permissions, or application bytes outside a package manager. It answers "which sessions contain package X at version Y", a join between session digests and OBOMs on the host, and the difference between two OBOMs shows how a rebuild's dependencies moved. It never decides whether two images are the same. Image identity is the digest.

Images build in their own CI, which never touches the main build.

The main build is already slow. Image builds get no say in it: they are not a step, a dependency, or a gate of just test, release-binaries, or trunk CI.

  • Its own workflow, outside trunk CI and the gate. Image definitions live in images/ in this repository. Their workflow shares no triggers, runners, concurrency groups, caches, or machine lock with trunk CI or just test, and an image failure never turns trunk red. Trunk CI never builds or publishes an image.
  • One job per image and architecture, in parallel, on native runners. Images do not depend on each other, so wall-clock time is the slowest single image.
  • Only changed images rebuild on push. An image's input key is its build file, pinned application versions, and base digest. A scheduled refresh rebuilds everything to pick up upstream security fixes, and every rebuild that passes qualification is published. There is no semantic deduplication.
  • One shared base. A Capsem base image carries the distribution, CA trust, and the unprivileged user. Application images build FROM it by digest, and the registry stores shared layers once.
  • An OBOM is computed once per digest, since it depends only on the image's bytes.
  • Each image has a local command that builds it the way its CI does and runs the same smoke tests, so nothing reaches image CI that has not run locally. Image builds are not byte-reproducible; the local bar is passing the same tests, not producing the same bytes.

Compatibility is checked in both directions.

Building images and the runtime independently means each side must prove it still works with the other's released artifacts.

  • New images on the released runtime. The image CI boots each image on the latest released Capsem runtime that supports the image's contract version, and runs its smoke tests: it boots, its entrypoint becomes ready, its surface is reachable.
  • A new runtime on existing images. Runtime qualification runs a small set of already-published official images, pinned by digest in config/gate.toml beside the debug image, including the Claude GUI image. Nothing is rebuilt; the images are pulled and cached by digest. The debug image proves the test contract; only the GUI image proves the GUI contract, so a runtime change to namespaces, seccomp, or surface publication is caught before release.
  • The first image on a new contract. When a runtime adds a contract version, the first images targeting it are built and smoke-tested against the candidate runtime, then published with that contract version. The catalog lists versions per contract, so clients on older runtimes never see them, and images can be published before the runtime that needs them ships.

A pinned debug image keeps test environments out of CI.

capsem-debug carries everything the test suites need inside a workload: test runners, network tools for the kingslanding suites, package managers for the package-manager tests, the agent CLIs for real agent execution, and benchmark peers. No CI builds it. A maintainer builds it locally, proves it with just test on the local machine in its own VM, and pushes it from there to our registry. CI only ever pulls it.

  • The flow is build, test, push, pin. Build locally, run just test against the new digest, push that same digest, then commit the pin. Digests are content addresses, so the bytes CI pulls are the bytes the local run proved.
  • It is excluded from the default catalog and policy. It is not in the catalog and outside the default policy, so no installation runs it unless an administrator explicitly allows its repository or digest. It is publicly pullable so contributors need no credentials.
  • CI jobs that run it hold no publication credentials. release.yaml already keeps test jobs apart from the jobs that sign and publish; that separation becomes a rule with a guard, because the debug image runs inside those test jobs.
  • config/gate.toml pins it by digest. A new debug image changes nothing in this repository until a commit moves the pin.
  • After the first pull it is local. The pull is cached by digest, so just test makes no network fetch for it and builds no rootfs beyond the minimal runtime, which is itself cached by input key.
  • It holds third-party tools only. Anything built from the commit under test, such as guest binaries or capsem-bench-rs, is staged into the workload at test time, never baked into a pinned image.
  • Security checks run where the attacker is. Untrusted code now runs in the workload, so the sandbox assertions that matter run from inside the debug image, not from the runtime.
  • It lands with the runtime. The debug image arrives in the same step as the minimal runtime, so the suite never runs without its tools. Later contract changes land the same way: the new debug image and the runtime change are proven by one local just test run, and the pin moves in that commit.

The runtime stays deliberately small, and owns its own tools.

A dependency stays in the runtime only when boot, isolation, lifecycle, transport, telemetry, or diagnostics need it. MITM enforcement stays on the host; the guest keeps the proxies that feed it. Python, Bash, and system utilities stay for now because Capsem components use them. Removing them means rewriting those components, which is a separate, measured change and not part of this one.

Some of what the runtime needs is profile-owned today. runc, umoci, python3, iptables, and iproute2 come from config/profiles/code/apt-packages.txt, not the rootfs template. They move into the runtime's own package list before profiles go.

Agent CLIs, Node, scientific Python, compilers, Electron, Xpra, X11, fonts, D-Bus, and keyrings belong in images. A terminal image does not inherit GUI dependencies. Capsem ships purpose-specific official images, and administrators extend them or bring their own.

Profile policy moves into settings.

Profiles carried more than packages: default CEL enforcement rules (enforcement.toml), detection rules (detection.yaml), MCP server declarations (mcp.json), plugin modes, and VM sizing, and capsem-process enforces the merged profile today. Each of these gets a home before profiles are deleted:

  • Built-in defaults ship in the binary. The default enforcement rules, detections, and plugin modes are the product's defaults, not an environment's.
  • The existing settings and corp overlay carry the rest. Rules, detections, MCP servers, and plugin modes are set per installation and overridden by corp policy, through the settings files that already exist.
  • VM sizing comes from create flags, checked against the image's declared minimum.
  • Images carry no policy. An image describes what it needs; it never brings its own rules, detections, or MCP grants.

This also gives #206's standalone capsem proxy its configuration source.

The workload runs with less authority than the runtime.

The container is defense in depth inside the VM, not a second VM. The VM and host-side enforcement remain the boundary, and the host already treats every byte from the guest as untrusted. Even so, the workload today holds more than it needs: no user namespace, so container root is VM root with CAP_SETUID, CAP_SETGID, CAP_DAC_OVERRIDE, CAP_CHOWN, and CAP_FOWNER, under a seccomp profile that allows everything except AF_VSOCK sockets. VM root reaches every host service port over vsock, so a container escape is a real step for an attacker. This proposal tightens it:

  • A user namespace per workload. Container uids map to an unprivileged range of VM uids, fixed by the runtime contract (see ownership below). We turn on CONFIG_USER_NS in both guest kernel defconfigs, which today enable only the UTS, IPC, PID and NET namespaces. It is VM-wide, so guest services also get a seccomp filter denying namespace creation.
  • Non-root by default. Official images declare a non-root USER, and policy can require it of any image.
  • Capabilities only as proven necessary, held inside the user namespace rather than over the VM.
  • Deny-by-default seccomp, per surface. Every workload starts from the standard container allowlist, keeps the AF_VSOCK denial, and is denied mount, keyctl, setns, and netfilter netlink sockets. The guest kernel has CONFIG_NF_TABLES=y for the launcher's own firewall, and a workload that can create a user and network namespace would otherwise reach nf_tables, historically the richest source of kernel privilege escalations. BPF, io_uring, and userfaultfd are already compiled out.
  • noNewPrivileges stays. setuid bits inside an image are inert, so images need no setuid strip and the runtime needs no setuid exception.
  • Limits are proven, not assumed. runc runs rootless as uid 0, which can leave cgroup limits silently unapplied; a test proves memory, CPU, and pid limits actually bind.

Chromium still needs one exception, scoped to the surface. Its namespace sandbox creates user, PID, and network namespaces, which the standard allowlist denies, and seccomp is inherited by every renderer. Terminal images keep the denial. Xpra images allow exactly the namespace creation Chromium's sandbox uses, with user.max_user_namespaces inside the workload set to what it needs. That replaces the v0.7 setuid helper and its allowlist rail to restore the 4755 bit, which existed only because our kernel had user namespaces off. Disabling Chromium's sandbox was never the strategy.

File ownership is part of the runtime contract.

Today everything works because the guest runs as root and root ignores file permissions. On KVM, the workspace filesystem reports every file as uid/gid 0 (crates/capsem-core/src/hypervisor/fuse/mod.rs) so that tools such as git's ownership check accept their own files, and chown has no ownership effect. Under a user namespace, anything owned outside the mapped range appears as nobody inside the container: the image's user could not write its own workspace, and git would reject it.

The contract fixes one uid/gid mapping (for example, container 0–65535 to VM 100000–165535) and requires every filesystem the workload sees to present files in that range:

  • Workspace on KVM. The FUSE server, which we own, reports the image user's mapped uid instead of 0.
  • Workspace on Apple VZ. Ownership comes from Apple's VirtioFS server, which we do not control. The mapping is met on the guest side, through the user namespace's uid map or an idmapped mount; which works is measured first.
  • Unpacked roots. umoci applies the mapping at unpack.
  • Prebuilt EROFS roots. The image CI bakes the offset in with mkfs.erofs --uid-offset and --gid-offset, so the contract's mapping is part of the published .erofs.
  • Persistent volumes. Created and owned inside the mapped range.

The proof runs on both backends alongside the Chromium sandbox proof, before anything else builds on the user namespace: clone, commit, and rebase with git; create a private 0600 file and read it back; edit through the workspace from inside and outside the VM; restart a named session and repeat.

Surfaces use the existing publication path. Nothing new touches vsock.

The container keeps its AF_VSOCK denial, and there is no new transport component. An image's GUI surface is a port on the container's loopback, declared by its surface label. At create, the service grants that port as an exposure. The guest port bridge already enters the container's network namespace and connects to its loopback (crates/capsem-agent/src/port_bridge/setup.rs), the security engine already admits exposures, and the gateway's preview route already relays authenticated WebSocket upgrades (crates/capsem-gateway/src/preview.rs, crates/capsem-router/src/preview.rs). Xpra binds the declared port, and the gateway serves the pinned xpra-html5 client.

This works the same on Apple VZ and KVM. The v0.7 relay depends on host-initiated vsock connections, which only the VZ backend has; KVM would need new hypervisor code for it. Authorization stays outside the container: the security engine admits the exposure, and an image cannot widen it.

Workloads reach MCP over HTTP at the host proxy, and profiles are not deleted until they do.

Today's in-guest MCP server talks vsock, which the container cannot use. The agent's MCP configuration instead points at a fixed internal name, which the host proxy routes to the MCP gateway. No Capsem binary goes into any image, it works for every image, and policy and credentials stay on the host. The route is added to the current proxy and moves with it when #206 extracts the proxy, so it does not wait on #206.

This is an explicit milestone, not a deferral: deleting profiles removes today's agent MCP path, so profile deletion is blocked until workload MCP ships and is proven with a real agent.

Xpra is an image concern within a stable Capsem display contract.

A Claude Desktop image starts Xpra, its private display and D-Bus session, its Secret Service, and Claude as an unprivileged user. It imports Capsem's CA, which the runtime already mounts, into any application trust store that needs it (Chromium's NSS database did on v0.7).

Capsem supplies the HTML Xpra client and owns authenticated display access, input permissions, clipboard and file-transfer policy, teardown, and protocol compatibility. Capsem supports two surfaces, terminal and Xpra, without learning how to install or launch any vendor's application.

Image metadata describes needs; policy grants them. Standard OCI fields carry what they already express: entrypoint, command, environment, USER, and VOLUME. A small, versioned label set carries the rest: the surface (terminal, or Xpra and its port), the runtime contract version the image targets, and a minimum memory. That is the whole list; a package-based profile DSL does not come back as labels.

Images reach the VM through verified bytes, with a prebuilt EROFS as the fast path.

Today every launch, restarts included, copies each layer into the workspace share, copies it again in the guest, re-verifies it, unpacks every layer with umoci, and deletes the result on exit. For a large GUI image that work dominates startup. Delivery becomes two tiers, and in both the host only ever hands the guest bytes it has verified:

  • Universal path, for any image. The host shares the image's verified OCI layout read-only, containing only that image's blobs and never the whole cache. The guest unpacks it once. Named sessions keep the unpacked root across restarts instead of deleting it, and a restart re-stages from the host cache, never from the user-writable workspace stage. This covers external and administrator images with no extra publishing.
  • Fast path, for official images. The image CI builds a compressed .erofs per image and architecture from the exact image digest, publishes it to GHCR next to the image, and the catalog pins its digest next to the image's and the OBOM's. The host downloads it, checks the digest, and attaches it as a read-only disk that runc uses as the container root. This removes the per-launch unpack, and one copy on disk serves every session using that digest. The first download and the application's own startup still grow with the image. The runtime itself already boots from EROFS.

Clients never build an .erofs. Producing one means flattening, compressing, and tuning it, which belongs in the image CI, not on every user platform. An image without a published .erofs takes the universal path.

The host blob cache is namespaced per repository, so layers shared between images are stored once in the registry but not on the user's disk. Its budget (4 GiB warm, 8 GiB maximum, one-week age limit) was sized for a single image and is revisited once a GUI image and the debug image share a machine. A named session whose image was evicted re-pulls it by digest.

Declared state is VOLUME, and resources follow the VM.

  • State. An image's VOLUME paths are its declared application state, and they live on the VM's ext4 system overlay. VirtioFS has no per-uid ownership, so it cannot hold state owned by a mapped uid. In an ephemeral session they die with the VM; in a named session they persist with the overlay, which --from already clones. This replaces today's 64 MiB tmpfs per volume, which is right for neither case. The staged-launcher shield stays as it is.
  • Resources. The workload gets the VM's --ram and --cpu minus a fixed runtime reserve. That replaces the hardcoded 256 MiB and one CPU, and the reserve keeps Capsem's own services alive when the workload exhausts its cgroup. Create refuses an image whose declared minimum exceeds what the session can give it.
  • Mounts. GUI images get /dev/shm (v0.7 needed it) and writable home through VOLUME. Development images get build and cache locations the same way. There is no general host-path mount and no privileged-container escape hatch.

Administrators control where images come from and which images run, separately.

Two rules, checked at different times:

  • Sources say where bytes may be fetched from: registries, repository paths, and mirrors. They are checked before any registry access.
  • Admission says which images may execute, by repository or by digest. It is checked after resolution, before staging or execution.

An administrator who mirrors official images allows the mirror as a source; the official digests are admitted wherever they were fetched from, because admission is by digest. That is how the enterprise-mirror case works without extra rules.

Scope Illustrative selector
Entire registry registry.company.com
Repository subtree ghcr.io/company/agents/**
Exact repository ghcr.io/company/agents/codex
Exact immutable image ghcr.io/company/agents/codex@sha256:…

The syntax is illustrative; the matching semantics are not. Selectors match normalized registry authorities and whole repository path components. Each case below has a test before the matcher is trusted:

  • a subtree does not accept a sibling (agents/** does not match agents-evil/x);
  • registry.company.com.evil.example does not match registry.company.com;
  • docker.io, index.docker.io, and the implicit library/ prefix normalize to one identity;
  • an explicit port is part of the authority (registry.company.com:5000 is a different registry);
  • authority case folds, path case does not, and ./.. components are rejected;
  • in repo:tag@sha256:…, the digest wins and the tag is ignored.

Today admission runs only before the pull, on the digest the reference spells out, which is none for a tag, and it runs inside capsem-process per VM. The post-resolution digest check is new, and admission moves into the service so capsem images can answer without a VM. One check covers catalog names, explicit references, cached images, restart, and fork. Anything outside the effective policy is denied.

The default policy admits the catalog's supported digests, not a registry path. With no enterprise policy installed, exactly the digests the catalog lists as supported may run, wherever they were fetched from. Admitting "anything under Capsem's repository" would also admit the debug image, test pushes, and every image ever published there. External sources and images need an explicit grant.

Tags are for selection; sessions pin digests. Every session records the resolved digest and platform. Restart uses that record and never follows a moved tag. Starting a stopped session rechecks current policy, which a catalog update does not change unless it revokes that digest. Revoking a running session is an explicit lifecycle action, never a side effect of a catalog refresh.

Session behavior is the same for every image.

  • Create resolves, admits, pulls, and starts the image in its VM.
  • Open connects to the declared surface once the workload is ready.
  • Exec and files target the workload and its workspace. Today the agent runs commands in the VM, not the container, so routing exec and the terminal into the workload is an API change with ledger implications, specified on its own. Runtime diagnostics stay with capsem-doctor through the service, and capsem logs carries runc and umoci output so a failure names its layer.
  • Restart relaunches the workload from the pinned digest and persistent volumes. This is new: today the launch record is read only to report status, and nothing relaunches a workload when a named VM starts.
  • Fork copies the pinned digest and the persistent volumes, then mints fresh transport and browser grants. Capsem's credential grants are not copied. Application logins stored inside a volume, such as Claude's keyring, are user state and travel with the fork; the fork confirmation says so. The keyring's per-VM unlock secret travels with the volume, or the copy cannot be opened.

--from and --image are mutually exclusive today. That restriction is removed: every session has an image, so every fork has one.

Build it by adaptation, not reinvention.

The tree already has host OCI pulls, verified per-repository caching, guest unpacking, container execution, networking, the port bridge and preview publication, workspace integration, the OBOM pipeline, and lifecycle tests. v0.7 has the Claude/Xpra packaging. Every step below adapts one of those owners. "An afternoon" is the yardstick for that discipline, not an estimate: a step that cannot land as a focused change to an existing owner is a sign the design is reaching for a new subsystem, and the answer is to stop and rethink, not to build one. No new engine, no new transport, no wholesale merge of v0.7.

Restart, exec routing, service-side admission, and the workload MCP route are new code. They are called out where they appear rather than presented as reuse.

Order Focus Adapts Outcome
0 Contract proofs kernel defconfigs, launch.py, FUSE server User namespaces on; the ownership contract holds on KVM and VZ (git, private files, editing, restart); Chromium's namespace sandbox runs under the per-surface seccomp table
1 Minimal runtime and debug image rootfs template, launch.py, config/gate.toml The runtime owns its launcher tools and has no profile inputs; VM-sized resources, volumes on the system overlay, images from a read-only layout share; just test runs on the pinned capsem-debug
2 Catalog and admission OCI puller, container_setup.rs, security engine The versioned catalog is fetched as an OCI artifact; sources and admission are separate checks in the service, before and after resolution; images, named create, and explicit-image create share one resolver
3 Lifecycle container_setup.rs, agent exec path Restart relaunches from the pinned digest; exec and terminal reach the workload; fork works for image sessions
4 Workload MCP host proxy, MCP gateway Agents in any image reach Capsem MCP over HTTP at the host proxy, proven with a real agent
5 GUI image v0.7 packaging, port bridge, preview route The Claude image's declared port is published through the preview route with the xpra-html5 client; the GUI image joins the runtime's pinned qualification set
6 Profiles deleted settings, corp overlay, builder, release lanes Blocked on step 4. Policy lives in built-in defaults and settings; profiles, the profile build machinery, and release-profile are removed

Off the critical path and parallel to all of it: the images/ workflow, the shared base and official images, OBOM publication, and the prebuilt .erofs fast path.

The full change also needs official image publication wiring, desktop discovery, and platform qualification. A terminal container starting does not complete the migration. Image publication goes through the image CI, and runtime rollout goes through the release process, each after its own qualification. Track the work in Sprinty, in an isolated worktree.

The v0.7 work boots Claude Desktop in UI mode and renders it over the authenticated gateway, with a restart back to an authenticated home. Still open: frontend integration, main-window placement (the window arrived off-viewport), sustained responsiveness, Codex GUI compatibility, and Linux/x86_64 qualification.

Acceptance is observable.

  • A user discovers an approved application, creates it by name or reference, works through its declared surface, and restarts or forks it with the expected state.
  • A catalog update never stops an existing session unless it revokes that session's digest.
  • A disallowed image fails through every entrypoint, cached images included.
  • A workload cannot reach an undeclared port or channel, does not run as VM root, and cannot create namespaces unless its surface requires it. Resource limits are shown to bind.
  • The workload's user owns its workspace, volumes, and root on both backends; git, private files, editing, and restart work.
  • Agents in any image reach Capsem MCP before profiles are removed.
  • The runtime contains no application stack and no profile inputs. just test builds no application environment, and no image build can block, delay, or redden the main build.
  • A runtime candidate passes with the pinned published images, including the GUI image, and new images pass on the released runtime for their contract.
  • Official images have published digests, OBOMs, and provenance, and every client resolves through the same catalog and check.
  • First-create and restart time, with and without a prefetched image and with and without the prebuilt .erofs, are measured against today's boot rather than assumed.

The security gain is a smaller common runtime that is easier to qualify, and less authority for application code. Containers still share the guest kernel with Capsem's services, and user namespaces expose more of that kernel to the workload; the VM and host-side enforcement remain the outer boundary. Footprint and startup improvements get measured, not assumed.

Source basis.

  • OCI orchestration and launch record: crates/capsem-service/src/container_setup.rs.
  • Workload restrictions and staging: guest/artifacts/container/launch.py.
  • KVM workspace ownership: crates/capsem-core/src/hypervisor/fuse/mod.rs.
  • Port bridge into the container namespace: crates/capsem-agent/src/port_bridge/setup.rs.
  • Preview publication and WebSocket relay: crates/capsem-gateway/src/preview.rs, crates/capsem-router/src/preview.rs.
  • Guest kernel configuration: config/docker/image/kernel/defconfig.{arm64,x86_64}.
  • Profile packages and policy being moved or removed: config/profiles/, config/docker/Dockerfile.rootfs*.j2.
  • OCI cache budget: config/cache.toml (stages.oci-images).
  • Create arguments: crates/capsem/src/create_command.rs.
  • OBOM generation and validation: build_system/builder/image/docker.py, build_system/builder/release/obom.py.
  • Container acceptance: tests/ironbank/kingslanding/.
  • GitHub Packages permissions.
  • mkfs.erofs uid/gid offsets.
  • Claude/Xpra evidence on v0.7.
  • v0.7 GUI design and remaining qualification.

This proposal changes how applications are selected and distributed. It does not remove the VM, replace the host security engine, implement a registry server, or introduce multi-container orchestration.

Activity

  1. ebursztein commented on Oct 5, 2026

    @ebursztein
    CollaboratorAuthor

    0.7 handoff: macOS done, Linux takes over (head 5cddacba4)

    Plan: skip 0.6.4 and release 0.7.0 from branch 0.7 once the Linux lanes pass. Everything below is on origin/0.7.

    Proven on macOS (Apple VZ, arm64)

    What Evidence
    Installed signed .pkg in Tart: install, receipt, binary cohort, channel verification, asset repair, reinstall over live helpers. Automatic update activates a metadata-only republish; tampered and incompatible candidates are rejected and audited. focus-test install --slow green on 78409bfb7, with every step run
    Physical-VZ installed product: winterfell boots the pinned reference image through the installed gateway (workspace write as uid 1000, stop/resume relaunch, fork); installed doctor; managed restart same run: winterfell 2/2, doctor 202 passed
    Reference-image sessions (official dev @ sha256:64729596…): exec reaches the workload with explicit and omitted targets, a dead workload is refused (never falls back to the VM) and reported exited, interactive images run on a held terminal with detach/reattach replay focus-test kingslanding green on 78409bfb7 (57 passed, including the 5 reference-session tests)
    Shared image qualification (capsem-gate image-qualify, tests/qualification/) on the reference image: runtime basics, MCP transport, lifecycle, toolchain, ollama 13/13 on a19327779; capabilities the image doesn't declare are deselected, and any skip fails the run
    Copy-on-write session layer: a write to the workload root survives stop/resume, is carried by a fork, and survives create --from SESSION --image OTHER on 5cddacba4: focus-test kingslanding 58 passed (incl. test_a_session_that_changes_image_keeps_its_layer), image-qualify 13 passed, 0 skipped

    Fixed on the way (all on 0.7, CHANGELOG updated)

    • Interactive images lost their workload in detached sessions. The image's command now runs on a held PTY, and an exited workload is reported with its exit code (a61eec359, 4ed815ecc, 3ac214f80).
    • The installed proofs booted no OCI image. They now boot the pinned reference image (83e350ee0).
    • Release tooling still read profile-era manifest status and asset paths (c62bab863, ddfa7c8d3).
    • capsem update ignored metadata-only republishes. It fetched them and never installed them; now it installs them. A candidate that rewrites runtime digests without a new runtime revision is rejected as a digest mismatch (cd4ea58ca, 16a3c216e, 78409bfb7).
    • Workload homes were read-only. The root was mounted read-only, so agents couldn't write $HOME. It is now a session-owned copy-on-write layer above the read-only image: a named session keeps it, a fork copies it, and it survives an image change (0b3c8ae4d, 5699c0c6d). Volume seeding no longer follows links. Docs: What Makes Capsem Unique (5cddacba4).
    • A fork was reported running before its workload ran. The clone carried the source's live markers; a clone and a cold resume now clear them, and a warm restore keeps them (8f9a609bc).

    What Linux must do

    1. Pin an amd64 reference image. config/gate.toml [functional.reference_image] pins linux/arm64 only, so every image session on x86 fails hard (by design; it never skips). On an x86 host, build the official dev for linux/amd64 (capsem-gate reference-image) and pin its digest. Publishing it to ghcr needs Elie's OK.
    2. Run the KVM proofs (S01-005 / S01-007): just focus-test kingslanding on a /dev/kvm host, covering workspace ownership and stop/resume of reference sessions. The session layer on KVM is new and has not run there yet.
    3. Run capsem-gate image-qualify on KVM, for arm64 and amd64.
    4. Run the Linux installed proof (glowup.install, Docker/systemd). It needs CAPSEM_REFERENCE_IMAGE_LAYOUT; run_installed_winterfell.py --image-layout is required.
    5. Final: just test <head> on the release head, then the release commands (RELEASE.md).

    Not done (tracked in Sprinty S11 D1–D4)

    • Official application images (claude-code, codex-cli, agy, claude-desktop) have qualify.toml manifests but no qualification run yet. Their layouts must be built locally first.
    • Remaining qualification groups: the agy broker turn, a GUI frame test, negative controls, an images.yaml qualify job with per-image promotion, and an upgrade-from-0.6 contract test.
  2. ebursztein commented on Oct 5, 2026

    @ebursztein
    CollaboratorAuthor

    Approved implementation sequence: 0.7 runtime → clients → Inspect

    Each linked issue records dependencies, acceptance and PR boundaries. Work starts from origin/0.7 in isolated branch integration/0.7-clients-inspect; main and the active release are left untouched. #291 is the carried Inspect source, preserving Pierre’s authorship.

    Sprinty records implementation and exact verification evidence. Runtime qualification precedes dependent client/Inspect work. Packages publish only through verified release workflows after clean-install acceptance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions