sharkmagic.docs
Concepts

Agent? Fetch sharkmagic.si/docs/llms.txt or this page as markdown

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). 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 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), 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); beyond it a command waits in a queue and, if it waits too long, gets 429 lease_cap_reached before it starts (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).

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):

# .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#

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

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). 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.

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).

{"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). 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).
  • Account secrets (set once, used by every Space) and Space secrets: Secrets.

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.

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