Cocoon: Capability-Delimited Service Capsules for Redox
Notes on the design of a service deployment tool that sits between package management and runtime enforcement on RedoxOS. The project is at an early stage — P0, in our own terminology — and makes no claims about runtime isolation that it cannot yet back up with evidence. What follows is an account of the thinking behind it, not a finished-product announcement.
1. The Problem
RedoxOS has an architectural property that most operating systems lack: its scheme/resource model makes it possible, in principle, to give a process a narrow, explicitly declared view of the system. A network daemon can be started with access to only the TCP scheme and a specific log target. A file indexer can be given read-only access to a single directory tree. The capability boundary is first-class in the kernel, bolted on after the fact through SELinux policies or container isolation layers as one must do on Linux.
The trouble is that there is, at present, no convenient mechanism for declaring and enforcing these boundaries for long-running services. Redox has pkg and pkgar for installing files, which do their job perfectly well. But pkg does not know about schemes, or preopened handles, or permission rules, or the difference between installing a font and installing a network-facing daemon that should never be allowed to touch /etc/secrets. It installs bytes. What happens after the bytes land is someone else’s problem.
Cocoon is an attempt to fill the gap between “install files” and “run a service with declared authority.” It is not Docker. It is not a container runtime. It is not a replacement for pkg. It is, we hope, a relatively thin layer that takes a declared service manifest, verifies it, records what was declared, and — when Redox runtime support is ready — constructs the namespace and capability handles that enforce the declaration.
We are under no illusions that this is a solved problem. P0, which is what exists today, defines and verifies capsule intent. It does not enforce runtime isolation. That comes later, and we are honest about the gap.
2. The Three-Layer Model
The design rests on a deliberate separation of concerns that we think is worth stating explicitly, because getting it wrong would turn Cocoon into a general-purpose package manager, which it very much is not.
flowchart TD
classDef layer fill:diagramSurface,stroke:diagramBorder,stroke-width:1.5px,color:diagramText
PKG["pkg / pkgar<br/>Payload Layer<br/>Install bytes, content hashes,<br/>dependencies, relocatable payloads"]:::layer
COCOON["Cocoon<br/>Authority Layer<br/>Typed manifests, permission diffs,<br/>runtime plans, receipts, rollback"]:::layer
REDOX["Redox Namespace / fd Capabilities<br/>Enforcement Layer<br/>Scheme visibility, preopened handles,<br/>process authority boundaries"]:::layer
PKG --> COCOON
COCOON --> REDOX
pkg/pkgar installs files. It handles content hashes, dependencies, repository management, and relocatable installation. This is mature infrastructure and we have no intention of replacing it.
Cocoon installs authority. It takes a service manifest that declares what a service may do — which schemes it can see, which paths it may read or write, whether it may open network connections — and records that declaration in a verifiable, auditable form. It diffs authority between versions. It generates receipts for every install. It produces a runtime plan that specifies exactly what the Redox namespace should look like.
Redox namespace and fd capabilities enforce the declared authority at runtime. This is the kernel’s job, not Cocoon’s. Cocoon constructs the plan; Redox enforces it.
The boundary is strict. Cocoon must not reimplement dependency solving, package repositories, whole-system updates, or general app-store workflows. Any feature that needs payload packaging must first ask how to express or delegate it through pkg/pkgar. This is not an optimisation or a future preference. It is a hard design rule.
3. Capsules and Manifests
A Cocoon capsule is an archive containing a service payload, a typed manifest, hash metadata, and (eventually) a signature. The manifest — Cocoon.toml — is the heart of the design. It declares:
- The capsule identity (name, version, authors, licence).
- The entry point: which binary to run, from which working directory, with what arguments.
- The filesystem layout: a guest root (defaulting to
/app), writable and read-only subtrees, and explicit deny rules. - Permission rules: typed declarations of what the service may do, each with an effect (allow or deny), a scheme, an action, and a target.
- Preopened handles: which host paths are mapped to which guest paths, with what rights (read, write, execute).
- Scheme visibility: which Redox schemes the service can see, and at what level (hidden, read-only, read-write).
- Network default: deny or allow. The default is deny.
- Resource limits: memory, process count, open file descriptors.
- Update policy: whether updates must be signed, whether rollback is supported, whether permission expansion requires confirmation.
- Audit configuration: whether to capture stdout, stderr, and audit events.
A few design choices deserve explanation.
Deny-by-default networking. A capsule that does not explicitly declare network access gets none. This is not a policy that can be overridden by the service binary; it is a property of the manifest, and the runtime plan is derived from the manifest, not from the service’s runtime behaviour.
Explicit deny rules. A capsule may declare allow file readwrite /app/** and simultaneously deny file readwrite /etc/secrets/**. The deny rules are not advisory. They are part of the authority surface and are included in permission diffs. This is a pattern that we think is underused: default-allow within a confined subtree, but with hard deny boundaries for sensitive paths.
deny_unknown_fields on every struct. The manifest parser rejects unknown TOML fields. A typo like schtme instead of scheme produces a parse error, not a silently ignored permission. This is a small thing, but we have seen enough misconfigured services in production to believe that it matters.
4. Typed Domain Values
One of the earlier and, we think, more important decisions was to represent every security-sensitive concept as a validated newtype rather than a raw string.
flowchart LR
classDef ty fill:diagramSurface,stroke:diagramBorder,stroke-width:1.5px,color:diagramText
RAW["Raw String"] -->|"parse()"| NAME["CapsuleName"]:::ty
RAW -->|"parse()"| VER["CapsuleVersion"]:::ty
RAW -->|"parse()"| GUEST["GuestPath"]:::ty
RAW -->|"parse()"| CAP["CapsulePath"]:::ty
RAW -->|"parse()"| PERM["PermissionTarget"]:::ty
INVALID["Invalid input"] -->|"parse()"| ERR["Error<br/>(rejected at boundary)"]:::ty
style INVALID fill:diagramDangerBg,stroke:diagramDanger,stroke-width:1px,color:diagramDangerText
style ERR fill:diagramDangerBg,stroke:diagramDanger,stroke-width:1px,color:diagramDangerText
CapsuleName validates that the name is lowercase ASCII, starts with a letter, and contains only letters, digits, hyphens, underscores, and dots. CapsuleVersion validates through semver::Version::parse. GuestPath validates that the path is absolute, contains no .. components, no //, and no control characters. CapsulePath validates that the path is relative and normalised.
The principle is straightforward: invalid data should not be representable in the type system. A CapsuleName that exists has already been validated. A GuestPath that exists cannot contain ... This is not novel — it is the “parse, don’t validate” pattern, familiar from Haskell and Rust practice — but it is surprisingly effective at preventing the class of bugs where a path traversal or a malformed identifier slips through a validation boundary and causes trouble elsewhere.
The cost is some boilerplate. The benefit is that every function that accepts a GuestPath can reason about it without re-validating. We think the trade-off is worthwhile for security-sensitive code.
5. The Permission Diff
Perhaps the most practically useful feature of Cocoon, at least in its current form, is the authority diff between capsule versions.
Consider a service that is currently running version 0.1.0, and an update to 0.2.0 is available. Before installing the update, Cocoon can compare the authority surfaces of the two versions and produce a structured report:
Authority changes detected:
Added permissions:
HIGH allow tcp connect api.example.com:443
MEDIUM allow file readwrite /app/cache/**
Modified permissions:
LOW allow log read service-log -> allow log write service-log
Removed permissions:
LOW allow file read /app/assets/**
Modified schemes:
HIGH log readonly target=service-log -> log readwrite target=service-log
Confirmation required: yes
The severity classification is straightforward. Device, kernel, sudo, and sys permissions are critical. Network access and access to paths containing /etc/secrets or /home are high. General file access is medium. Everything else is low.
The key design decision is that only expansions of authority trigger confirmation. Adding a deny rule is a reduction. Removing a permission is a reduction. Narrowing scheme visibility is a reduction. These are displayed for information but do not require human approval. Adding a new TCP connection, widening a scheme from read-only to read-write, or adding a preopen — these are expansions, and they require confirmation if the severity meets the threshold.
flowchart LR
classDef change fill:diagramSurface,stroke:diagramBorder,stroke-width:1.5px,color:diagramText
classDef yes fill:diagramWarningBg,stroke:diagramWarning,stroke-width:1px,color:diagramWarningText
classDef no fill:diagramSuccessBg,stroke:diagramSuccess,stroke-width:1px,color:diagramSuccessText
OLD["v0.1.0<br/>Authority Surface"]
NEW["v0.2.0<br/>Authority Surface"]
OLD --> DIFF["Authority Diff"]
NEW --> DIFF
DIFF --> ADDED["Added permissions<br/>(severity-ranked)"]
DIFF --> MODIFIED["Modified permissions<br/>(expanded vs narrowed)"]
DIFF --> REMOVED["Removed permissions<br/>(informational)"]
DIFF --> SCHEMES["Scheme visibility changes"]
DIFF --> PREOPENS["Preopen changes"]
ADDED -->|"expansion?"| CHECK{"Severity >=<br/>threshold?"}
CHECK -->|"yes"| CONFIRM["Confirmation Required"]:::yes
CHECK -->|"no"| AUTO["Auto-approved"]:::no
REMOVED -->|"always reduction"| AUTO
class ADDED,MODIFIED,SCHEMES,PREOPENS change
We are aware that this model has limitations. It does not capture the interaction between permission changes — a new file read permission might be harmless on its own, but dangerous in combination with a new network connect permission that exfiltrates the read data. Analysing permission interactions is a harder problem, and we have not attempted it here.
6. Bundle Verification
Before any capsule is installed, Cocoon verifies its integrity. The verification pipeline is a sequence of checks, each producing a distinct issue if it fails:
flowchart TD
classDef pass fill:diagramSuccessBg,stroke:diagramSuccess,stroke-width:1px,color:diagramSuccessText
classDef fail fill:diagramDangerBg,stroke:diagramDanger,stroke-width:1px,color:diagramDangerText
classDef step fill:diagramSurface,stroke:diagramBorder,stroke-width:1.5px,color:diagramText
PARSE["Parse archive<br/>(path safety, size limits)"]:::step
HASH["Verify hash algorithm<br/>(must be BLAKE3)"]:::step
PAYLOAD["Verify payload hashes<br/>(every file matches)"]:::step
MANIFEST["Verify manifest hash<br/>(TOML content matches)"]:::step
ENTRY["Verify entrypoint<br/>(cmd exists, is executable)"]:::step
SIG["Verify signature<br/>(strict vs unsigned)"]:::step
PARSE --> HASH --> PAYLOAD --> MANIFEST --> ENTRY --> SIG
SIG -->|"unsigned, default mode"| WARN["Warning: unsigned capsule"]:::pass
SIG -->|"unsigned, strict mode"| BLOCK["Blocked: unsigned capsule"]:::fail
SIG -->|"valid signature"| OK["Verified"]:::pass
PAYLOAD -->|"hash mismatch"| FAIL1["Integrity failure"]:::fail
ENTRY -->|"missing or not executable"| FAIL2["Integrity failure"]:::fail
Every payload file in the archive must have a corresponding BLAKE3 hash in the manifest. Every hash in the manifest must correspond to a file in the archive. Archive paths must be relative, normalised, unique, and free of traversal sequences. The entry point binary must exist and have executable mode bits set.
The bundle parser also defends against pathologically malicious archives: there are hard limits on file count (10,000), individual file size (10 MB), total uncompressed size (100 MB), and path depth (256 components). A counting reader wrapper tracks decompressed bytes and errors out if the limit is exceeded, which prevents gzip bombs from consuming unbounded memory.
We have fuzz targets for both the path sanitiser and the full bundle parser. Fuzzing is not a substitute for a correctness proof, but it has been effective at catching edge cases in the path normalisation logic that our unit tests missed.
7. Staged Installation and Receipts
The install process is designed to be atomic and auditable.
sequenceDiagram
autonumber
participant CLI as cocoon install
participant LOCK as Advisory Lock
participant STAGE as Staging Directory
participant VER as Verified Bundle
participant FINAL as Version Directory
participant CUR as Current Symlink
participant RCPT as Receipt File
CLI->>VER: Verify capsule bytes
CLI->>LOCK: Acquire exclusive lock
CLI->>STAGE: Materialise payload (temp dir)
VER-->>STAGE: Payload files with modes
CLI->>RCPT: Build install receipt
CLI->>FINAL: Atomic rename (stage -> version dir)
CLI->>RCPT: Write receipt (temp file + atomic rename)
CLI->>CUR: Update current symlink (temp link + atomic rename)
CLI->>LOCK: Release lock
Note over FINAL,RCPT: Install tree: capsules/hello-service/ versions/0.1.0/ current receipts/0.1.0.json
Installation proceeds into a temporary staging directory. Only after the payload has been fully materialised and the receipt has been built does the staging directory get atomically renamed to its final location. On Unix, fs::rename is atomic on the same filesystem, which means that a crash at any point before the rename leaves no partial state. A crash after the rename leaves a fully installed, consistent version.
An advisory file lock prevents concurrent installs of the same capsule from interfering with each other. We are aware that advisory locks are not robust against all failure modes (a crashed process does not necessarily release its lock on all platforms), but they are sufficient for the common case and avoid the complexity of a proper lock manager.
The install receipt records the capsule name, version, bundle hash, manifest hash, permission hash, install root, timestamp, and a link to the previous receipt. Each receipt’s body_hash is computed from the canonical body JSON, excluding the body_hash and signature fields, so the receipt is not self-referential. The previous_receipt field chains receipts together, forming an audit trail:
flowchart LR
classDef rcpt fill:diagramSurface,stroke:diagramBorder,stroke-width:1.5px,color:diagramText
R1["Receipt v0.1.0<br/>manifest_hash: a1b2...<br/>permission_hash: c3d4...<br/>body_hash: e5f6..."]:::rcpt
R2["Receipt v0.2.0<br/>manifest_hash: 7a8b...<br/>permission_hash: 9c0d...<br/>body_hash: e1f2...<br/>previous_receipt: e5f6..."]:::rcpt
R2 -.->|"previous_receipt"| R1
We think of this as a poor man’s Merkle tree — not as efficient, not as cryptographically elegant, but simple enough to implement correctly and audit by hand. A more sophisticated chaining mechanism might replace it in the future, but for now, we prefer the design that we can reason about confidently.
8. The Runtime Plan
The runtime plan is the bridge between the capsule manifest and the Redox namespace. It is a normalised, verified statement of what the service intends to do at runtime: entry point, scheme visibility, preopened handles, permission rules, and stdio configuration.
The plan is derived from a verified bundle, not a raw one. This is a small but deliberate choice: you cannot produce a runtime plan from a capsule that has not passed integrity verification. The type system enforces this — RuntimePlan::from_verified_bundle takes a VerifiedBundle, which can only be obtained after the verification pipeline has passed.
P0 produces the plan but does not execute it. P1, which we are working towards, will take the plan and use it to construct the actual Redox namespace: set up scheme visibility, pass preopened file handles, spawn the service process, capture its output, and verify that denied access actually fails. This is where the design either works or does not — everything up to this point is bookkeeping.
9. What Is Honest About
We think it is important to be clear about what Cocoon does not do, at least in its current state.
It does not enforce runtime isolation. P0 defines and verifies capsule intent. It checks manifests, verifies hashes, diffs permissions, stages installs, and writes receipts. But the service binary, once launched, runs in whatever environment the host operating system provides. On macOS, that means no enforcement at all. On Redox, once P1 is complete, enforcement will come from the kernel’s namespace and capability machinery, not from Cocoon.
It does not sign capsules yet. The signature metadata is a placeholder. cocoon verify in default mode will warn about unsigned capsules but will not reject them. Strict mode rejects them. The signing infrastructure — key management, signature algorithms, revocation — is a non-trivial problem that we have deferred.
It does not solve the general problem of software supply chain security. Cocoon verifies that a capsule’s contents match its declared hashes, and it records what was installed and when. But it does not verify the provenance of the source code, the integrity of the build pipeline, or the trustworthiness of the capsule author. These are harder problems, and we do not wish to pretend that hash verification is a substitute for them.
10. Why Redox
A reasonable question is why this work targets Redox rather than, say, Linux. The answer is that the capability model is first-class in the Redox kernel in a way that it is not in Linux.
On Linux, one can approximate capability security through a combination of namespaces, cgroups, seccomp filters, SELinux policies, and container runtimes. This works, more or less, but the resulting configuration is spread across multiple subsystems, each with its own syntax, semantics, and failure modes. The mapping between “what I declared” and “what is enforced” is opaque and difficult to audit.
On Redox, the capability boundary is expressed through scheme visibility and preopened file descriptors, which are concepts that the kernel understands natively. A process that does not have a handle to the TCP scheme cannot open a network connection. A process that does not have a handle to a file path cannot read or write it. The enforcement is at the syscall boundary, not in an optional security module loaded after the fact.
This is not to say that Redox is more secure than Linux in all respects — it is a younger system with a smaller developer community and less battle-testing. But the architectural alignment between Cocoon’s manifest declarations and Redox’s enforcement mechanisms is, we think, genuinely cleaner than the equivalent on Linux. Whether this cleanliness translates into practical security is, of course, an empirical question.
Concluding Remarks
Cocoon is an attempt to bring the principle of least privilege to service deployment on RedoxOS, in a way that is auditable before installation and verifiable after. The design is deliberately narrow: it does not try to be a general-purpose package manager, a container runtime, or an orchestration system. It tries to do one thing — declare, verify, and record service authority — and to do it well enough that the runtime enforcement layer can trust it.
We are at the beginning of this work, not the end. P0 is a foundation: typed manifests, bundle verification, permission diffs, staged installs, and receipts. P1 will prove the runtime path on Redox/QEMU. P2 will align the payload format with pkgar. Each phase has explicit acceptance criteria, and we are not in the habit of declaring victory before the tests pass.
The code is MIT-licensed and available for inspection. We would be particularly interested in feedback from people with experience in capability-based operating systems, Redox internals, or the security of software supply chains.