# Concepts

Every word Shark uses, defined once, with what it guarantees today. Labels used on every page:

- **GA-path**: served by the node API (`/v1`) and tested against the real server; part of early access.
- **Beta**: served and tested, but only locally (no microVM in the loop) or with a known gap written next to it.
- **Coming**: the route answers `422 unsupported` today, or the piece is a spec only.

## Space

A Space is an agent's persistent workspace: **your files, plus your own persistent services when you need them**.

- **Files** live in SharkFS, a content-addressed store (chunks and Merkle trees, like git objects) mounted lazily as a
  POSIX filesystem inside each command's VM. A command reads only the bytes it touches; identical files are stored once.
- **History.** Every command that changes files lands as one commit stamped with its `op_id`.
  `GET /v1/spaces/{id}/audit` lists, per commit, the op and the paths it changed.
- **A new Space is empty.** Put code in with the files API (`shark cp -r ./repo demo:repo`, `PUT /v1/spaces/{id}/files`,
  archive upload) or with a command (`git clone`, a package manager), within the Space's egress policy.
- **Ids** look like `<prefix>-<32 hex>` (128 random bits). A Space can also have a `name` (unique per account) and up to
  32 `labels`; every route accepts the id or the name.
- **State**: `ready` (no VM runs unless a command, process or service does) or `hibernated` (packed on disk, see
  [Hibernation](#hibernation-sleeping-spaces)). `GET /v1/spaces/{id}` never wakes a Space.

GA-path.

### SDK Spaces and devbox Spaces

A Space is made for one of two things, and says which in its `profile`, set at create:

- **devbox** (`shark create`, `shark code`): one developer's Space. It takes the account's secrets and its GitHub
  connection, what a command leaves running stays as a background job, and an agent in it gets the in-Space
  `shark` CLI.
- **sdk** (`POST /v1/spaces` from the SDKs or raw HTTP, the default there): a Space a platform makes for **its own
  end users**. Nothing of the account reaches it unless asked: no account secret (`account_secrets`), no GitHub
  (`github`), no Space CLI credential (`space_cli`), and what a command leaves running ends with it
  (`keep_background`). Each is a field at create, by `PATCH` and at fork; a fork keeps its source's choices.

Kept jobs, services and exposed processes of either profile sleep when idle and wake on a connection;
`keep_awake: true` opts a Space's jobs out (billed awake). [api.md](api.md#sdk-spaces-and-devbox-spaces-profile)
has every field.

## Lease: one command, one microVM

Every `exec` gets a **fresh Firecracker microVM with its own Linux kernel**, restored from a snapshot, with the Space
mounted. The VM exists only while the command runs. Lease shape on the hosted node: 8 vCPU / 4 GiB.

When the command ends:

- its file changes are committed to the Space **whatever the exit code**, and the VM is destroyed;
- you get `exit`, `stdout`, `stderr` (sync: the last 1 MiB of each; the full output stays readable as the op's event
  log), `shark_status`, `commit`, `changed_paths`, `run_s` (metered seconds, measured in the guest) and timings.

**What persists between commands, and what does not.** This is the contract to design around:

| persists | does not persist |
|---|---|
| the Space's file tree (every command's changes are committed) | process memory of a command (each command is a new VM) |
| services' data (Postgres, Redis volumes, checkpointed separately) | shell state: `cd`, `export`, aliases. Every command starts in the Space root; use `cwd` and `env` per command, or `shark connect`, which carries them for you |
| background processes kept as jobs ([below](#background-processes)), and exposed processes, until they exit | |

**`shark_status`** says whether an unusual exit came from Shark or from your command:

| `shark_status` | meaning | what to do |
|---|---|---|
| `ok` | your command ran to completion (any exit code: a non-zero exit is a normal `200`) | read `exit` |
| `conflict` | a concurrent command of the same Space changed the same paths first; this one's file changes were not published | re-run with a **new** `op_id` |
| `timeout` | your `timeout_s` fired | raise it, stream it, or run it async |
| `infra_timeout` | the infrastructure deadline killed the VM; nothing published | shorten it or run it in the background |
| `infra_error` | Shark failed (mount, store, VM) or the Space was busy | retry with the **same** `op_id` (the SDKs do it once for you) |
| `capacity_unavailable` | the node was too busy to start the command (its VM got no CPU to boot); **it did not run** (`exit` 125, `retryable`, `retry_after_s`) | retry with the **same** `op_id` after `retry_after_s` (the CLI and the SDKs do it) |
| `signaled` | stopped by a signal you sent (`/ops/{op_id}/signal`, Ctrl-C in `shark run`) | |

**Concurrency.** Commands of one Space can run at the same time; each starts from the HEAD it reads and lands by
three-way merge (overlapping paths give `conflict`). Per account, at most a fixed number of leases run at once
(your account's value: `shark quotas`, [limits.md](limits.md#your-account)); beyond it a command waits in a queue
and, if it waits too long, gets `429 lease_cap_reached` **before it starts** ([errors.md](errors.md)).

**Inside a command, practical notes** (check a tool with `command -v` before relying on it):

- **Python packages**: the system `pip install` is refused (the image's Python is "externally managed"). Use a virtual
  environment in the Space and call it by path, since `source` does not carry over between commands:
  `python3 -m venv .venv && .venv/bin/pip install -r requirements.txt`, then `.venv/bin/python app.py`. The venv is
  part of the Space's files, so later commands and forks reuse it.
- **Disk**: `df` inside a command currently shows the host's disk, not your account's quota; do not plan from it. The
  account's disk quota is enforced with `507 insufficient_storage` ([limits.md](limits.md)).

GA-path.

## Op and `op_id`: exactly once

A command is a resource, the **op**, named by its `op_id` (1–64 of `[A-Za-z0-9_.:-]`).

- **Exactly once.** A retry with the same `op_id` (lost response, dropped connection, a crashed client) returns the
  stored result with `"replayed": true` and header `Shark-Replayed: true`. The command does not run twice. A retry that
  arrives while the op still runs joins it.
- **Mismatch is an error, not a silent replay.** The same `op_id` with a different request (`cmd`, `cwd`, `env`,
  `stdin`, `timeout_s`, `background`) answers `409 idempotency_mismatch`.
- **Re-attach from anywhere.** `GET /v1/spaces/{id}/ops/{op_id}` (state and result) and `.../events` (Server-Sent
  Events, resumable with `Last-Event-ID`, nothing lost or repeated) work while it runs and after it ended. Output is
  kept 7 days, then `410 op_expired` (its commit stays in `audit`).
- **Use the tool call id.** In an agent loop, pass the model's tool-call id as `op_id`: a harness that resumes from a
  checkpoint and replays a tool call gets the stored result. The SDKs and `shark run` generate an `op_id` per call and
  reuse it on their own retries.
- **`Idempotency-Key`** (header, 1–255 chars, kept 24 h) does the same for create and fork: a retried create never makes
  two Spaces.

Three ways to run an op: **sync** (default; JSON when it ends; `timeout_s` max 900 s), **stream** (`Accept:
text/event-stream`; SSE while it runs; max 2 h) and **async** (`"wait": false`; `202` with the op, follow its
`events_url`; max 2 h). GA-path.

## Services that sleep

A Space can declare packaged services. Each runs in **its own VM with its own data volume**, belongs to that Space
alone, is paused or snapshotted when nothing talks to it, and wakes when a command connects to `127.0.0.1:<port>`.

| service | port | VM shape | env your commands see |
|---|---|---|---|
| `postgres@16`, `postgres@17` | 5432 | 2 vCPU, 1024 MiB | `DATABASE_URL`, `POSTGRES_URL`, `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE` |
| `redis@7` | 6379 | 1 vCPU, 512 MiB | `REDIS_URL`, `REDIS_HOST`, `REDIS_PORT` |

Declare one by committing `.shark/services.toml` to the Space (with the files API or a command):

```toml
# .shark/services.toml
[postgres]
version = "17"
```

Measured on the AX41 (2026-11-05): a sleeping service answers its first query in **37–40 ms** (Postgres, the example
the measurement used). The data volume is not part of the file tree; it is checkpointed separately.

**Beta.** In every measurement the file was in the Space before its first command. Declaring it from a running
Space is expected to take effect from the next command but is not yet measured end to end. The services API
(`/v1/spaces/{id}/services`, `/v1/catalog/services`) is **coming** (`422` today).

## Background processes

A process still running when the command's shell exits either ends with the command's VM or, on a node that keeps
background jobs (job promotion; on in hosted `shark code`, and `$SHARK_CONTEXT` says `can.background_after_command`),
stays alive as a **job** of the Space (`bg-1`, …) in its own VM. The command returns at once and prints one line
naming the job, its ports and its log file.

- **Works:** later commands reach a kept job's ports at `127.0.0.1:<port>`; it keeps writing to the Space (its writes
  land when it snapshots or ends); it sleeps when idle and a connection wakes it.
- **Not like a local shell:** it is in another VM, so `ps`, `pkill`, `kill %1` in later commands do not see it. Use
  `shark ps`, `shark logs bg-1`, `shark kill bg-1` (inside `shark code` with the agent's own Space credential; from
  outside with `<space>`), or `GET /v1/spaces/{id}/processes`, `…/processes/{pid}/logs`, `POST …/processes/{pid}/signal`.
  Its log is committed output: it grows at the job's snapshots and at its end, not byte by byte.
- **Bound it:** `(timeout 1800 npm run dev > dev.log 2>&1 &)`; jobs also end when the Space hibernates. Nothing else
  stops a job nobody remembers.
- **Wait in the starting command:** between an agent's commands the Space may be frozen while the agent thinks, and a
  job still starting freezes with it, so start the server and poll its port in the same command
  (`… & for i in $(seq 120); do curl -sf localhost:3000 >/dev/null && break; sleep 0.5; done`).

Beta: the metal proofs ran with job promotion; P131 measures the agent path. For a server you want to reach from a
browser, use an [exposed process](#exposed-processes-and-preview-urls).

## Exposed processes and preview URLs

An **exposed process** is a long-running server (a dev server, an API, a demo app) declared on a port:

```sh
shark expose demo 3000 --cwd app -- npm run dev
shark open demo 3000          # one-time login link, then the preview in your browser
```

- It is a **sleeping service of the Space**: it starts on the first connection, sleeps after `idle_s` without traffic
  (default 30 s), and wakes on the next request.
- It **follows HEAD**: a file written by any command is visible to it before the next request it serves (hot reload
  works, including Vite HMR over WebSocket). What the process itself writes does not become a commit.
- **Preview URL**: `https://<space-id>-<port>.preview.sharkmagic.si`. Private by default: `shark open` mints a one-time
  link (60 s) that sets a cookie for that preview host only. `--public` makes it reachable without the link, where the
  account allows it.

- `shark expose <space> --logs PORT` shows its output, `--sleep PORT` puts it to sleep now, `--rm PORT` stops it and
  revokes every preview cookie.
- **Billed while awake, by size; asleep it is not** ([billing.md](billing.md#what-is-metered)). Auto-sleep is a safety
  net: remove what nobody is looking at.

**GA-path, live on the hosted node**: each exposed process runs in its own microVM, and the whole flow above
(private by default, one-time link, public toggle, logs, sleep after idle, wake on request, remove) passed end to end
there (P130 flow PASS, 2026-11-07). Through the API and the CLI; the SDKs have no method for it yet (coming). A node
without the exposed-process runner answers `422 unsupported` ("exposed ports and terminals are off on this node").

## Terminal: `shark connect`

An interactive shell over a WebSocket (`shark-pty.v1`): **each line you type runs as one op** in its own microVM, and
`cd`, `export`/`unset` and `$?` carry over between lines. Ctrl-C interrupts the running line. No TTY inside the command
(full-screen programs such as `vim` do not work this way). **Coming** on the hosted node: local gates PASS (P112), not
yet verified there.

## Fork

`POST /v1/spaces/{id}/fork` (`shark fork demo -n 5`) creates 1–50 new Spaces from the source's current commit (or a past
one, `at`).

- **Files: O(1).** A fork is a new ref to the same commit; nothing is copied (`bytes_copied: 0`), objects are shared and
  deduplicated. Measured: a fork returns in milliseconds of server time.
- **Copied**: env defaults, egress policy, labels (unless `"inherit_labels": false`; then only the `labels` you pass).
- **Not copied**: the Space's own secrets (account secrets reach the fork like any Space of the account), running
  processes and jobs, service memory. Service data volumes are not forked yet
  (`"services": "copy"` answers `422`).
- Use it for evals, test shards, one branch per LangGraph thread, one task per Prefect run, or "try three fixes in
  parallel and keep the one that passes".

GA-path.

## Hibernation (sleeping Spaces)

A hibernated Space is packed into a small record on disk (~57 KB measured, P87) and holds no memory and no live slot.
Any request for it wakes it first (the `exec` response then carries `wake_ms`); listing does not wake it.

- `POST /v1/spaces/{id}/hibernate` / `.../wake` (`shark hibernate`, `shark wake`) do it explicitly.
- Automatic hibernation after an idle period: per Space, `shark space set <space> --idle 30m` (`dormancy` in
  `PATCH /v1/spaces/{id}`; `off` = never), else the node's setting. Your account can keep a limited number of Spaces
  awake at once (`awake_spaces` in `shark quotas`); hibernated ones do not count.
- Measured on the AX41 (2026-11-05): first command of a sleeping Space **58 ms**.

GA-path.

## Durability

With durability on, each Space's commits are replicated in the background to an object store.

- `POST /v1/spaces/{id}/checkpoint` returns once HEAD and its objects are in the store; that is the only durability
  promise. Without a checkpoint, the exposure is the replicator's lag, reported in `GET /v1/spaces/{id}/durable`
  (`behind`, `rpo_ms`).
- Measured on the AX41 (2026-11-05): creating a durable Space costs **1.3 ms** of server time (earlier, with sequential
  store round trips, it cost ~0.8 s: P96).

GA-path.

## Secrets: `inject` (default) vs `exec`

`PUT /v1/spaces/{id}/secrets` stores values sealed per Space on the node (AES-256-GCM). Values are never returned by
any read, never in the Space's tree, never replicated, and redacted from logs.

| `expose` | what the command sees | use it for |
|---|---|---|
| **`inject`** (default) | `GH_TOKEN=shk_ph_…`: a per-command **placeholder**. The node's egress proxy swaps it for the real value only in requests to the hosts in `scope.hosts` (default header `Authorization: Bearer`) and scrubs it from responses. **The value never enters the VM.** | API tokens: GitHub, package registries, data APIs, model providers |
| `exec` (explicit opt-in) | the real value, in the environment of the commands that list it in `"secrets": ["NAME"]` | a value code must read (a database URL, a signing key). The response warns `secret_exposed_to_code`: any code in that command can read and send it |

`scope.hosts` must be allowed by the Space's egress policy (the `PUT` warns `scope_host_not_allowed` otherwise).
Forks do not copy a Space's own secrets. Beta: the contract and the "never in the VM" property passed locally (P111
N1–N4, 2026-11-05); the same checks on metal are pending.

**Account secrets** (`PUT /v1/secrets`, `shark secrets set --account NAME …`) are set once for the whole account and
reach every Space's commands the same way (`apply: all`, the default, new Spaces and forks included), or only the
Spaces that reference them (`apply: ref`). A Space secret with the same name always wins in that Space. Beta: passed
locally with a canary value found 0 times outside its scope (P125, 2026-11-06). The guided `shark secrets` at a
terminal, `shark secrets status` (what is missing, and the command that fixes it), `rotate` and `edit`:
[cli.md](cli.md#secrets).

## Egress policy

Each Space has an outbound network policy, set by API (`PUT`/`PATCH /v1/spaces/{id}/egress`, scope
`network:write`), at create (`egress`), and inherited by forks. It governs the Space's commands **and its agent's
session VM** (the harness's own calls: model API, login, catalogs). It applies from the Space's next command (the
session VM: from its next start).

**Nobody configures egress by hand.** A policy is **profiles** (named, versioned host lists Shark maintains, each
host with its source and the date it was checked: `GET /v1/egress/profiles`, `shark egress profiles`) + the Space's
own **allow** entries − its **deny** entries (deny wins over everything).

```json
{"profiles": ["essentials", "opencode"], "allow": ["api.example.com", "db.example.com:5432"], "deny": ["*.githubusercontent.com"]}
```

| profile | opens |
|---|---|
| `essentials` | `npm`, `pypi`, `crates`, `go`, `github`, `gitlab`, `apt-ubuntu`, `docker-hub`, `shark-docs` (the devbox default) |
| `coding` | the same without `gitlab` and `docker-hub`: the base every Space had before profiles, plus `shark-docs`; the SDK default |
| `shark-docs` | `sharkmagic.si`: these docs, which `$SHARK_CONTEXT` tells every agent to read (in both defaults; an agent start adds it to a Space with its own list) |
| `anthropic-api`, `openai-api`, `openrouter`, `google-ai` | one provider's API with an API key |
| `anthropic-login`, `openai-login` | the Claude / ChatGPT subscription login and token refresh |
| `claude-code`, `codex`, `opencode` | everything that harness needs (its API, its login, its model catalog) |
| `browser-testing` | Playwright's browser downloads |
| `none` / `open` | nothing from Shark / no control (mode `open`) |

- **Plug and play.** `shark code --agent X` adds X's profile to the Space by itself, and with an API key the key's
  provider (`anthropic-api`, `openai-api`); a secret preset with `--allow-egress` adds its profile (`anthropic` →
  `anthropic-api`, `github` → `github`). They are listed in `auto` and in `effective` like any other; remove one with
  `-name`, or turn the behavior off with `"auto_profiles": false`. A Space in mode `deny` or `open` is never changed.
- **Composition.** `+name` adds, `-name` removes; `-npm` on a Space with `essentials` replaces `essentials` by its other
  parts. `GET …/egress` returns `effective.entries`: every host with the profiles it came from (`"essentials/npm"`,
  `"opencode"`, `"space"` for yours) and `effective.denied`: what your deny list took out.
- **Denials are visible.** `GET …/egress/denials?since=1h` (`shark egress denials`): each host the proxy refused, how
  often, when, and the profile that contains it (`suggest`, `add`: the command to run). Host names and ports only,
  never content.
- Defaults: devbox Spaces start with `essentials`, SDK Spaces with `coding` (the node's `-egress-default`, hosted:
  `allowlist`). The old shape `{"mode", "allow", "base": "coding"|"none"}` still works: `base` maps to the `coding`
  profile or none, and reads keep returning it.
- Modes: `allowlist` (the profiles + allow − deny). `deny`: nothing leaves. `open`: everything public (only for
  accounts allowed it; otherwise `403 plan_restriction`); deny entries need `allowlist`.
- Web traffic is decided by name (HTTP Host / TLS SNI). `name:port` opens another TCP port of a name;
  `public-cidr:port` a non-HTTP destination by address. Bare IPs, private and link-local ranges are always denied.

Beta: contract and enforcement passed locally (P111 E1/E2, 24/24 probes; profiles: each opens only its hosts,
`proofs/egress-profiles/results/local-r1/`); the on-metal check is metal-queue row `egp1`.

## Account, keys and scopes

- An **account** is a tenant. Everything you create belongs to the tenant of the key you used; another account's Space
  answers `404` (never `403`, so ids cannot be probed).
- **Workspace and teams.** Every account starts with a **personal workspace** (just you; nothing to set up). Teams are
  opt-in: `shark org create`, `shark org invite`, `shark org use` ([cli.md](cli.md#account)). Keys, balance, usage and
  Spaces belong to one org.
- **API keys** look like `shk_live_<id>_<secret>`: shown once, stored only as an HMAC. They live in the **account
  service** (`https://api.sharkmagic.si`): the CLI and the SDKs exchange a key there for 5-minute node tokens
  (`POST /v1/token`), so the key itself never reaches a node. Create, rotate and revoke them with `shark keys` or the
  dashboard; a revoked key's tokens stop working on the node within seconds. Keep keys in the environment
  (`SHARK_API_KEY`) or in `~/.shark/credentials` (mode 0600), never in code, files of the Space or prompts.
- **Logins.** `shark login` at a terminal logs a person in through the browser with a one-time code; `shark logins`
  lists and ends them. A key is for scripts, CI and agents.
- **Scopes**: a key carries a subset of `spaces:read`, `spaces:write`, `exec`, `files:read`, `files:write`,
  `services:write`, `secrets:write`, `network:write`, `usage:read`, `admin`. Default keys get the first four plus
  `files:write` and `usage:read`; ask for `secrets:write` and `network:write` when you need them.
- **Space-scoped keys**: `shark keys create --name N --space SP` makes a key limited to one Space, for handing a single
  Space to an agent or a script. Agents under `shark code` already get a credential limited to their Space
  ([shark-code.md](shark-code.md#what-the-agent-knows)).
- **Account secrets** (set once, used by every Space) and Space secrets: [Secrets](#secrets-inject-default-vs-exec).

## Credits

Usage is metered per second of what runs (command seconds scaled by lease shape, service and job awake seconds,
storage, egress) and paid from a prepaid credit balance. Idle time, the model thinking, and reads served without a
VM are not billed. At zero balance new work answers `402 credits_exhausted`; files stay readable. Details:
[billing.md](billing.md).

## Vocabulary: Shark vs E2B, Daytona, Modal and boat

Closest equivalents, not identical semantics. The column that matters is the last one.

| Shark | E2B | Daytona | Modal | boat | how Shark differs |
|---|---|---|---|---|---|
| **Space** | sandbox (+ its filesystem) | sandbox | sandbox + volume | sandbox (`bx_…`) | the Space is the files and services, not a running machine; nothing runs while it is idle |
| **lease** (one command, one microVM) | — (commands run in the one long-lived sandbox VM) | — | — | — | every command gets a fresh VM with its own kernel; there is no machine to keep warm or stop |
| **`exec` / op** with `op_id` | `commands.run` | `process.exec` | `Sandbox.exec` | `exec` | the command is a resource: exactly-once by `op_id`, re-attachable, output replayable for 7 days |
| **fork** | — (pause/resume of one sandbox) | — | `snapshot_filesystem` → new sandbox | `fork` (from the last background snapshot) | O(1) on files at an exact commit, up to 50 per call |
| **hibernate / wake** | pause / resume | stop / start, archive | — (terminate; volumes persist) | stop / resume | automatic: any request wakes it; first command measured at 58 ms |
| **service** (Postgres, Redis per Space) | — (run it inside the sandbox) | — | — | — (systemd inside the VM) | its own VM and volume, sleeps and wakes on connect |
| **exposed process** + **preview URL** | `getHost(port)` | preview link | tunnel / encrypted port | preview URL (`?_token`) | sleeps when idle, wakes on request, follows every commit; private by default (one-time link) |
| **secret `inject`** | env vars | env vars | `Secret` (env) | credential proxy for AI keys | the value never enters the VM; the host adds it on the way out |
| **egress policy** | internet access on/off | network allow list | `block_network` / CIDR allowlist | — (iptables inside the VM) | per Space by API, by name, private ranges always denied |
| **credits** | usage billing | usage billing | usage billing | plan + wallet | prepaid credits, plus an optional monthly plan with a usage allowance; billed per second of command, service and job; idle is not billed |
