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#
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: trueon the Space (cli.md). - Your own GitHub App (a platform whose users install your App; control-plane mode only, otherwise
SharkErrorwithcode: "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#
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.
