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 unsupportedtoday, 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}/auditlists, 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 aname(unique per account) and up to 32labels; every route accepts the id or the name. - State:
ready(no VM runs unless a command, process or service does) orhibernated(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-SpacesharkCLI. - sdk (
POST /v1/spacesfrom 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, byPATCHand 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 installis refused (the image's Python is "externally managed"). Use a virtual environment in the Space and call it by path, sincesourcedoes 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:
dfinside 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 with507 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": trueand headerShark-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_idwith a different request (cmd,cwd,env,stdin,timeout_s,background) answers409 idempotency_mismatch. - Re-attach from anywhere.
GET /v1/spaces/{id}/ops/{op_id}(state and result) and.../events(Server-Sent Events, resumable withLast-Event-ID, nothing lost or repeated) work while it runs and after it ended. Output is kept 7 days, then410 op_expired(its commit stays inaudit). - 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 andshark rungenerate anop_idper 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 %1in later commands do not see it. Useshark ps,shark logs bg-1,shark kill bg-1(insideshark codewith the agent's own Space credential; from outside with<space>), orGET /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_swithout 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 openmints a one-time link (60 s) that sets a cookie for that preview host only.--publicmakes it reachable without the link, where the account allows it. -
shark expose <space> --logs PORTshows its output,--sleep PORTputs it to sleep now,--rm PORTstops 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 thelabelsyou 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"answers422). - 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(dormancyinPATCH /v1/spaces/{id};off= never), else the node's setting. Your account can keep a limited number of Spaces awake at once (awake_spacesinshark 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}/checkpointreturns 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 inGET /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 Xadds 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-egressadds its profile (anthropic→anthropic-api,github→github). They are listed inautoand ineffectivelike any other; remove one with-name, or turn the behavior off with"auto_profiles": false. A Space in modedenyoropenis never changed. - Composition.
+nameadds,-nameremoves;-npmon a Space withessentialsreplacesessentialsby its other parts.GET …/egressreturnseffective.entries: every host with the profiles it came from ("essentials/npm","opencode","space"for yours) andeffective.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 withcoding(the node's-egress-default, hosted:allowlist). The old shape{"mode", "allow", "base": "coding"|"none"}still works:basemaps to thecodingprofile or none, and reads keep returning it. - Modes:
allowlist(the profiles + allow − deny).deny: nothing leaves.open: everything public (only for accounts allowed it; otherwise403 plan_restriction); deny entries needallowlist. - Web traffic is decided by name (HTTP Host / TLS SNI).
name:portopens another TCP port of a name;public-cidr:porta 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(never403, 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 withshark keysor 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 loginat a terminal logs a person in through the browser with a one-time code;shark loginslists 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 plusfiles:writeandusage:read; ask forsecrets:writeandnetwork:writewhen you need them. - Space-scoped keys:
shark keys create --name N --space SPmakes a key limited to one Space, for handing a single Space to an agent or a script. Agents undershark codealready 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 |
