sharkmagic.docs
TypeScript SDK

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

TypeScript SDK reference#

Every public class and method of the TypeScript SDK (version 0.1.0): signature, options, return type, the errors it throws, and a short example. ESM with type declarations, no dependencies; runs anywhere with fetch and ReadableStream (Node 22.18+, Bun, Deno, edge runtimes). Checked against the code by proofs/a12-docs-acceptance/sdkcheck.py (it fails when this page and sdk/typescript/src drift apart). The Python SDK, which has a few more helpers (differences): sdk-python.md. The HTTP routes behind each call: api.md.

Status follows api.md: GA-path (served and tested), beta (served; local-only tests or a gap noted), coming (answers 422 unsupported today). A method has the status of the route it calls; it is marked below where it is not GA-path.

Options are one object per call; keys you leave out are not sent. In the signatures below, options?: { a, b } lists every key the code accepts (their types are in the text).

Install#

<TS_SDK_INSTALL>

The package is not on npm yet; the placeholder is explained in quickstart.md.

Authentication#

Put your account key (shk_live_…) in the environment and construct the client with no arguments:

export SHARK_API_KEY=...        # never in code, in a file of the Space, or in an agent's prompt
import { Shark } from "@sharkmagic/sdk";
const shark = new Shark();

The client sends the key only to the account service (https://api.sharkmagic.si), which returns a 5-minute node token; the SDK refreshes it 60 s before it expires (one refresh in flight), finds the node that holds each Space, and sends the call there. The key never reaches a node. The TypeScript SDK reads SHARK_API_KEY (from process.env or Deno.env) or apiKey; it does not read ~/.shark/credentials.

Leave SHARK_API unset. SHARK_API (or baseUrl) is direct mode, for a self-hosted node and that node's own keys; an account key there answers 401 unauthenticated. Mode order: controlPlane > baseUrl > $SHARK_CP > $SHARK_API > https://api.sharkmagic.si.

Errors#

Every API error is thrown as a subclass of SharkError (all exported) with: code (the stable code from errors.md; branch on it), status (HTTP), message, requestId, details, retryable, retryAfter (seconds, from Retry-After). A code the SDK does not know maps by HTTP status (400 → InvalidRequestError, 401 → AuthenticationError, 402 → CreditsExhaustedError, 403 → PermissionDeniedError, 404 → NotFoundError, 409/412 → ConflictError, 410 → OpExpiredError, 413 → InvalidRequestError, 422 → UnsupportedError, 429 → RateLimitError, 5xx → ServerError).

Retries are built in. Network errors, 429 and 5xx marked retryable are retried (up to maxRetries, honouring Retry-After; lease_cap_reached keeps waiting up to queueTimeoutMs), always with the same op_id or Idempotency-Key, so a command never runs twice and a create never makes two Spaces. Only the final error reaches you. A non-zero exit is not an exception: run() resolves with it in exit (unless check: true). Argument mistakes in secrets (an exec secret with hosts, an inject secret without hosts or preset, an unknown preset) throw a plain Error before any request.

class extends API codes thrown when
SharkError Error base of all; also code: "config" (no API key, bad URL, usage.balance() in direct mode), code: "needs_control_plane" (githubApp in direct mode)
AuthenticationError SharkError unauthenticated, auth_rate_limited bad, revoked or expired key; account key in direct mode
PermissionDeniedError SharkError insufficient_scope, plan_restriction, tenant_not_allowed the key lacks a scope (details.required) or the account the feature
NotFoundError SharkError not_found unknown id or name, or another account's Space
ConflictError SharkError conflict, precondition_failed state does not allow it; ifMatch mismatch
IdempotencyMismatchError ConflictError idempotency_mismatch, idempotency_in_progress an op_id / key reused for a different request
OpExpiredError SharkError op_expired the op's stored output is past its 7 days
InvalidRequestError SharkError invalid_request, invalid_op_id, path_outside_space, payload_too_large, upgrade_required fix the request (details.field, details.max_s)
UnsupportedError SharkError unsupported a route this node does not serve yet (details.phase)
RateLimitError SharkError rate_limited request rate per key, after the retries
LeaseCapError RateLimitError lease_cap_reached concurrent commands at the cap after queueTimeoutMs; the command did not start; .scope = details.scope
SpaceLimitError RateLimitError space_limit_reached the node's live-Space cap
QuotaExceededError SharkError quota_exceeded, insufficient_storage an account cap (details.quota: spaces, awake) or disk
CreditsExhaustedError SharkError credits_exhausted balance at zero; reads keep working
ServerError SharkError internal after the retries; quote requestId
CapacityError ServerError capacity_unavailable, out_of_capacity, canceled the node was too busy; the command did not run
DurableUnavailableError ServerError durable_unavailable checkpoint() could not reach the durable store
ConnectionError SharkError the network failed after the retries (code: "connection")
CommandFailedError SharkError run(cmd, { check: true }) and the exit is not 0 (code: "command_failed"; .exit, .stdout, .stderr, .opId)
InfraError SharkError run() got shark_status: infra_error twice for the same op_id (code: "infra_error")
CommandTimeoutError SharkError run() got shark_status timeout or infra_timeout (code is that status)
import { Shark, QuotaExceededError, CreditsExhaustedError, SharkError } from "@sharkmagic/sdk";

try {
  await shark.spaces.create({ name: "job-1" });
} catch (e) {
  if (e instanceof QuotaExceededError) console.log(e.code, e.details.quota);   // spaces | awake
  else if (e instanceof CreditsExhaustedError) console.log("top up; files stay readable");
  else if (e instanceof SharkError) console.log(e.code, e.requestId);
  else throw e;
}

errorClass#

errorClass(code: string, status: number): Ctor

The SharkError subclass (constructor) the SDK uses for an error code and HTTP status. CODE_CLASSES (code → class) is exported too.

Client#

new Shark#

new Shark(options?: { apiKey, baseUrl, controlPlane, timeoutMs, maxRetries, queueTimeoutMs, fetch })

apiKey: default $SHARK_API_KEY. baseUrl: a self-hosted node (direct mode). controlPlane: the account service URL (default https://api.sharkmagic.si, or $SHARK_CP). timeoutMs: per HTTP request, default 60,000 (commands get their own). maxRetries: default 5. queueTimeoutMs: how long to keep retrying lease_cap_reached, default 600,000. fetch: your own fetch implementation. Throws SharkError (code: "config") without a key or with a bad URL (plain http only to loopback). Members: spaces, secrets (account secrets), spaceDefaults, githubApp, keys, usage.

shark.me#

shark.me(): Promise<Record<string, any>>

Your account (tenant), plan, and the credential's id, kind, scopes and expiry (GET /me).

shark.capabilities#

shark.capabilities(): Promise<Record<string, any>>

The node's API version, features, limits and lease shapes. Read features from here rather than assuming them.

shark.limits#

shark.limits(): Promise<any[]>

Every limit that applies to you with current use: [{name, scope, limit, used}, …] (GET /quotas). Read limits from here, never from memory (limits.md).

shark.transport#

shark.transport: Transport

The low-level transport (transport.request(operationId, { params, query, json }) sends one call of ROUTES with the retry policy above; transport.stats counts requests, retries and reconnects). Not covered by this reference; prefer the methods below.

newOpId#

newOpId(): string

A fresh time-ordered id (UUIDv7 layout). The SDK makes one per call when you pass none; in an agent loop pass your tool-call id as opId instead, so a retried tool call replays rather than runs twice.

parseSse#

parseSse(body: ReadableStream<Uint8Array>): AsyncGenerator<SseEvent>

Parses a text/event-stream body into events (for calls you make through transport.request).

Spaces#

A Space is a persistent workspace; each command runs in a fresh microVM with it mounted (concepts.md).

SDK Spaces. Spaces the SDK creates are sdk Spaces: nothing of your account reaches them unless the Space asks. Every option below can be set at create, changed with space.update(...), and overridden per fork; leave it out for the default, pass null to update to go back to the profile's default (api.md).

option sdk default values
profile "sdk" "sdk" or "devbox" (as shark create / shark code: the account's secrets and GitHub, background jobs, the Space CLI); create only
accountSecrets "none" "all" (the account's apply: all secrets), "none" (only those the Space references), or an array of names
github false true: the account's GitHub connection (a managed GITHUB_TOKEN; connect it once with shark github connect, cli.md)
git none { name, email }: the Space's commit identity
spaceCli false true, or an array of scopes (subset of spaces:read, files:read, exec, usage:read): a Space credential for the shark CLI inside the Space
keepBackground false true: a process a command leaves running (cmd &) stays as a job (beta, background jobs)
keepAwake false true: kept jobs never sleep when idle (billed awake the whole time)
egress node default a policy object, or "deny", "coding" (package registries and code hosts), "open" (egress)
labels none { k: "v" }; shark.spaces.list({ label: "k:v" }) filters

Changing accountSecrets, github or git needs the secrets:write scope; keepBackground, keepAwake and spaceCli need exec; egress needs network:write. A refused value is InvalidRequestError with details.field, and nothing of the request is applied.

shark.spaces.create#

shark.spaces.create(options?: { name, labels, egress, env, fromSpace, idempotencyKey, profile, accountSecrets, github, git, keepBackground, keepAwake, spaceCli }): Promise<Space>

Creates a Space (POST /spaces). name: unique in your account, usable in place of the id. env: non-secret variables for every command. fromSpace: start from another Space's content, O(1). idempotencyKey: generated if absent; the SDK retries with the same key, so a retried create never makes two Spaces. Throws QuotaExceededError (details.quota spaces or awake), CreditsExhaustedError, CapacityError, ConflictError (duplicate name), InvalidRequestError.

const space = await shark.spaces.create({ name: "acme-1", labels: { customer: "acme" }, egress: "coding",
  accountSecrets: ["OPENAI_API_KEY"], git: { name: "Acme Bot", email: "bot@acme.test" } });

shark.spaces.get#

shark.spaces.get(idOrName: string): Promise<Space>

One Space by id (tti-…) or name; does not wake it. Throws NotFoundError.

shark.spaces.list#

shark.spaces.list(options?: { label, state, name }): AsyncGenerator<Space>

Every matching Space, newest first, pages followed; does not wake hibernated ones. label: "k:v" or an array (all must match). state: "ready" or "hibernated".

for await (const s of shark.spaces.list({ label: "customer:acme" })) console.log(s.id, s.state);

space.id#

space.id: string

The Space id (tti-<32 hex>).

space.state#

space.state: string

ready or hibernated, as of the last response (space.refresh() to update).

space.labels#

space.labels: Record<string, string>

space.profile#

space.profile: string

sdk or devbox (Spaces made before profiles report devbox).

space.refresh#

space.refresh(): Promise<Space>

Re-reads the Space, including settings. space.data holds the raw JSON.

space.settings#

space.settings(): Promise<Record<string, any>>

Every per-Space setting with its source: name, labels, env, egress (with effective.hosts), dormancy, secrets (names only), ports, durable, profile, account_secrets, github, git, keep_background, keep_awake, space_cli (each {value, source}; the keys are the API's snake_case), limits (api.md).

space.update#

space.update(options?: { name, labels, env, egress, accountSecrets, github, git, keepBackground, keepAwake, spaceCli }): Promise<Space>

Changes settings in one PATCH /spaces/{id}: every field is validated before any is applied. labels and env replace the whole map. null for an account option sets it back to the profile's default. Throws InvalidRequestError (details.field), PermissionDeniedError (insufficient_scope, or plan_restriction for egress open).

await space.update({ github: true, keepBackground: null });

space.fork#

space.fork(options?: { count, labels, inheritLabels, namePrefix, at, idempotencyKey, accountSecrets, github, git, keepBackground, keepAwake, spaceCli }): Promise<Space[]>

count (1–50, default 1) copies at HEAD (or at commit at): files O(1); env and egress copied; the Space's own secrets, processes and service memory not (account secrets reach a fork as any Space of the account). Forks keep the source's profile and account options unless you pass them. inheritLabels: false: the forks get only labels. Retried with one Idempotency-Key. Throws QuotaExceededError, InvalidRequestError (count), NotFoundError.

const attempts = await space.fork({ count: 3, labels: { try: "1" } });
const exits = await Promise.all(attempts.map(async (f) => (await f.run("npm test")).exit));

space.hibernate#

space.hibernate(): Promise<Space>

Hibernates now (any later request wakes it). Throws ConflictError while something runs.

space.wake#

space.wake(): Promise<Record<string, any>>

Wakes now; the result has wake_ms. Not needed before a command: any request wakes the Space.

space.checkpoint#

space.checkpoint(): Promise<Record<string, any>>

Resolves once HEAD and its objects are in the durable store. Throws DurableUnavailableError.

space.delete#

space.delete(options?: { purge }): Promise<void>

Waits for the running command, then deletes the Space; purge: true also deletes the durable copy. Deleting a Space that is already gone is not an error.

space.audit#

space.audit(): Promise<Record<string, any>>

Commits from HEAD with their op_id, changed paths and exit.

Commands#

Three ways to run a command, all exactly once per op_id (concepts.md): run waits (sync; server default 120 s, max 900 s), stream yields live events, start returns an Op at once (stream and async: default 1,800 s, max 7,200 s). A timeoutMs above the maximum is InvalidRequestError with details.max_s. The options (RunOptions, shared by the three):

option meaning
timeoutMs how long the command may run (sent as seconds)
cwd directory relative to the Space root
env non-secret variables for this command
stdin string or Uint8Array (≤ 1 MiB)
opId the command's id (1–64 of [A-Za-z0-9_.:-]); generated if absent. Use your tool-call id
background true: what the command leaves running (cmd &) stays as a job; false: it ends with the command; unset: the Space's keepBackground (sdk: off)
secrets names of the Space's exec-exposed secrets to put in this command's environment
check run only: throw CommandFailedError on a non-zero exit
onStdout, onStderr run only: callbacks with each chunk of text as it arrives (the call then streams)
signal run only (sync): an AbortSignal that aborts the HTTP wait
idleTimeoutMs stream (and run with callbacks): silence before reconnecting, default 45,000 (the server pings every 15 s)

space.run#

space.run(cmd: string, options?: { timeoutMs, cwd, env, stdin, opId, check, onStdout, onStderr, signal, idleTimeoutMs, secrets, background }): Promise<ExecResult>

Runs one command and waits. An infra_error is retried once with the same opId; a node too busy to start the command is retried after its Retry-After. Throws InfraError, CommandTimeoutError, CapacityError (still busy: the command did not run), CommandFailedError (with check), and the API errors (LeaseCapError, CreditsExhaustedError, NotFoundError, …).

const r = await space.run("npm ci && npm test", { cwd: "web", env: { CI: "1" }, timeoutMs: 900_000, opId: toolCall.id });
console.log(r.exit, r.sharkStatus, r.stdout.slice(-2000));

space.stream#

space.stream(cmd: string, options?: { timeoutMs, cwd, env, stdin, opId, check, onStdout, onStderr, signal, idleTimeoutMs, secrets, background }): AsyncGenerator<SseEvent>

Live events: start, stdout, stderr, detached, then exit (the result) or error. A dropped connection resumes from the last event id: each event is yielded exactly once. Throws ConnectionError when the stream cannot be resumed.

for await (const ev of space.stream("make -j8")) {
  if (ev.type === "stdout" || ev.type === "stderr") process.stdout.write(ev.text);
  else if (ev.type === "exit") console.log("exit", ev.data.exit);
}

space.start#

space.start(cmd: string, options?: { timeoutMs, cwd, env, stdin, opId, check, onStdout, onStderr, signal, idleTimeoutMs, secrets, background }): Promise<Op>

Starts the command and returns its Op at once ("wait": false, 202). Come back later, from this or any other process, with space.op(opId).

const op = await space.start("python train.py", { timeoutMs: 7_200_000 });
// later, anywhere: const res = await (await space.op(op.opId)).wait();

space.op#

space.op(opId: string): Promise<Op>

Re-attaches to an op by id. Throws NotFoundError, OpExpiredError (output past 7 days).

space.ops#

space.ops(options?: { state, limit }): Promise<any[]>

Recent ops, newest first (state: "running" or "done").

op.refresh#

op.refresh(): Promise<Op>

Re-reads the op into op.data (state, cmd, result). op.opId and op.space are fields.

op.events#

op.events(after?): AsyncGenerator<SseEvent>

Every event after event id after (default 0), live or after the op ended (kept 7 days), each exactly once.

op.wait#

op.wait(): Promise<ExecResult>

Follows the event stream to the end and returns the result.

op.signal#

op.signal(sig?): Promise<void>

Sends "TERM" (default), "INT", "KILL" or "HUP" to the running command.

ExecResult#

field meaning
opId the command's id
exit the exit code (non-zero is a normal result)
sharkStatus ok, conflict (re-run with a new opId), timeout, infra_timeout, infra_error, capacity_unavailable, signaled (concepts.md)
stdout, stderr output (sync: the last 1 MiB of each stream)
stdoutTruncated true when stdout was cut; op.events() has all of it
commit the commit the command made, or null when it changed no file
changedPaths number of paths changed
wallMs time at the node, end to end
queueMs time waiting for a slot
wakeMs set when the request woke the Space
runS the command's own run time
replayed true when this is the stored result of an earlier request with the same opId
raw the response JSON (raw.stderr_truncated, raw.processes, …)

SseEvent#

field meaning
id the event's sequence number (resume point)
type start, stdout, stderr, detached, exit, error
data the event's JSON (data.exit on exit)
text a stdout/stderr chunk as text (base64 chunks decoded)

Files#

Reads come from the committed tree without starting a VM (and without waking a hibernated Space). Writes are ops: one commit each, exactly once per opId; a write that races a command on the same path is ConflictError (details.paths). Paths are relative to the Space root; one that escapes it is InvalidRequestError (path_outside_space). at: read at a past commit.

space.files.read#

space.files.read(path: string, at?: string): Promise<Uint8Array>

Throws NotFoundError.

space.files.readText#

space.files.readText(path: string, at?: string): Promise<string>
console.log(await space.files.readText("out/report.json"));

space.files.write#

space.files.write(path: string, data: string | Uint8Array, options?: { mode, opId, ifMatch }): Promise<Record<string, any>>

Writes one file (≤ 512 MiB) as one commit; parents are created. mode: e.g. "0755". ifMatch: an etag from stat(): compare-and-swap, ConflictError (precondition_failed) if the file changed.

space.files.stat#

space.files.stat(path: string): Promise<Record<string, string | null>>

{type, size, mode, mtime, etag, commit} without the content (all strings). Throws NotFoundError.

space.files.list#

space.files.list(path?: string, options?: { depth, glob, at }): Promise<FileEntry[]>

The tree under path (default "/"), pages followed. glob: e.g. "**/*.ts".

space.files.remove#

space.files.remove(path: string, options?: { recursive, opId }): Promise<Record<string, any>>

space.files.move#

space.files.move(from: string, to: string, options?: { overwrite, opId }): Promise<Record<string, any>>

space.files.uploadArchive#

space.files.uploadArchive(path: string, tar: Uint8Array, options?: { replace, opId }): Promise<Record<string, any>>

Uploads a tar (plain, gzip or zstd, ≤ 2 GiB) into the directory path as one commit. replace: true: the directory becomes exactly the archive.

space.files.downloadArchive#

space.files.downloadArchive(path: string, options?: { plain, at }): Promise<Uint8Array>

The directory as a tar (plain: true asks for an uncompressed one).

FileEntry#

field meaning
path relative to the Space root
type file, dir or symlink
size bytes
mode e.g. 0644
mtime modification time
hash content hash
target a symlink's target

Background jobs (processes)#

Beta. On a node that keeps background jobs, a process a command leaves running (with background: true, or a Space with keepBackground: true) becomes a job bg-1, bg-2, … in its own VM; later commands reach its ports at 127.0.0.1:<port> (concepts.md). A job is billed (process seconds) while it is awake, and with keepAwake: true it never sleeps: stop jobs you no longer need. The TypeScript SDK lists jobs; to read a job's log or stop it, use the Python SDK (space.processes.logs/signal), the CLI (shark logs, shark kill), or POST /v1/spaces/{id}/processes/{pid}/signal.

space.processes#

space.processes(): Promise<any[]>

Jobs: id (bg-1), op_id, state (running, paused, snapshotted, exited), ports, exit.

await space.run("(timeout 1800 node server.js > srv.log 2>&1 &)", { background: true });
console.log(await space.processes());

Exposed processes and previews#

Live on the hosted node (beta). A server declared on a port starts on the first request, sleeps after idleS (default 30 s) without traffic, wakes on the next request, follows HEAD and is reachable at https://<space-id>-<port>.preview.sharkmagic.si. Private by default: only a browser that opened a one-time login link from the account sees it. An exposed process is billed (service seconds) while it is awake, by size, and not while asleep: remove the ones you no longer need; shark.ports.list({ state: "running" }) finds what is awake. The account caps how many run at once (awake_services in shark.limits()). A node without the ports runner throws UnsupportedError (api.md).

space.ports.declare#

space.ports.declare(port: number, cmd: string, options?: { cwd, env, idleS, public }): Promise<any>

Declares (or replaces, restarting it) the server on port; returns the exposure (port, state, url, ...). env is for non-secret values.

const p = await space.ports.declare(3000, "npm run dev", { cwd: "app", idleS: 60 });
const { login_url } = await space.ports.loginLink(3000);   // hand it to a person; it works once

space.ports.list#

space.ports.list(): Promise<any[]>

Every exposure of the Space: port, state (declared, starting, running, asleep, exited, failed), url, cmd, cwd, idle_s, public, connections, last_request_at (null = no request since the node started), awake_since (null unless running), exit_code, error.

space.ports.get#

space.ports.get(port: number): Promise<any>

space.ports.setPublic#

space.ports.setPublic(port: number, isPublic: boolean): Promise<any>

true: anyone with the URL (every request can wake it); false: one-time links only. PermissionDeniedError (plan_restriction) where public previews are off.

space.ports.sleep#

space.ports.sleep(port: number): Promise<any>

Sleeps now; the next request wakes it.

space.ports.logs#

space.ports.logs(port: number): Promise<string>

space.ports.remove#

space.ports.remove(port: number): Promise<void>

Stops it and revokes every preview cookie.

space.ports.loginLink(port: number, options?: { path }): Promise<any>

{url, public, login_url, expires_at}: login_url logs in whoever opens it, once, within 60 s. Never store, log or paste it.

shark.ports.list#

shark.ports.list(options?: { state }): Promise<any[]>

Every exposure of the account (GET /ports), awake first, with space, space_name and awake. state: a state or an array of them.

Secrets#

Beta. Values are write-only: no call returns one. Default exposure is inject: commands see a placeholder shk_ph_… and the node's egress proxy puts the real value only into requests to the secret's hosts; the value never enters the VM. exec: true puts the value in the environment of commands that list it in secrets: [...] (the response warns secret_exposed_to_code). in: where the value goes: "bearer" (default), "basic", "header:NAME", "query:PARAM", or { kind, name }. preset: "anthropic", "github", "npm", "openai" fill hosts and in (concepts.md). Writing secrets needs secrets:write.

space.secrets holds the Space's own secrets and references to account secrets; shark.secrets holds the account's (set once; apply: "all" reaches every Space whose accountSecrets takes it, apply: "ref" only Spaces that reference it). A Space secret wins over an account secret of the same name.

space.secrets.set#

space.secrets.set(name: string, value: string, options?: { preset, hosts, in, exec, allowEgress }): Promise<any>

Stores a Space secret; resolves {names, warnings} (e.g. scope_host_not_allowed). allowEgress: true also adds the hosts to the Space's egress allowlist.

await space.secrets.set("STRIPE_KEY", key, { hosts: ["api.stripe.com"] });
await space.secrets.set("GITHUB_TOKEN", tok, { preset: "github", allowEgress: true });   // your own GitHub token
await space.secrets.set("DB_URL", url, { exec: true });
await space.run("node migrate.js", { secrets: ["DB_URL"] });

space.secrets.reference#

space.secrets.reference(name: string): Promise<any>

Uses the account secret of this name in this Space (how an sdk Space with accountSecrets: "none" takes one). A name with no account secret is accepted with the warning account_secret_not_found.

shark.secrets.set#

shark.secrets.set(name: string, value: string, options?: { preset, hosts, in, exec, apply }): Promise<any>

Stores an account secret (PUT /secrets). apply: "all" (default) or "ref".

await shark.secrets.set("OPENAI_API_KEY", key, { preset: "openai", apply: "ref" });

The methods below exist on both space.secrets and shark.secrets.

secrets.presets#

secrets.presets(): Promise<any[]>

The provider catalog: name, suggested_name, hosts, in, note.

secrets.list#

secrets.list(): Promise<any[]>

Names, expose, hosts, in, source (space or account), shadowed, missing, excluded, apply: never values.

secrets.get#

secrets.get(name: string): Promise<any>

One entry of list() (the one in effect). Throws NotFoundError.

secrets.rotate#

secrets.rotate(name: string, value: string): Promise<any>

A new value; exposure, hosts and locations stay. Rotate an account secret with shark.secrets.rotate.

secrets.edit#

secrets.edit(name: string, options?: { preset, hosts, in, exec, apply }): Promise<any>

Changes hosts, locations, exposure or apply and keeps the stored value.

secrets.remove#

secrets.remove(name: string): Promise<void>

Removing a name that is not there is not an error.

secrets.usage#

secrets.usage(name: string): Promise<any>

Per host (and, for an account secret, per Space): count, blocked, last_used.

secrets.status#

secrets.status(options?: { agents, presets }): Promise<any>

What is missing and the fix, computed by the node (GET /spaces/{id}/secrets/status, GET /secrets/status; the shark secrets status --json document): scope, secrets, gaps (code, severity, secret, agent, hosts, message, fix) and ok. Gap codes: missing_secret, egress_not_allowed, account_secret_unset (errors), shadowed, placeholder_blocked, egress_unknown (info). Never a value.

shark.spaceDefaults.get#

shark.spaceDefaults.get(): Promise<any>

The account's defaults for Spaces that do not set accountSecrets / github, and effective (what an sdk and a devbox Space get).

shark.spaceDefaults.set#

shark.spaceDefaults.set(options: { accountSecrets, github }): Promise<any>

accountSecrets: "all", "none" or null (back to the profile's). github: true, false or null. Needs secrets:write.

Egress#

Beta. Per-Space outbound policy for the Space's commands and its agent's session VM: profiles (Shark's named, versioned host lists: shark.egress.profiles()) + the Space's own allow hosts − its deny hosts (deny wins). Modes allowlist (default), deny, open (only accounts allowed it: PermissionDeniedError plan_restriction). Private and link-local ranges are always refused. Defaults: SDK Spaces start with the coding profile, devbox Spaces with essentials. At create: egress: { profiles: [...], allow: [...], deny: [...] }, or a name ("deny", "open", "coding", any profile). An agent start adds its harness's profile by itself; space.secrets.set(…, { preset, allowEgress: true }) adds the preset's profile. Applies from the next command; forks inherit it. Writes need network:write. Details: api.md, concepts.md.

const sp = await shark.spaces.create({ egress: { profiles: ["coding", "anthropic-api"], allow: ["db.example.com:5432"] } });
await sp.egress.use("+openrouter", "-pypi");                 // one atomic change; -pypi also takes it out of coding
for (const e of (await sp.egress.get()).effective.entries) console.log(e.host, e.from);
for (const d of await sp.egress.denials({ since: "1h" })) console.log(d.host, d.count, d.suggest);

shark.egress.profiles#

shark.egress.profiles(): Promise<any[]>

Every profile: name, title, kind, about, includes, hosts (host, why, verified), all_hosts, source, checked, not_included, harness.

space.egress.get#

space.egress.get(): Promise<Record<string, any>>

The policy: mode, profiles, allow, deny, auto, auto_profiles, base (old field), source, effective (hosts, entries with from, denied).

space.egress.set#

space.egress.set(options?: { mode, profiles, allow, deny }): Promise<Record<string, any>>

Replaces the policy (PUT); a field left out keeps its current value.

space.egress.use#

space.egress.use(...changes: string[]): Promise<Record<string, any>>

"+name" adds a profile, "-name" removes one (also out of a composite: "-npm" under essentials).

space.egress.allow#

space.egress.allow(...hosts: string[]): Promise<Record<string, any>>

Adds hosts of the Space's own.

space.egress.disallow#

space.egress.disallow(...hosts: string[]): Promise<Record<string, any>>

Removes hosts of the Space's own (not_listed for one that was not there).

space.egress.deny#

space.egress.deny(...hosts: string[]): Promise<Record<string, any>>

Refuses hosts (name or *.suffix) even when a profile or an allow entry has them.

space.egress.undeny#

space.egress.undeny(...hosts: string[]): Promise<Record<string, any>>

Drops deny entries.

space.egress.denials#

space.egress.denials(options?: { since }): Promise<any[]>

Hosts the egress proxy refused (host, port, count, first, last, kinds, suggest, add); never content.

GitHub#

Two ways to give a Space GitHub access:

  • Your account's connection (a person connects once with shark github connect): github: true on the Space (cli.md).
  • Your own GitHub App (a platform whose users install your App; control-plane mode only, otherwise SharkError with code: "needs_control_plane"): register it once, then link each Space to one installation.

shark.githubApp.register#

shark.githubApp.register(appId: string | number, privateKey: string): Promise<any>

privateKey: the App's PEM, sealed by the account service and never returned. Resolves {registered, app_id, slug, verified}. Needs secrets:write; a pair GitHub refuses is github_app_refused.

shark.githubApp.status#

shark.githubApp.status(): Promise<any>

{registered, app_id, slug, linked_spaces}, never the key.

shark.githubApp.remove#

shark.githubApp.remove(): Promise<any>

Forgets the App and every Space link to it.

shark.githubApp.link(space: Space | string, installationId: number, repos?: string[]): Promise<any>

Stores a token of that installation (narrowed to repos) as the Space's GITHUB_TOKEN (github preset, inject); the Space's node keeps it fresh. Resolves the metadata and secret: "GITHUB_TOKEN", never the token. Error codes: github_app_not_registered, github_not_connected (not an installation of your App), github_no_access (a repository outside it).

await shark.githubApp.register(123456, pem);
await shark.githubApp.link(space, 7890, ["site"]);

Usage, quotas and balance#

Usage is counts and durations only (never command text, paths or contents). Live caps: shark.limits(). What is metered: billing.md.

shark.usage.get#

shark.usage.get(options?: { from, to, groupBy }): Promise<any>

Beta. Rows of commands, command_s, command_vcpu_s, service_awake_s, process_awake_s, storage_gib_h, egress_bytes; groupBy: "day", "space" or "kind"; from / to: ISO dates. Needs usage:read.

shark.usage.balance#

shark.usage.balance(): Promise<Record<string, any>>

The prepaid balance in micro-units: balance_micro, paid_micro, promo_micro (control-plane mode; direct mode throws SharkError with code: "config").

API keys#

Beta. Need the admin scope.

shark.keys.list#

shark.keys.list(): Promise<any[]>

Never the secret.

shark.keys.create#

shark.keys.create(options?: { name, scopes }): Promise<any>

The new key's secret is in this response only: store it in your secret manager, never in code or a Space.

shark.keys.rotate#

shark.keys.rotate(keyId: string, overlapS?: number): Promise<any>

A new secret; the old one keeps working for overlapS seconds (default 86,400).

shark.keys.revoke#

shark.keys.revoke(keyId: string): Promise<void>

Differences from the Python SDK#

The same API underneath; these helpers exist only in Python today: space.usage(), space.files.exists(), space.files.watch(), space.files.uploadDir/downloadDir (TypeScript: uploadArchive / downloadArchive with a tar you build), op.output(), space.processes.get/logs/signal (TypeScript: space.processes() lists), run's retryConflict and requestTimeout, create's at and tenant, Shark's caFile and close(), an async client (TypeScript is async already), reading ~/.shark/credentials, and the framework integrations (integrations.md).

Not in the SDK yet#

Services (.shark/services.toml instead), snapshots, webhooks and events, Space-scoped tokens and sub-accounts answer 422 unsupported on the node (api.md); the terminal is coming.