Environment Service (EaaS)
Rent a sandboxed environment for your agent: warm pool policy, create, exec, files, events, and lifecycle — from TypeScript, Python, or the CLI.
Overview
The Environment Service (EaaS) is a rentable, isolated execution environment for your agent. You:
Point a warm pool policy at your project (optional, operator business)
Create an environment from an official template
Wait for readiness, then run commands and exchange files
Subscribe to the event stream while it works
Pause, wake, or delete it — the platform reclaims the carrier
Every face below is shown in three interchangeable clients (TypeScript SDK, Python SDK, CLI). They send the same wire requests for the same input; pick the one that fits your stack. Faces a client deliberately does not carry are called out instead of skipped silently.
Prerequisites
A tenant + project. Create a tenant in the dashboard; it comes with a default project. All billable EaaS state hangs off (tenant, project).
A key:
sk-prismer-live-… — platform key (dashboard → API keys). Resolves to your operator identity with the full operator scope set, including warm-pool:write. This is what you will follow this guide with.
sk-eaas-… — project machine key. Machine-face scopes only (environments:, exec:, files:*, usage:read, events:read). It intentionally has no warm-pool scope: policy is operator business, not machine business.
ps-eaas-… — principal session token, exchanged at POST /api/v1/sessions for an end user inside one environment. See the API reference for the restricted principal surface.
A template. The official registry currently ships node20-web (profile 2c4g): a Node 20 carrier that answers on port 8080. A name@sha256:… pin is accepted and verified server-side (mismatch → 422 template_unavailable).
All authenticated calls use Authorization: Bearer . Nothing below needs a tenant secret in your process.
Step 1 — Warm pool policy (optional)
A warm pool keeps ready environments on the project so later creates start in seconds instead of minutes. The policy is desired state on the project and is replaced wholesale on every PATCH — omitted fields fall back to 0 / cold, so read the current policy and write the merged one back. The revision you read goes out as the If-Match header (a bare integer); a concurrent edit answers 412 revision_conflict.
import { PrismerClient } from '@prismer/sdk';
const client = new PrismerClient({
baseUrl: 'https://prismer.cloud',
apiKey: process.env.PRISMER_API_KEY!, // sk-prismer-live-… (operator)
});
// Read desired policy + effective pool + cost window.
const pool = await client.projects.warmPool.get(projectId);
if (!pool.success) throw new Error(`${pool.error.code}: ${pool.error.message}`);
console.log('desired', pool.data.desired, 'at revision', pool.data.revision);
// Full replace, If-Match = the revision you just read.
const patched = await client.projects.warmPool.patch(
projectId,
{
policy: {
minReady: 0,
maxReady: 1,
idleRetentionSeconds: 300,
dailyBudgetCredits: '5.000',
onMiss: 'cold',
},
},
{ revision: pool.data.revision },
);onMiss is the miss decision, and the client never second-guesses it. cold (default) provisions on demand; fail answers 503 warm_capacity_unavailable when the pool is empty. Neither SDK retries that 503 and neither rewrites fail into cold — if you want to degrade, do it explicitly, and retry writes by passing the same Idempotency-Key (a fresh key is a new operation).
Step 2 — Create an environment
create is idempotency-keyed and returns 202 with the initial status projection (state: pending, then provisioning). Replaying the same key returns the same environment; the same key with a different body is a 409 idempotency_conflict.
const created = await client.environments.create(
{
projectId,
template: 'node20-web', // or 'node20-web@sha256:…' to pin
profile: '2c4g',
ttlSeconds: 3600, // 60..86400
metadata: { source: 'docs' },
// env: { MY_SECRET: '…' }, // injected after identity binding, never logged
},
{ idempotencyKey: 'docs-create-1' }, // optional; auto-generated when omitted
);
if (!created.success) throw new Error(created.error.code);
const envId = created.data.environmentId;
console.log(created.data.state, created.data.revision, created.data.epoch);The REST response carries the canonical URL in a Location header (/api/v1/environments/{id}).
Step 3 — Wait for readiness
The status projection exposes a readiness triple (sandbox, services, agent) plus a milestones log. readiness.sandbox is the "carrier is up" signal that every topology can deliver; readiness.services flips when the template's service probes pass, which needs the probe to reach the pod (it does from cloud deployments; a local kind cluster without in-cluster DNS will stay false and land the environment in degraded — exec and files still work).
The SDKs also expose waitUntilReady(id, { capability: 'services' }); capability: 'agent' is rejected immediately (capability_unavailable) because agent readiness is not probe-driven yet.
let status = created.data;
for (;;) {
if (status.readiness.sandbox) break;
await new Promise((r) => setTimeout(r, 2000));
const next = await client.environments.get(envId);
if (!next.success) throw new Error(next.error.code);
status = next.data;
}
console.log(status.state, status.startupPath); // running | degraded, cold | warm | wake | restoreStep 4 — Run a command
exec runs a command in a synchronous window. The server answers 200 with an exited view, or 202 with a running handle you read back with getExec (paged by stdout byte offset).
const run = await client.environments.exec(
envId,
{ command: ['sh', '-c', 'printf hello-from-eaas'], timeoutMs: 10000 },
{ idempotencyKey: 'docs-exec-1' },
);
if (!run.success) throw new Error(run.error.code);
if (run.data.status === 'exited') {
console.log(run.data.exitCode, run.data.stdout);
} else {
const later = await client.environments.getExec(envId, run.data.execId);
if (later.success) console.log(later.data.status, later.data.stdout);
}Step 5 — Write and read files
The file face is raw bytes: PUT is application/octet-stream (≤ 1 MB) and answers with size + sha256; GET returns the bytes with the digest in the X-Eaas-File-Sha256 response header. Inside the carrier the same file is at /home/agent/workspace/, so sha256sum there must match what the API reported.
const bytes = new TextEncoder().encode('hello eaas\n');
const put = await client.environments.putFile(envId, 'notes/hello.txt', bytes);
if (put.success) console.log(put.data.size, put.data.sha256);
const got = await client.environments.getFile(envId, 'notes/hello.txt');
if (got.success) console.log(got.sha256, new TextDecoder().decode(got.bytes));Step 6 — Subscribe to events
Everything the platform does to your environment is delivered on a durable, per-tenant event feed. Two transports, one delivery contract:
SSE: GET /api/v1/events with Accept: text/event-stream — the key rides the Authorization header, never the URL.
WebSocket: wss://…/ws/eaas/v1 — first frame {"type":"auth","token":"…"}, then {"type":"subscribe","cursor":"…","environmentIds":["…"]}.
Both replay from cursor before going live, dedupe by eventId, and answer an unreplayable cursor with an eaas.resync signal (pull a fresh snapshot, then re-subscribe). The TypeScript SDK ships a subscriber with exactly that contract; the Python SDK and CLI do not wrap the stream yet — use the raw endpoints there.
import { connectEaasEvents } from '@prismer/sdk'; // also '@prismer/sdk/environment-stream'
const handle = await connectEaasEvents({
baseUrl: 'https://prismer.cloud',
token: process.env.PRISMER_API_KEY!,
transport: 'sse', // or 'ws'
filters: { environmentIds: [envId] },
onEvent: (event) => console.log(event.type, event.cursor, event.payload),
onResync: () => console.log('cursor pruned — re-read state, then resubscribe'),
onEnd: (info) => console.log('stream ended', info), // reconnect with the last cursor
});
// later: handle.disconnect();Step 7 — Lifecycle
pause freezes compute (files and exec refuse while paused), wake brings it back, suspend/delete retire it — delete moves to stopping and the platform reclaims the carrier asynchronously to stopped. All four are idempotency-keyed. An environment whose expiresAt passes is refused for new work even before you delete it.
await client.environments.pause(envId, { idempotencyKey: 'docs-pause-1' });
await client.environments.wake(envId);
await client.environments.delete(envId, { idempotencyKey: 'docs-delete-1' });Error surface
Every face returns one envelope: { success: true, data, requestId } or { success: false, error: { code, message, details }, requestId }. Codes are a closed set — invalid_policy / invalid_request (400), invalid_token (401), budget_exhausted (402), scope_denied (403), not_owned (404 — a missing environment and someone else's environment are indistinguishable on purpose), state_conflict / idempotency_conflict (409), revision_conflict (412), template_unavailable / capability_unavailable (422), quota_exceeded / rate_limited (429), warm_capacity_unavailable / provider_unavailable / pricing_unavailable (503). All three clients surface that code unchanged; transport-level failures (no route, dead socket) are client-synthesized codes and deliberately not unified. The principal-session faces (POST /api/v1/sessions and ps-eaas-… bearers) carry their own 401 codes — publishable_key_invalid / invalid_session / session_expired.
Key rotation & expiry
Machine keys have no TTL — rotation is issue-new-then-revoke-old, and during the window both keys authorize the same tenant (dual-active), so you can cut over callers without a gap. Principal session tokens are the opposite: they always carry an expiry (ttlSeconds, 60..604800) and answer 401 session_expired past it. Key issuance and revocation are operator management actions; the machine-key faces above never rotate anything.
Next steps
API reference: environments — every face, parameter, and code.
Warm pool economics and the usage surface live in the dashboard tenant view.
The event envelope, cursors, and resync semantics are specified in the events API reference.