@computesdk/givemeanode
v1.2.2
Published
givemeanode for ComputeSDK - sandboxes served from a per-region warm pool, with a signed credential that keeps the database off the create path
Readme
@computesdk/givemeanode
givemeanode provider for ComputeSDK. Very fast microVM sandboxes.
Installation & Setup
npm install @computesdk/givemeanodeMint a token and put it in a .env file. Any org admin can mint one from
/team or the CLI; it is shown once.
gman token create --name computesdk --workspace my-workspace
# prints GMN_TOKEN=gmnt_... onceGMN_TOKEN=gmnt_your_token
# Optional. Defaults to us-west-2. givemeanode runs an endpoint per region
# and they are not interchangeable for latency, so set this to the one
# nearest your workload.
GMN_API_HOST=https://api.use1.givemeanode.comUsage
import { givemeanode } from '@computesdk/givemeanode'
const compute = givemeanode({ apiKey: process.env.GMN_TOKEN })
const sandbox = await compute.sandbox.create()
const result = await sandbox.runCommand('node -v')
console.log(result.stdout) // v24.19.0
await sandbox.destroy()Configuration
| Option | Type | Default | What it does |
|---|---|---|---|
| apiKey | string | GMN_TOKEN | The gmnt_ org service token. |
| baseUrl | string | GMN_API_HOST, else https://api.givemeanode.com | Which regional endpoint to use. |
| fastToken | 'absorb' \| 'prime' \| 'off' | 'absorb' | See below. |
| transport | 'auto' \| 'http2' \| 'fetch' | 'auto' | One HTTP/2 session for every request, or fetch. See "One connection for a burst". |
| warm | 'connect' \| 'prime' \| 'off' | 'connect' | What construction does with the connection. See "One connection for a burst". |
| connectTimeout | number | 10000 | How long opening the HTTP/2 session may take, in ms. |
| ramGib | number | 2 | Guest memory. memoryMiB / memMiB on create are read too and rounded up to whole GiB; memory is decimal MB, per the shared options. |
| egress | 'open' \| 'none' | account default | Whether the guest can reach the network. Fixed when the guest image is prepared, not per command. |
| execRetries | number | 1 | See "Two behaviours worth knowing about". |
| timeout | number | 120000 | Per-request timeout in ms. |
Asking for more than one core
A sandbox is one vCPU by default. create takes a vCPU count under any of
ComputeSDK's spellings (vcpus, cpus, resources.vcpus), and it is mapped
onto the smallest named instance type that satisfies both dimensions:
// 8 vCPU / 32 GiB (sandbox-lg)
const sandbox = await compute.sandbox.create({ vcpus: 8, memory: 16384 })
// ...or name the size outright, which wins over the hints
const xl = await compute.sandbox.create({ size: 'sandbox-xl' })| Size | vCPU | RAM | Billed as |
|---|---:|---:|---:|
| sandbox-sm (default) | 1 | 2 GiB | 2 GiB |
| sandbox-md | 4 | 8 GiB | 16 GiB |
| sandbox-lg | 8 | 32 GiB | 32 GiB |
| sandbox-xl | 16 | 64 GiB | 64 GiB |
Smallest-that-fits rather than largest is deliberate: asking for 8 vCPUs gets
the 8-vCPU shape, never a 16-vCPU one. An ask past the top of the range clamps
to sandbox-xl and lets the door refuse it with a message naming your own
ceiling, rather than throwing something less useful locally.
Sizes above the default are opt-in per workspace. A new workspace's
sandbox_vcpus ceiling is 4 and its sandbox_ram_gib ceiling is 8, so
sandbox-lg and sandbox-xl are refused until an org admin raises both. The
refusal names the limit and how to raise it, and this provider surfaces that
message verbatim.
A named size and ramGib are mutually exclusive, because the size already
fixes both dimensions. A memory-only ask stays on ramGib, so wanting 6 GiB on
one core gets exactly that instead of being rounded up into a shape you would
be billed more for.
Container images
Pass any container image by digest and givemeanode will run it:
const sandbox = await compute.sandbox.create({
image: 'ghcr.io/acme/task@sha256:abc...',
})Or prepare it once and start many sandboxes from it, which is much faster per sandbox:
const template = await compute.template.create({
image: 'ghcr.io/acme/task@sha256:abc...',
})
const a = await compute.sandbox.create({ templateId: template.id })
const b = await compute.sandbox.create({ templateId: template.id })Three things to know:
- The reference must be digest-pinned.
ghcr.io/acme/task:latestis refused;ghcr.io/acme/task@sha256:<64 hex>is accepted. A tag can be moved to point at different bytes, and the prepared image is cached under the image's identity, so a tag would eventually start a sandbox from content that no longer answers to that name. Read a digest withdocker buildx imagetools inspect <ref>orcrane digest <ref>. - Write the registry host in full.
alpine@sha256:...is refused because it does not say which registry to authenticate to. - The first sandbox from a new image is slow, because the image has to
be prepared before anything can start; every one after it is fast. Use
template.createwhen you know the image up front. The provider prepares a given image only once per process and shares the result, so starting N sandboxes from one image does not prepare it N times.
givemeanode's own curated images are also available by name (sbx-base,
the default, has python3, node 24, git, curl and a compiler; sbx-min and
sbx-task are smaller). Anything containing @sha256: is treated as a
container image; anything else is treated as a curated name.
The signed credential, and why you get it for free
Authenticating a request costs a round trip that presenting a signed
credential does not. givemeanode hands one back on the response to any
request made with your gmnt_ token:
gmn-fast-token: gmns_<compact JWT>
gmn-fast-token-expires: 2026-08-30T19:48:19ZThis package absorbs that offer and presents it automatically. There is
nothing to configure, nothing new to store, and no extra round trip:
the offer rides on a response you were already getting, and your gmnt_
token stays the only secret you hold. Every failure falls back to the
ordinary credential, so a credential that cannot be used is never worse
than not having one.
Three modes, because the right one depends on your shape:
absorb(default) never adds a round trip. Your first request pays the ordinary cost, its response carries the credential, and everything after it is cheaper - including the command that follows that very first create.primepays one request up front, single-flighted across every caller sharing the token, so even the first creates of a burst present the credential. It is oneGET /preview/sandboxesbefore the first create, and that listing crosses to the database: about 115 ms on the us-east door.offnever presents one.
const compute = givemeanode({
apiKey: process.env.GMN_TOKEN,
fastToken: 'prime', // pay the warm-up before the first burst
})The default was absorb through 1.0.x, prime in 1.1.x, and is absorb
again from 1.2.0. prime was measured on 2026-09-09 against a door that
read its database to validate a gmnt_ token, so 100 cold creates queued
100 of those reads (about 600 ms of a 767 ms create). Later that day the
door began answering a gmnt_ from an in-memory replica of its token
table and the read stopped costing anything, while the prime kept costing
its listing: measured from us-east-1 on 2026-09-18 at 100 concurrent
creates over one HTTP/2 session, the median time-to-interactive read 56 to
106 ms with no prime against 166 ms waiting on one.
What it costs, stated plainly: a signed credential is valid for its own
lifetime regardless of what happens to the token behind it, so gman token
revoke stops anything new immediately, but a credential already in a
client's hands keeps working until it expires. Bans behave the same way.
One connection for a burst
On Node this provider speaks HTTP/2 to the door: one session per provider, every request a stream on it, opened when the provider is constructed so the handshake is paid while you are still setting up. Measured from us-east-1 against the us-east door, 100 concurrent create-then-command pairs:
| wire | TTI median | p95 | p99 |
|---|---|---|---|
| fetch, one connection per in-flight request | 211 ms | 315 ms | 338 ms |
| one HTTP/2 session | 40 ms | 44 ms | 45 ms |
The door answered in about 1 ms either way. The difference is the client:
fetch opens one TLS connection per in-flight request, a single-threaded
runtime performs those 100 handshakes one after another, and the median
create waited about 170 ms for its turn before a byte reached the door.
fetch stays as the fallback wherever node:http2 is not available
(browsers, edge runtimes) or a session cannot be opened, and it is what an
injected fetch selects. Set transport: 'fetch' to never open a session,
or transport: 'http2' to use one against a plaintext loopback dev server
too (h2c).
Construction sends nothing on the session: your token first leaves the
process with the first operation. Set warm: 'prime' to pay the signed
credential's warm-up at construction, so even the first create of a burst
presents it, or warm: 'off' to open nothing until the first request. An idle session does not keep a Node
process alive, so a script that makes its requests and returns exits as it
did over fetch. A request whose connection never completes fails at its
own timeout, and a connection that takes longer than connectTimeout
is given up on, with fetch answering from then on.
That window is one credential lifetime and no longer.
Snapshots
A sandbox can be snapshotted, and a snapshot started from. This is the fastest way to get N copies of a prepared environment:
const snapshot = await compute.snapshot.create(sandbox.sandboxId)
const copy = await compute.sandbox.create({ snapshotId: snapshot.id })Supported
| Feature | Supported |
|---|---|
| sandbox.create / getById / list / destroy | yes |
| runCommand (cwd, env, background, timeout) | yes |
| filesystem (read, write, mkdir, readdir, exists, remove) | yes, over runCommand |
| template.create / list / destroy | yes, from a container image |
| snapshot.create / list / delete | yes |
| getUrl (a public HTTPS URL for a port) | yes - see below |
| streaming stdout/stderr | not natively; the SDK's bridge applies |
Two behaviours worth knowing about
Writes go through base64, not a heredoc. A heredoc's body is every line
up to its marker, so it can only ever produce a file ending in a
newline: writing hello and reading it back gave hello\n. Base64 has no
such rounding, needs no escaping (the alphabet is shell-inert), and carries
content a heredoc cannot - no trailing newline, a line equal to the marker,
arbitrary bytes. The cost is that the encoded content rides in one argv
entry, so a single writeFile is bounded by the guest's ARG_MAX,
typically around 1.5 MB of content. Every givemeanode image ships
coreutils, so base64 is present.
A command is retried once, and only for one error. The connection to a
sandbox is re-established on demand, so the first command after a quiet
stretch can find it closed and needs a redial. Nothing else is retried - a
command that ran and failed has an exit code and is returned as a result,
not an exception. In the rare case where the sandbox did receive the
request before the connection dropped, the retry means the command can run
twice, so pass execRetries: 0 for a workload where that matters.
The preview URL
getUrl({ port }) returns a public HTTPS URL that reaches a server
running inside the sandbox - the page a coding agent just wrote, looked at
from outside:
const sandbox = await compute.sandbox.create({ templateId: template.id })
await sandbox.runCommand('sh', ['-c', 'cd app && (npm run dev > /tmp/dev.log 2>&1 &)'])
const url = await sandbox.getUrl({ port: 3000 })Three things decide whether it works:
- The server listens on the sandbox's loopback.
127.0.0.1:3000is what dev servers bind by default and is exactly right; the host connects in over vsock and the guest dials its own loopback, so no packet arrives on the guest's network interface. That also means an exposed port works on a sandbox prepared withegress: 'none'. - The server survives the command that started it.
runCommandwaits for its command's output to end, sonpm run dev &alone hangs the call and then dies with it. Redirect:(npm run dev > /tmp/dev.log 2>&1 &). - The URL is the secret. The hostname carries an unguessable
capability, so treat it like a password; it expires (24h by default) and
dies with the sandbox.
unexposePort(sandbox, port)closes it sooner.
The call is idempotent per port - the same port returns the same URL with a refreshed expiry - so there is no reason to cache one.
Limitations
- An exposed port is HTTP(S) only, and it is a preview URL rather than a
CDN.
getUrl({port})mints a public HTTPS URL that reaches a port inside the sandbox; requests ride one proxy hop, which is right for a dev server or an API you are testing and not for serving production traffic. Streaming, SSE and WebSocket (protocol: 'wss', same URL) pass through. A raw TCP port cannot be exposed. - Container images must be digest-pinned and registry-qualified, and the first sandbox from a new one is slow. See "Container images".
- Regional endpoints are not interchangeable. Set
baseUrlto the one nearest your workload.
