Runtime standardisation for decentralised apps on the Atmosphere
at-run docs architecture.md
10.0 kB
Markdown
at main

at-run beta V2 architecture #

Goals #

at-run executes code that is owned by an AT Protocol account. A deployment is not a copy uploaded to a runner: it is a record and blob in the author's PDS. Every execution begins by resolving that record from the network.

V2 also makes runtimes public, content-addressed configuration. Runner operators do not map names such as python or javascript to local images. A code blob's MIME subtype identifies the exact runtime-record CID that describes how to run it.

Authority boundaries #

Code authors control #

  • code records and code blobs in their PDS;
  • a canonical runtime name used for discovery and display;
  • a strong reference to an exact public runtime record;
  • an entrypoint inside their code artifact;
  • optional, digest-pinned addition images; and
  • requested capabilities and resource limits.

Runtime publishers control #

  • public runtime records in their PDS;
  • platform-specific, digest-pinned OCI artifacts;
  • the language adapter that implements the at-run worker protocol; and
  • relative launch commands and environment additions inside the sandbox.

Runner operators control #

  • the fixed sandbox image and isolation backend;
  • accepted protocol versions and platforms;
  • publisher, registry, size, network, and resource policies;
  • the fixed supervisor command; and
  • whether execution requires stronger isolation such as gVisor, Kata, or a disposable VM.

A runtime or addition image selected by a network record is never started. Its fixed export directory is read from a stopped container and copied into a volume. Only the runner-owned sandbox image is started.

V2 record families #

V2 uses a distinct namespace and does not modify existing beta records:

dev.mainasara.at-run.beta.v2.code
dev.mainasara.at-run.beta.v2.runtime
dev.mainasara.at-run.beta.v2.alias
dev.mainasara.at-run.beta.v2.runner
dev.mainasara.at-run.beta.v2.execution
dev.mainasara.at-run.beta.v2.job
dev.mainasara.at-run.beta.v2.secrets

The JSON files continue to declare "lexicon": 1; that is the AT Protocol Lexicon format version. beta.v2 is the application schema version.

Runtime binding #

The code record contains all three parts of the binding:

runtime              canonical human-readable name
runtimeRecord.uri    location used to retrieve the public record
runtimeRecord.cid    immutable identity of that record
blob.mimeType        application/<runtimeRecord.cid>

The URI is necessary because AT Protocol does not provide generic record lookup by CID alone. The runner fetches the URI and requires its returned CID to match both the strong reference and the code blob's MIME subtype.

The canonical name is never execution authority. Two records may use the same name; the CID determines the exact runtime configuration.

Cross-field equality cannot be expressed by Lexicon JSON. Clients, appviews, and runners must enforce it.

PDS-first execution #

  1. Receive a code AT URI, handler name, and JSON input.
  2. Resolve the author's DID and authoritative PDS.
  3. Fetch the V2 code record and verify its CID.
  4. Fetch the runtime strong reference and verify the runtime-record CID.
  5. Require blob.mimeType == application/<runtime-record-cid>.
  6. Select the runtime platform entry matching the Docker daemon.
  7. Pull the runtime OCI artifact by digest without starting it.
  8. Extract and validate its fixed export directory into a cached runtime volume.
  9. Pull and extract optional additions in the same non-executing manner.
  10. Fetch the code blob from the author's PDS and verify its blob CID.
  11. Populate an execution-specific code volume.
  12. Start the fixed runner-owned sandbox with read-only runtime, addition, and code mounts plus an optional writable work volume.
  13. Send one invocation envelope over stdin and receive protocol frames over stdout. Application logs use stderr.

Mutable aliases are resolved on every execution. Immutable code and runtime bytes may be cached by CID, but a strict runner still confirms that their PDS records exist and reference those CIDs before using cached content.

Runtime record validation #

The runner accepts worker protocol version 1 and runtime kind language. Platform selection is an exact match against the Docker daemon's reported OCI OS, architecture, and optional variant. Missing and duplicate matches are both errors; there is no host-maintained fallback map.

Before pulling, the selected image must be a canonical SHA-256 digest reference. The export path must be an absolute container path without traversal. The launcher executable is a relative path, its arguments contain no shell syntax, and exactly one argument is the literal {entrypoint} placeholder. Runtime environment names use portable process-environment syntax and values may not contain NUL bytes.

Code artifact format #

A V2 code blob is a tar archive with exactly one top-level directory. The code record's entrypoint is a safe path relative to that directory and must name a regular file or internal symbolic link in the archive. This keeps the archive's display/root name out of the record while supporting Rust, JavaScript, Python, and other multi-file artifacts uniformly.

After CID verification and archive validation, the exact PDS bytes are copied into a separate Docker volume through a stopped carrier container. The carrier is the already-selected, digest-pinned runtime image and is not started. The future runner-owned sandbox receives the code and runtime volumes read-only.

Code records cannot choose the sandbox or runtime image. Their only OCI-image field is the optional additions list. Each addition must be digest-pinned and the runner always reads its fixed /at-run/addition directory into a separate volume without starting it; authors cannot supply another export path or command.

Filesystem layout #

/opt/at-run/runtime       selected public runtime, read-only
/opt/at-run/additions/0   optional addition, read-only
/opt/at-run/additions/1   optional addition, read-only
/opt/at-run/app           PDS-owned code, read-only
/opt/at-run/work          execution-local writable storage

Runtime and addition archives must be relocatable. Archive validation rejects absolute paths, traversal, escaping symlinks, device nodes, sockets, setuid and setgid bits, and configured size or file-count violations.

Docker materialization #

The Docker implementation uses only Engine API operations:

  1. Require a canonical repository@sha256:<digest> reference.
  2. Reuse the image from the daemon content store or pull that exact digest.
  3. Create a named volume and a stopped container from the selected image.
  4. Mount the volume writable at /at-run-output in that stopped container.
  5. Download the configured export path as a tar archive.
  6. Validate the exact tar bytes against operator size and entry limits.
  7. Upload those same bytes into /at-run-output through Docker's archive API.
  8. Remove the stopped container; remove the volume as well if any step failed.

No container process participates in this operation. ENTRYPOINT, CMD, health checks, and environment declared by the selected image are ignored.

PDS code archives use the same stopped-container upload mechanism, after their blob CID, paths, entrypoint, entry types, links, permissions, and expansion limits have been checked.

Invocation protocol #

Arguments are not command-line flags or environment variables. The runner attaches to the stopped container, starts it, and writes a framed JSON message to stdin:

{
  "version": 1,
  "id": "exec_01J...",
  "type": "invoke",
  "code": {
    "uri": "at://did:plc:alice/dev.mainasara.at-run.beta.v2.code/3abc",
    "cid": "bafycode"
  },
  "handler": "greet",
  "input": {"name": "Ada"},
  "context": {
    "callerDid": "did:plc:bob",
    "deadline": "2026-08-18T16:00:00Z"
  }
}

For the initial fresh-container model, stdin is closed after one invocation. The protocol can later support progress, cancellation, streaming, and multiple warm-worker invocations without changing language-facing handler APIs.

The concrete V1 transport is one bounded newline-delimited JSON object. The Rust core exposes matching InvocationEnvelope, encode_invocation, and decode_invocation types so the runner and language adapters share the same wire contract. See runtimes.md for language-facing examples and the public runtime publishing contract.

Security posture #

Docker is an isolation mechanism, not a perfect hostile-tenant boundary. The baseline sandbox must use a non-root user, a read-only root filesystem, dropped capabilities, no-new-privileges, no Docker socket, no host mounts, disabled networking by default, bounded PIDs, memory and CPU limits, and ephemeral work storage. Public multi-tenant runners should additionally use a sandboxed runtime or disposable worker VM.

Runtime and addition records are executable dependencies even though their OCI entrypoints are ignored: their files may later be invoked by code inside the sandbox. Runners may therefore apply generic publisher or registry policy without maintaining runtime-name mappings.

Main-container initialization #

Only after runtime, code, and addition volumes have been populated does the runner create the main container. Its image, supervisor path, UID/GID, and maximum resource policy are operator-owned. Public records cannot override them.

The Docker adapter creates this container stopped with all content volumes read-only, an ephemeral writable work tmpfs, a read-only root filesystem, a non-root user, all Linux capabilities dropped, no-new-privileges, Docker's built-in seccomp profile, no health check, no network, and bounded memory and PIDs. Requested network access currently fails closed until an enforcing egress proxy exists. Starting, attaching, timeout enforcement, and result-frame collection are a subsequent execution slice.