# Runtime Sandbox Security Contract

This document defines the security contract for Covenant runtime isolation. It separates trusted-local execution, the initial Linux gVisor runtime runner, and the remaining production work so public claims stay aligned with the code.

## Current State

Implemented:

- Agent manifests may declare `[sandbox]`.
- `sandbox.required = true` is rejected unless the manifest names a sandbox-grade backend.
- The current subprocess runner is `trusted-local`; it refuses to run agents that require sandbox-grade isolation.
- `GvisorRunner` prepares a restrictive OCI bundle and invokes `runsc`.
- The initial `GvisorRunner` supports `filesystem = "read-only-package"` and `network = "off"` only.
- `covenantd` can select `trusted-local` or `linux-gvisor` at startup through explicit runtime backend configuration.
- The live coverage matrix includes an ignored Linux gVisor dispatch test gated on `runsc` and an explicit rootfs.
- A repeatable Linux runner guide documents the host, `runsc`, rootfs, and CI adoption requirements for that live path.
- Sandbox stderr redacts configured host-local paths before surfacing failure text.

Not implemented:

- `outbound-https-only`, `full`, `ephemeral`, and `host` sandbox policies are not enforced by the initial runner; they fail closed instead.
- macOS execution is trusted-local only.
- Default CI does not run live sandbox validation on every PR — only sandbox-runtime path PRs trigger the dedicated `.github/workflows/gvisor-live.yml` workflow, which provisions `runsc` and the rootfs, and that workflow is not yet a required check.

## Trust Boundary

The trusted-local runner is useful for first-party automation and local development. It is not a security boundary against hostile agent code.

Trusted-local protects:

- Covenant-mediated state mutations through daemon-side capability checks.
- Runtime budget enforcement via projection-tick preempt on projected overshoot, with a wall-clock kill at `cpu_ms_per_task` as the final backstop.
- Agent protocol attribution because the daemon chooses which manifest produced a result.

Trusted-local does not protect:

- Host filesystem reads available to the operator user.
- Host environment variables inherited by a child process.
- Network access beyond whatever the host OS allows.
- Memory, CPU, syscall, or device abuse within the runtime budget window.
- Malicious code executed as the same user outside the daemon protocol.

## Sandbox-Required Semantics

If an agent declares:

```toml
[sandbox]
required = true
backend = "linux-gvisor"
```

then Covenant must fail closed when the active runtime cannot satisfy that backend. It must not silently downgrade to trusted-local subprocess execution.

This invariant is enforced by `covenant-manifest` validation, `covenant-runtime` subprocess dispatch, and daemon startup backend selection. If the daemon runs with the default `trusted-local` backend, sandbox-required agents fail rather than downgrading.

## Daemon Backend Configuration

`covenantd` defaults to trusted-local execution:

```bash
COVENANT_RUNTIME_BACKEND=trusted-local
```

To opt into the initial Linux gVisor runner:

```bash
COVENANT_RUNTIME_BACKEND=linux-gvisor
COVENANT_GVISOR_ROOTFS=/path/to/rootfs
```

Optional gVisor settings:

```bash
COVENANT_RUNSC=runsc
COVENANT_GVISOR_SCRATCH=$COVENANT_HOME/runtime/gvisor
```

`COVENANT_GVISOR_ROOTFS` is required for `linux-gvisor`. Missing or unknown backend configuration fails daemon startup. Runtime execution errors from `runsc` are surfaced as dispatch failures; Covenant must not fall back to trusted-local execution for sandbox-required agents.

## Live gVisor Validation

The runtime crate has an opt-in live test for the real `runsc` dispatch path. It is intentionally ignored by default because the host requirements are not portable. Host prerequisites include a Linux kernel, a usable `runsc` binary on `PATH`, and a `COVENANT_LIVE_GVISOR_ROOTFS` directory that resolves to a minimal Alpine rootfs; the GitHub Actions workflow `.github/workflows/gvisor-live.yml` exercises the same path on `ubuntu-22.04` for sandbox-runtime changes.

```bash
cd agent-os
COVENANT_LIVE_GVISOR_ROOTFS=/path/to/rootfs \
  cargo test -p covenant-runtime --test live_gvisor -- --ignored live_gvisor_runner_dispatches_with_runsc
```

Optional:

```bash
COVENANT_LIVE_RUNSC=/path/to/runsc
```

The rootfs must contain `/bin/sh`. When the rootfs is not provided, the test takes a prerequisite-skip path and exits successfully. When the rootfs is provided, missing `runsc`, invalid rootfs layout, sandbox startup failure, or fallback behavior is a test failure.

## First Production Backend

The accepted first production sandbox backend is Linux gVisor through `runsc`. The runtime crate contains the first runner boundary, the daemon can select it explicitly, and an opt-in live dispatch test exists. Covenant still cannot claim production sandbox-grade local execution until Linux host requirements and repeatable CI coverage are complete. The backend must meet this minimum contract:

- prepare an OCI bundle from the agent package without mounting the host home directory;
- mount the agent package read-only unless the manifest explicitly requests an ephemeral writable layer;
- start with no ambient host secrets or arbitrary inherited environment variables;
- enforce manifest-visible network policy;
- enforce wall-clock timeout and kill the sandbox on timeout;
- surface sandbox setup failure as dispatch failure, not fallback execution;
- redact host-local paths and secret-looking values from operator-facing logs;
- provide live Linux-only validation gated on `runsc` availability.

## Filesystem Policy

Manifest values are parsed now. The initial gVisor runner enforces `read-only-package` with `network = "off"` and rejects other policies until they have real enforcement:

| Policy | Meaning |
| --- | --- |
| `read-only-package` | Agent package is visible read-only. This is the default. |
| `ephemeral` | Planned. Agent receives a writable scratch layer that is discarded after dispatch. |
| `host` | Planned privileged policy. This must require explicit operator approval before use by untrusted agents. |

Policies other than the initial enforced subset must not be described as available sandbox behavior.

## Budget-Driven Preempt

The runtime hard-preempts a subprocess that is projected to exceed its remaining credit budget *before* it completes naturally. This is a stronger guarantee than the wall-clock kill on `cpu_ms_per_task` elapsed, which still fires as the final backstop. The end-to-end path is covered by the opt-in `live_runtime_preempt` test (`agent-os/crates/covenantd/tests/live_runtime_preempt.rs`), which spawns a fixture binary against a real daemon, pins `budget=1` and `cpu_ms_per_task=10000`, dispatches a single intent, and polls the audit feed for the `BudgetPreempted` row before the binary's wall-clock budget would have fired.

Shipped end-to-end:

- `covenant-runtime::SubprocessRunner` and `GvisorRunner` spawn the agent (or the runsc host process, for gVisor) in a new POSIX process group via `process_group(0)` on Unix so a subsequent `kill(-pid, SIG)` targets every grandchild the agent forked. The configuration is a no-op on non-Unix; the platform gap remains.
- `covenant-runtime::SubprocessTracker` and `TrackedSubprocess` are the in-memory primitives the daemon uses to look up a running subprocess's pid by intent id. The daemon constructs the tracker once at startup and passes it into both runners; on every successful spawn the runner registers an entry keyed by `intent.id` and the entry is unregistered when the dispatch returns (success, error, or timeout-driven kill).
- `covenant-runtime::preempt_subprocess_pg(pid, grace)` is the pure async dispatcher that sends `SIGTERM` to a process group, polls every 50ms up to `grace` for the group to drain via `kill(-pid, 0)==ESRCH`, then sends `SIGKILL` if any member is still alive. It returns a `PreemptOutcome` the daemon-side audit emitter maps to `BudgetPreempted` or `BudgetPreemptFailed` rows.
- `covenant-budget::project_overshoot(...)` is the pure projection function the daemon-side tick calls to decide whether an in-flight subprocess is on track to exceed its remaining budget. `BudgetProjectionPolicy::NoExtrapolation` preserves the post-completion-only behavior; `LinearExtrapolation` short-circuits below either an observation-window or sample-count threshold to avoid spurious early triggers.
- `Server::run_projection_tick_iteration` walks the tracker on each call, applies `project_overshoot` per entry, and dispatches `Server::preempt_intent` for each exhausted intent. `spawn_projection_tick_driver` wraps this in a `tokio::time::interval` (default period: 250ms, configurable via `COVENANT_BUDGET_PROJECTION_TICK_MS`) and is spawned by the daemon's `main` at startup.
- `Server::preempt_intent` reads the tracker entry for an intent_id, calls `preempt_subprocess_pg` with the grace window, and emits the resulting `BudgetPreempted` or `BudgetPreemptFailed` row. The grace window is read from `COVENANT_BUDGET_PREEMPT_GRACE_MS` (default: `2000` ms). A 0-ms grace immediately escalates to `SIGKILL` and produces audit noise; operators should leave room for `SIGTERM` to land.
- `covenant-audit::AuditKind::BudgetPreempted` and `BudgetPreemptFailed` are wired into `audit_kind_requires_persistence` so the daemon refuses to drop them on audit-write failure.

Remaining gap:

- Daemon-restart recovery. The tracker is in-memory only; a crash between the projection decision and the signal dispatch leaves an orphan subprocess outliving the preempt window. Recovery via a pidfile or `/proc` scan on daemon startup is a separate followup. The steady-state hard guarantee holds; recovery from an interrupted preempt does not yet.

## Security Review Checklist

Runtime sandbox changes require review against this checklist:

- Does any code path downgrade `sandbox.required = true` to trusted-local execution?
- Does the backend mount `$HOME`, the repo root, SSH keys, credential stores, or shell config by default?
- Does the backend inherit all operator environment variables?
- Does network access match the manifest policy?
- Are timeout and cleanup enforced when agent startup hangs?
- Are sandbox setup errors auditable without leaking host-local paths or secrets?
- Do public docs still distinguish trusted-local, implemented sandbox behavior, and planned behavior?
- Is there at least one failure-mode test for required-sandbox refusal?

## Human Escalation

Humans retain authority over:

- approving any public claim that Covenant safely executes hostile third-party agents;
- choosing production Linux host requirements for sandbox validation;
- enabling `host` filesystem policy for untrusted agents;
- publishing signed releases or transparency-log entries for sandboxed execution claims.
