@deus-hq/sdk
v0.10.2
Published
Deus SDK — run AI coding agents in isolated sandboxes
Readme
@deus-hq/sdk
TypeScript SDK for running AI coding agents (Claude) inside isolated cloud sandboxes.
Install
npm install @deus-hq/sdkEnvironment variables
| Variable | Required | Description |
|----------|----------|-------------|
| DEUS_API_KEY | Yes | Your Deus API key (starts with deus_sk_) |
| ANTHROPIC_API_KEY | Yes* | Anthropic API key for the agent (*unless org-level key configured) |
| DEUS_BASE_URL | No | API base URL (defaults to https://api.deusmachine.ai) |
All can also be passed directly to configure() or as options on individual function calls.
Quick start
Level 1: run() — fire-and-forget
One async call. Provisions a sandbox, clones a repo, runs the agent, returns the result.
import { configure, Environment, run } from "@deus-hq/sdk";
configure({
apiKey: process.env.DEUS_API_KEY,
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
});
const env = Environment.from("agnt-base")
.repo("acme/backend"); // Also accepts github.com/acme/backend, full URLs, or SSH
const result = await run({
task: "Add a health-check endpoint at GET /healthz",
environment: env,
});
console.log(result.output); // Agent's final response
console.log(`$${result.cost.usd}`); // Cost in USD
console.log(result.filesChanged); // Files modifiedRunResult
interface RunResult {
id: string; // Session ID
workspaceId: string; // Workspace ID
output: string; // Agent's final text output
cost: Cost; // { inputTokens, outputTokens, usd }
turns: number; // Number of agent turns
duration: number; // Wall-clock time in milliseconds
filesChanged: string[]; // Files the agent modified
}Level 2: stream() — runtime events
Pull-based async iteration over canonical runtime events. Supports multi-turn conversations.
import { configure, Environment, createWorkspace, createSession, stream } from "@deus-hq/sdk";
configure({
apiKey: process.env.DEUS_API_KEY,
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
});
const env = Environment.from("agnt-base").repo("acme/backend");
const handle = await createWorkspace({ environment: env });
const session = await createSession({ workspaceId: handle.id });
for await (const event of stream(session, "Fix the failing tests")) {
switch (event.type) {
case "message.text.delta":
process.stdout.write(event.delta);
break;
case "message.ended":
console.log();
break;
case "message.part.updated":
if (event.part.type === "TOOL") {
console.log(`tool ${event.part.toolName}: ${event.part.state.status}`);
}
break;
case "turn.started":
console.log(`\n--- turn ${event.turnId} ---`);
break;
case "turn.ended":
console.log(`Done — $${event.cost ?? 0}`);
break;
case "session.error":
console.error(event.error.message);
break;
}
}
// Send a follow-up message on the same session
for await (const event of stream(session, "Now add integration tests")) {
// ...
}RuntimeEventStream helpers
stream() returns a RuntimeEventStream with additional methods:
const events = stream(session, "Add tests");
// Convert to Web ReadableStream (for SSE endpoints)
const readable = events.toReadableStream();
// Cancel the agent
events.abort();Level 3: createSessionClient() — reactive UI
Push-based WebSocket client for browser frontends. Maintains full session state with subscribe/notify.
import { createSessionClient, createSessionToken } from "@deus-hq/sdk";
// Server-side: create a short-lived token (don't expose your API key to browsers)
const { token } = await createSessionToken(sessionId);
// Client-side: connect via WebSocket
const client = createSessionClient({
sessionId,
token,
url: "https://api.deus.co",
});
client.subscribe((state) => {
console.log(`Status: ${state.status}`);
console.log(`Text: ${state.turn?.text}`);
if (state.pendingQuestion) {
client.answerQuestion(
state.pendingQuestion.questionId,
state.pendingQuestion.sessionId,
["yes"],
);
}
if (state.pendingPermission) {
client.respondToPermission(
state.pendingPermission.requestId,
state.pendingPermission.sessionId,
{ behavior: "allow" },
);
}
});
client.connect();
client.send("Fix the bug");SessionState
interface SessionState {
connected: boolean;
status: "connecting" | "provisioning" | "ready" | "running" | "error";
history: Message[];
completedTurns: TurnState[];
turn: TurnState | null;
pendingQuestion: PendingQuestion | null;
pendingPermission: PendingPermission | null;
pendingHook: PendingHook | null;
error: string | null;
}Environments
Environments are persistent provisioning recipes. Define once — template, repo, packages, setup steps, secrets — and create many workspaces from it by ID reference. This eliminates repetitive configuration and enables a natural two-phase workflow:
- Define the environment (once, or when the build steps change)
- Create workspaces from it (repeatedly, with just an ID)
Why environments?
Without environments, every workspace creation requires the full config:
// Without environments: repeat the full config every time
const result1 = await run({
task: "Fix the auth bug",
environment: Environment.from("agnt-base")
.repo("https://github.com/acme/backend")
.packages(["postgresql-client"])
.setup(["npm install", "npm run db:migrate"]),
});
const result2 = await run({
task: "Add rate limiting",
environment: Environment.from("agnt-base")
.repo("https://github.com/acme/backend")
.packages(["postgresql-client"])
.setup(["npm install", "npm run db:migrate"]), // duplicated!
});With environments, define once and reference by ID:
// With environments: define once, reuse everywhere
const env = await createEnvironment({
name: "backend-dev",
environment: Environment.from("agnt-base")
.repo("https://github.com/acme/backend")
.packages(["postgresql-client"])
.setup(["npm install", "npm run db:migrate"]),
});
const result1 = await run({ task: "Fix the auth bug", environmentId: env.id, user: "alice" });
const result2 = await run({ task: "Add rate limiting", environmentId: env.id, user: "alice" });Creating an environment
Use the Environment.from() builder to define the provisioning recipe, then persist it with createEnvironment():
import { Environment, createEnvironment } from "@deus-hq/sdk";
const env = await createEnvironment({
name: "backend-dev",
environment: Environment.from("agnt-base")
.repo("https://github.com/acme/backend", "main")
.packages(["postgresql-client", "redis-tools"])
.setup(["npm install"])
.setup(["npm run db:migrate"])
.env({ NODE_ENV: "development" })
.browser(),
});
console.log(env.id); // "019abc..."
console.log(env.name); // "backend-dev"With auto-stored secrets
If you have the secret values at environment creation time, pass them via secrets. The backend stores them encrypted and scopes them to the new environment:
const env = await createEnvironment({
name: "backend-dev",
environment: Environment.from("agnt-base")
.repo("https://github.com/acme/backend")
.setup(["npm install"]),
secrets: {
GITHUB_TOKEN: "ghp_abc123...",
NPM_TOKEN: "npm_xyz...",
},
});
// Equivalent to creating the environment, then calling createSecret()
// for each key with { environmentIds: [env.id] }.User-scoped environments
Environments can be scoped to a specific user. This is useful when end-users of your platform each have their own repos or configurations:
// Org-level template (no userId — created by the integrator)
const orgTemplate = await createEnvironment({
name: "backend-dev",
environment: Environment.from("agnt-base")
.repo("https://github.com/acme/backend")
.setup(["npm install"]),
});
await createSecret("ANTHROPIC_API_KEY", "sk-ant-...");
// User-scoped environment (scoped to alice)
const aliceEnv = await createEnvironment({
name: "my-project",
userId: "[email protected]",
environment: Environment.from("agnt-base")
.repo("https://github.com/alice/my-project")
.setup(["pip install -r requirements.txt"]),
secrets: {
GITHUB_TOKEN: "ghp_abc123...",
},
});Using environments with run() and createWorkspace()
Once an environment exists, reference it by ID:
// One-shot run
const result = await run({
task: "Fix the flaky test in auth.test.ts",
environmentId: env.id,
user: "[email protected]",
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
});
// Or with manual workspace lifecycle
const ws = await createWorkspace({
environmentId: env.id,
userId: "[email protected]",
});
const session = await createSession({ workspaceId: ws.id, user: "[email protected]" });
for await (const event of stream(session, "Refactor the auth module")) {
// ...
}environmentId and inline config (environment) are mutually exclusive — you must use one or the other, not both.
Per-workspace checkout
The environment owns the repo and its default branch. Each workspace can optionally override only the checkout target:
// Use the environment's configured branch, or the repo default branch
await createWorkspace({ environmentId: env.id });
// Checkout an existing branch or ref; provisioning fails if it cannot be resolved
await createWorkspace({
environmentId: env.id,
checkout: "feature/existing-branch",
});
// Create or reuse a task branch from a base branch or commit SHA
await createWorkspace({
environmentId: env.id,
checkout: {
branch: "deus/fix-login",
from: "main",
},
});The same checkout option is available on run() when it auto-creates a workspace:
await run({
task: "Fix the login redirect",
environmentId: env.id,
checkout: { branch: "deus/fix-login", from: "main" },
});Updating an environment
Name is immutable. Config can be updated:
import { updateEnvironment } from "@deus-hq/sdk";
await updateEnvironment(env.id, {
config: {
packages: ["postgresql-client", "redis-tools", "curl"], // added curl
},
});Updates affect future workspaces only — existing running workspaces keep the config they were provisioned with (config is snapshotted at workspace creation time).
Listing environments
import { listEnvironments } from "@deus-hq/sdk";
// List org-level templates only
for await (const env of listEnvironments()) {
console.log(`${env.name} (${env.id})`);
}
// List org templates + a specific user's environments
for await (const env of listEnvironments({ userId: "[email protected]" })) {
console.log(`${env.name} — ${env.userId ?? "org-level"}`);
}Environment builder reference
| Method | Description |
|--------|-------------|
| Environment.from(template) | Create from a template (e.g. "agnt-base") |
| .repo(url, branch?) | Repository to clone, with optional branch |
| .packages(list) | System packages to install via apt-get |
| .setup(commands) | Sequential setup commands (post-clone) |
| .setup(phase, commands) | Sequential setup commands in a specific phase |
| .setupParallel(...branches) | Parallel branches (post-clone) |
| .setupParallel(phase, ...branches) | Parallel branches in a specific phase |
| .env(vars) | Environment variables for setup + agent |
| .secrets(secrets) | Secret values to store encrypted and scope to the created environment |
| .timeout(ms) | Sandbox timeout (30s-24h, default 1h) |
| .metadata(data) | Arbitrary key-value metadata |
| .lifecycle({ onTimeout }) | Sandbox lifecycle ("pause" or "kill") |
| .browser(config?) | Enable agent-browser (headless Chrome) |
| .user(userId) | Scope to a specific user |
Secrets
Secrets store sensitive values (API tokens, credentials) encrypted at rest. Secret names are the environment variable names injected into setup commands and the agent process. A secret can apply to every environment or only to specific environment IDs, so rotating a token takes effect on the next workspace without updating the environment.
Two scopes
- Org-level secrets: Shared across all users in the organization. Typically platform keys like
ANTHROPIC_API_KEY. - User-scoped secrets: Personal to a specific user. Typically personal access tokens like
GITHUB_TOKEN.
When resolving secrets at workspace creation, environment-scoped secrets take priority over global secrets, and user secrets take priority over org secrets of the same name.
Storing secrets
import { createSecret } from "@deus-hq/sdk";
// Org-level secret (no userId)
await createSecret("ANTHROPIC_API_KEY", "sk-ant-...");
// User-scoped secret
await createSecret("GITHUB_TOKEN", "ghp_abc123...", {
userId: "[email protected]",
});
// Environment-scoped secret
await createSecret("DATABASE_URL", "postgres://...", {
environmentIds: [env.id],
});createSecret is an upsert for the same key name, owner, and scope. This makes token rotation straightforward:
// Rotate a token — next workspace creation picks it up automatically
await createSecret("GITHUB_TOKEN", "ghp_NEW_TOKEN...", {
userId: "[email protected]",
});Listing secrets
Returns metadata only — secret values are never returned by the API.
import { listSecrets } from "@deus-hq/sdk";
// Org-level secrets
for await (const secret of listSecrets()) {
console.log(secret.keyName, secret.updatedAt);
}
// User-scoped secrets
for await (const secret of listSecrets({ userId: "[email protected]" })) {
console.log(secret.keyName, secret.updatedAt);
}Deleting secrets
Delete by the stable id returned from listSecrets().
import { deleteSecret } from "@deus-hq/sdk";
await deleteSecret("sec_123");Secret resolution at workspace creation
When a workspace is created from an environment, the backend resolves every secret that applies to that environment:
- User-owned secret scoped to the environment
- Org-owned secret scoped to the environment
- User-owned global secret
- Org-owned global secret
Environment: env_123, user: "[email protected]"
|
+-- "GITHUB_TOKEN" -> user secret scoped to env_123
+-- "ANTHROPIC_API_KEY" -> org global secret
|
Result: { GITHUB_TOKEN: "ghp_...", ANTHROPIC_API_KEY: "sk-ant-..." }Inline Anthropic API key
As an alternative to storing the Anthropic key as a secret, you can pass it inline on every run() or createWorkspace() call:
const result = await run({
task: "Fix tests",
environmentId: env.id,
user: "[email protected]",
anthropicApiKey: "sk-ant-...", // bypasses secrets store
});Typical integration pattern
A complete integration typically looks like this:
import {
configure,
createEnvironment,
createSecret,
Environment,
run,
} from "@deus-hq/sdk";
// 1. Configure the SDK (once, at app startup)
configure({ apiKey: process.env.DEUS_API_KEY });
// 2. Store platform secrets (once, or via admin dashboard)
await createSecret("ANTHROPIC_API_KEY", process.env.ANTHROPIC_API_KEY!);
// 3. Define environments (once per project, or when build steps change)
const env = await createEnvironment({
name: "acme-backend",
environment: Environment.from("agnt-base")
.repo("https://github.com/acme/backend")
.packages(["postgresql-client"])
.setup(["npm install", "npm run db:migrate"])
.browser(),
});
// 4. When a user connects: store their personal token
async function onUserConnect(userId: string, githubToken: string) {
await createSecret("GITHUB_TOKEN", githubToken, { userId });
}
// 5. When a user submits a task: run the agent
async function onTaskSubmit(userId: string, task: string) {
const result = await run({
task,
environmentId: env.id,
user: userId,
onText: (delta) => sendToClient(userId, delta),
onEvent: (event) => {
if (event.type === "workspace.provisioning") sendStatus(userId, "Setting up...");
if (event.type === "workspace.ready") sendStatus(userId, "Running agent...");
},
});
return result;
}Environment configuration (inline)
The Environment.from() builder can also be used inline with createWorkspace() or run() without persisting an environment:
const env = Environment.from("agnt-base")
.repo("github.com/acme/backend", "main")
.packages(["postgresql-client"])
.setup(["npm install", "npm run db:migrate"])
.env({ DATABASE_URL: "postgres://localhost:5432/test" })
.timeout(600_000);
const result = await run({ task: "Run the test suite and fix failures", environment: env });Setup phases
Setup commands run in two phases: pre-clone (before the repo is cloned) and post-clone (after — the default). Multiple .setup() calls accumulate in order.
const env = Environment.from("agnt-base")
.repo("acme/backend")
.setup("pre-clone", ["setup-credentials.sh"]) // runs before git clone
.setup(["npm install"]) // post-clone (default)
.setup(["npm run db:migrate"]); // runs after npm installParallel setup
Use .setupParallel() to run independent branches concurrently. Each branch runs its commands sequentially; branches execute in parallel. The next call waits for all branches to finish.
const env = Environment.from("agnt-base")
.repo("acme/backend")
.setupParallel(
["npm install"], // branch 1
["pip install -r requirements.txt"], // branch 2 (runs concurrently)
)
.setup(["npm run db:migrate"]); // waits for both branches
// With a phase
Environment.from("agnt-base")
.setupParallel("pre-clone",
["setup-aws.sh"],
["setup-gcp.sh"],
);Private repositories
For private repos, store a GitHub token as a secret. The provisioning pipeline uses resolved GITHUB_TOKEN / GH_TOKEN values to configure git credentials before cloning:
// Store the user's GitHub PAT
await createSecret("GITHUB_TOKEN", "ghp_abc123...", { userId: "[email protected]" });
// Create the environment
const env = await createEnvironment({
name: "private-backend",
environment: Environment.from("agnt-base")
.repo("https://github.com/acme/private-repo"),
});
// Workspace creation resolves the token and clones the private repo
const result = await run({
task: "Fix the auth bug",
environmentId: env.id,
user: "[email protected]",
});The repo URL is always clean (https://github.com/...) — tokens are never embedded in the URL.
MCP servers
Attach MCP tool servers to the agent:
const env = Environment.from("agnt-base").repo("github.com/acme/backend");
const result = await run({
task: "Look up the latest issues and fix the top one",
environment: env,
mcpServers: {
github: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-github"],
env: { GITHUB_TOKEN: "ghp_..." },
},
},
});Hooks
Intercept agent lifecycle events. Decision hooks (PreToolUse, Stop) block the agent until you respond:
const env = Environment.from("agnt-base").repo("github.com/acme/backend");
const result = await run({
task: "Refactor the auth module",
environment: env,
hooks: {
PreToolUse: (input) => {
if (input.tool_name === "Bash" && input.tool_input?.command?.includes("rm")) {
return { allow: false, reason: "Destructive commands are not allowed" };
}
return { allow: true };
},
Stop: (input) => {
return { allow: true }; // Let the agent stop
},
},
});Error handling
import { run, isDeusError, Environment } from "@deus-hq/sdk";
try {
const env = Environment.from("agnt-base").repo("github.com/acme/backend");
await run({ task: "Fix tests", environment: env });
} catch (err) {
if (isDeusError(err)) {
console.error(`[${err.code}] ${err.message}`);
// err.code: "MISSING_API_KEY" | "API_ERROR" | "TIMEOUT" | "ABORTED"
// err.statusCode: HTTP status (if from API)
}
}During streaming, errors arrive as events:
for await (const event of stream(session, "Fix tests")) {
if (event.type === "session.error") {
console.error(event.error.message);
if (!event.recoverable) break;
}
}Workspace management
import {
Environment,
createWorkspace,
listWorkspaces,
stopWorkspace,
pauseWorkspace,
resumeWorkspace,
deleteWorkspace,
} from "@deus-hq/sdk";
// Create from inline config
const env = Environment.from("agnt-base").repo("github.com/acme/backend");
const handle = await createWorkspace({ environment: env });
// Or from a persistent environment
const handle2 = await createWorkspace({
environmentId: "019abc...",
userId: "[email protected]",
});
// List, stop, pause, resume, delete
for await (const ws of listWorkspaces({ status: "RUNNING" })) {
console.log(ws.id, ws.status);
}
await pauseWorkspace(handle.id); // Suspend the VM (preserves state)
await resumeWorkspace(handle.id); // Wake it back up
await stopWorkspace(handle.id); // Shut down the VM
await deleteWorkspace(handle.id); // Permanently removeAPI reference
Configuration
| Function | Description |
|----------|-------------|
| configure(options) | Set global defaults (apiKey, baseUrl, anthropicApiKey, logLevel) |
Environments
| Function | Description |
|----------|-------------|
| createEnvironment(options) | Persist a provisioning recipe. Returns EnvironmentHandle |
| updateEnvironment(id, options) | Update config (name is immutable) |
| listEnvironments(options?) | Auto-paginating async generator of EnvironmentSummary |
Secrets
| Function | Description |
|----------|-------------|
| createSecret(keyName, value, options?) | Create or update a secret. Pass userId for user-scoped and environmentIds for environment-scoped |
| updateSecret(keyName, value, options?) | Alias for createSecret |
| listSecrets(options?) | Auto-paginating async generator of SecretMetadata (no values) |
| deleteSecret(id, options?) | Delete a secret by ID |
Workspaces
| Function | Description |
|----------|-------------|
| createWorkspace(options) | Create workspace from environment or environmentId, with optional checkout. Returns WorkspaceHandle |
| listWorkspaces(options?) | Auto-paginating async generator of WorkspaceSummary |
| stopWorkspace(id, options?) | Shut down the sandbox VM |
| pauseWorkspace(id, options?) | Suspend the sandbox VM (preserves state) |
| resumeWorkspace(id, options?) | Resume a paused sandbox |
| deleteWorkspace(id, options?) | Permanently remove workspace and snapshots |
Sessions
| Function | Description |
|----------|-------------|
| createSession(options) | Create a session in a workspace. Returns Session |
| getSession(id, options?) | Get session detail with messages |
| listSessions(options?) | Auto-paginating async generator of SessionSummary |
| createSessionToken(id, options?) | Create a short-lived JWT for browser WebSocket connections |
Execution
| Function | Description |
|----------|-------------|
| run(options) | Fire-and-forget: auto-provisions, runs agent, returns RunResult |
| stream(session, message, options?) | Returns RuntimeEventStream (async iterable of SessionRuntimeEvent) |
| consume(eventStream, sessionId, workspaceId, options?) | Iterate with callbacks and hooks |
| createSessionClient(options) | Push-based WebSocket client for browser UIs |
License
MIT
