sharkmagic.docs
Errors

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

Errors#

Every /v1 error has the same body, a stable code, and says whether a retry can succeed.

{"error": {
  "code": "lease_cap_reached",
  "message": "too many leases running: tenant cap reached; the command did not start",
  "retryable": true,
  "request_id": "req_01M6N8R52A3XZP6Z2DTKWFNRRW",
  "details": {"scope": "tenant", "limit": 6, "active": 6, "queued": 1, "waited_ms": 30012}
}}
  • code is stable; branch on it. The list is open: treat an unknown code by its HTTP status.
  • message is for humans and may change.
  • retryable: true means the same request may succeed later. Retryable 429 and 503 always carry a Retry-After header (integer seconds, 1–30), computed from your queue rather than fixed.
  • request_id is also in the Shark-Request-Id header of every response, errors or not. Quote it when you ask for help.
  • details carries the field at fault (details.field), limits (limit, used, max_s) or a scope.

A command that fails is not an HTTP error. exec answers 200 with the command's exit; check shark_status for Shark-side outcomes (conflict, timeout, infra_timeout, infra_error, signaled: concepts.md).

A busy node is not an infra error. When the node's CPUs are saturated it refuses new work up front (503 out_of_capacity, details.reason = cpu_pressure or cpu_runqueue), and a command it admitted but could not start in time (its VM got no CPU to boot) ends as shark_status: "capacity_unavailable", not infra_error. In both cases the command did not run: resend it with the same op_id after Retry-After. A sync exec gets that second case as 503 capacity_unavailable (details.reason = lease_start, details.op_id); a stream or an async op gets it in the result (exit 125, "retryable": true, "retry_after_s"). The CLI and both SDKs retry both on their own. To see whether a node is admitting work right now: the node_admission row of GET /v1/quotas (state open or closed, reason, the CPU pressure it read).

Codes#

HTTP code retryable when what to do
400 invalid_request no malformed JSON or a bad field (details.field); also timeout_s above the mode's maximum (details.max_s, details.mode); GET …/secrets/status with an unknown agent or preset (details.reason = unknown_agent | unknown_preset); GET /ports with an unknown state; an egress host adds details.host and details.reason (ip_without_port, private_range, cidr_without_port, bad_cidr, bad_port, bad_name, invalid_host, too_many_entries) fix the request
400 invalid_op_id no op_id (or Shark-Op-Id) is not 1–64 of [A-Za-z0-9_.:-]
400 path_outside_space no a path or cwd escapes the Space root, or touches reserved .shark/ internals use paths relative to the Space root
401 unauthenticated no missing, malformed, revoked or expired credential; also an account key sent straight to a node (direct mode, SHARK_API set) instead of a node token check SHARK_API_KEY; unset SHARK_API; raw HTTP: exchange the key at POST https://api.sharkmagic.si/v1/token (api.md)
402 credits_exhausted no (until you top up) prepaid balance at zero; new work is refused, reads still work top up
403 insufficient_scope no the key lacks the scope (details.required, e.g. network:write) use a key with that scope
403 insufficient_role no account service: your role in the org cannot do it (owner/admin-only routes: API keys, top-up and plan, the Stripe portal, GitHub connect, invitations; a console token with a write scope for a role the console does not know). A team member gets every node scope, secrets:write and network:write included ask an owner or admin
403 user_credential_required no account service: a route about a person (/v1/orgs create and use, /v1/logins) called with an API key, which belongs to one org shark login, or create a key in the other org
403 personal_workspace no account service: inviting or adding someone to a personal workspace, or deleting it create a team (shark org create) and invite people there
403 origin_not_allowed no a browser preflight from an origin the node does not serve CORS to (the node's -cors-origins; on the hosted node SHARK_CORS_ORIGINS, default the console's origin) call from the dashboard, or with a key outside a browser
403 plan_restriction no allowed by the API, not for this account (e.g. egress mode: open, public previews)
403 tenant_not_allowed no the body names another account drop tenant from the body: it comes from the key
403 tenant_suspended no the account is suspended contact support
404 not_found no unknown id or name, deleted, or another account's (never 403, so ids cannot be probed)
409 no_billing_customer no account service: the Stripe portal before the account's first payment top up or subscribe first
409 idempotency_mismatch no an op_id or Idempotency-Key reused with a different request (details.op_id, details.created_at) use a new id for a new request
409 idempotency_in_progress yes the original request with that key is still running retry after Retry-After
409 conflict no the state does not allow it (hibernate while something runs: details.why; a duplicate name, a file write that raced a command on the same path: details.paths)
410 op_expired no the op's stored output passed its retention (7 days); its commit stays in audit
412 precondition_failed no If-Match on a file write does not match the current version re-read, then write
413 payload_too_large no body, stdin or upload above its limit (limits.md) stream the file or use the archive upload
416 invalid_request no a Range the file cannot satisfy
422 unsupported no a valid request for something this node does not serve yet (details.phase); e.g. services API, snapshots, webhooks, exposed ports on a node without them see the feature's page
426 upgrade_required no a terminal route without the WebSocket upgrade
429 rate_limited yes request rate per credential (100/s, burst 500) wait Retry-After
429 lease_cap_reached yes your account's concurrent commands are at the cap (details.scope = tenant) or the node's (host). The command did not start. wait Retry-After, resend with the same op_id
429 space_limit_reached yes the node's live-Space cap (hibernated Spaces do not count) retry, or hibernate idle Spaces
429 quota_exceeded no an account quota (details.quota = spaces: Spaces held; awake: Spaces awake at once, on create, fork or wake; an exposed process's start or wake past the account's awake_services cap is refused with a quota_exceeded message the preview shows; label_spaces / label_awake: a per-label limit, with details.label and selector; with limit, used, limits_class and raise: how to get more) spaces/label_spaces: delete Spaces; awake/label_awake: hibernate one (shark hibernate); your caps: shark quotas; more: details.raise
429 auth_rate_limited yes too many failed authentications from your address fix the key, then wait
429 too_many_terminals yes 8 terminals already open on the Space close one
500 internal yes, for idempotent requests a bug on our side; request_id identifies it retry with the same op_id / Idempotency-Key; report the request_id
503 out_of_capacity yes the node admits no new command, Space or wake right now (details.reason = memory_headroom | memory_pressure | cpu_pressure | cpu_runqueue; CPU reasons add cpu_psi_some_avg10, runq_per_cpu). Nothing was started. wait Retry-After (≥ 2 s for CPU), resend with the same op_id
503 capacity_unavailable yes a VM or wake could not be placed now; or (details.reason = lease_start, details.op_id) a sync command the node admitted but was too busy to start. That command did not run. wait Retry-After, resend with the same op_id: it runs then
503 canceled yes you disconnected while queued resend with the same op_id
503 durable_unavailable yes checkpoint could not reach the durable store retry
507 insufficient_storage scope: tenant: no; scope: node: yes your account's Spaces exceed their disk quota on the node (limit_mb, used_mb), or the node is low on disk. Deleting files and Spaces still works delete files or Spaces / wait

Warnings (in a 2xx body, warnings: [{secret, code, message}]; the request succeeded):

code when what to do
secret_exposed_to_code a secret set with expose: exec: any code in a command that lists it can read the value prefer inject
scope_host_not_allowed an inject secret's host is not allowed by the Space's egress policy shark egress add <space> HOST
account_secret_not_found a Space references ({"ref": "account"}) an account secret that does not exist: commands get nothing under that name until it is set shark secrets set --account NAME …

Secrets routes also answer 400 invalid_request with details.field = secrets.NAME.apply (apply is all or ref, account secrets only) or secrets.NAME.ref (ref is "account", a Space's secrets only, with no value), and 422 unsupported on a node without the sealed store.

SDK-only codes (raised by the SDKs, never sent by the API): wait_timeout (Python op.wait(timeout=…) ran out of time; the command keeps running), command_failed, infra_error, connection, config (sdk-python.md).

Lines Shark adds to a command's output#

A command that ran (or was refused before it started) may end with one shark: line on stderr from Shark itself. They are meant for coding agents as much as for people: one line, what happened, the next step.

line when next step
shark: egress: connection to HOST:PORT blocked by this Space's egress policy (…); … ask the user to run … (the line ends with the exact shark egress add command) the command connected to a host its Space does not allow (no-NIC egress; with the classic network the program only sees a refused connection) shark egress add SPACE HOST
shark: background process "CMD" (pid N) was stopped when this command finished; nothing outlives a command here… a process started with & was still running at the end use it within the command, or an exposed process / shark run --detach
shark: N background process(es) of this command keep running as job bg-N (CMD); listening on 127.0.0.1:PORT…; output: FILE; ps/pkill do not see it: … a node that keeps background jobs (hosted shark code): the process runs on in its own VM shark ps, shark logs bg-N, shark kill bg-N when done
shark: capacity: … (the host is busy; the command did not run; retry…) the host could not start the command's VM run it again after a few seconds
shark: quota: … the Space's CPU, memory or process limits lower parallelism
shark: browser: not found in this VM. … a command naming a browser exited 127 (image without a browser) install the browser in the Space; use it within one command
shark: browser: that name is not installed; this image's headless browser is … a command named another browser; the image has chrome-headless-shell $CHROME_PATH, or shark screenshot URL out.png

Inside shark code, commands also get $SHARK_MODE (devbox), $SHARK_SPACE and $SHARK_CONTEXT (JSON: what works in this session); shark context --json prints it. See shark-code.md.

Retrying safely#

The rule: a retried request never does the work twice, and never silently does another request's work.

  1. Give every command an op_id and every create/fork an Idempotency-Key. Reuse it only for the same request.
  2. Retry on connection errors, on 5xx, and on any error with "retryable": true, after Retry-After when present (otherwise exponential backoff with jitter, from 0.5 s, capped at 30 s).
  3. Do not retry 4xx with "retryable": false; fix the request.
  4. A retried DELETE that answers 404 means your first attempt worked.

The CLI and both SDKs already do all four. The SDK error classes map one-to-one to the codes (Python: RateLimitError, LeaseCapError, QuotaExceededError, CreditsExhaustedError, IdempotencyMismatchError, NotFoundError, UnsupportedError, …; each has .code, .status, .retryable, .retry_after, .request_id, .details).

Streams#

On an SSE stream (exec with Accept: text/event-stream, /ops/{op_id}/events), the last event is always exit or error; an error event carries {"code", "message"} with the codes above. A stream that drops before either is not an error: reconnect to /ops/{op_id}/events with Last-Event-ID and you get the rest, each event once.