@nodus-ai/equile
v0.4.0
Published
TypeScript SDK for running user-owned Codex, Claude, and other runtime agents through Nodus.
Readme
@nodus-ai/equile
Run authenticated Codex, Claude, Grok, Gemini, Cursor, or Factory agents through Nodus.
Use this package from trusted server-side code only. Never expose NODUS_API_KEY in a browser, Git repository, prompt, screenshot, or log.
Install
npm install @nodus-ai/equileNode.js 18 or newer is required.
Required values
export NODUS_BASE_URL="https://api.equile.tech"
export NODUS_API_KEY="ndk_..."
export NODUS_AGENT_ID="..."NODUS_API_KEYis either scoped to fixed Agent IDs or to one Agent Workspace. A Workspace key dynamically includes Agents added to that Workspace later.NODUS_AGENT_IDidentifies an Agent already created and authenticated in Nodus.- The SDK cannot create Agents or change provider credentials.
Run one task
For a Workspace SDK key, omit agentId. Nodus distributes runs across healthy matching Agents in that Workspace:
const run = await nodus.agentRuns.createAndWait({
provider: "codex",
model: "gpt-5.5",
mode: "goal",
prompt: "Begin the review and write the final report.",
goal: `# Review Goal
## Requirements
- Inspect the complete project.
- Identify the three highest-priority issues.
- Support every finding with concrete evidence.
## Deliverable
Write the completed report to output/report.md.`
});The bundled CLI supports interactive login and persistent local profiles:
npm install -g @nodus-ai/equile@preview
nodus login
nodus auth status
nodus agents listnodus login opens a browser, displays a one-time code, and waits for approval.
The resulting revocable Workspace SDK key is stored in
~/.config/nodus/config.json with file mode 0600. Use --profile NAME or
NODUS_PROFILE for multiple accounts and environments. NODUS_API_KEY and
NODUS_BASE_URL always take precedence for CI and headless use. nodus logout
revokes the key server-side and removes the local profile.
The CLI also accepts direct environment credentials:
export NODUS_API_KEY="ndk_..."
nodus sdk
nodus capabilities
nodus agents list
nodus agents quota --agent "$NODUS_AGENT_ID" --refresh
nodus agents configure --agent "$NODUS_AGENT_ID" --model gpt-5.5 --reasoning high
nodus agents concurrency --agent "$NODUS_AGENT_ID" --max 12
nodus run --provider codex --model gpt-5.5 --instructions "Return exactly: workspace smoke" --wait
nodus runs list --status running
nodus runs get --run <run-id>
nodus runs events --run <run-id>
nodus runs cancel --run <run-id>
nodus runs artifact --run <run-id> --artifact <artifact-id>
nodus session list # Active Sessions only; use --status destroyed --limit 100 for history
nodus session create --agent "$NODUS_AGENT_ID" --persistent
nodus session run --session <session-id> --instructions "Continue the task" --wait
nodus session exec --session <session-id> --command "git status --short"
nodus session events --session <session-id> --after 0
nodus session telemetry --session <session-id>
nodus session telemetry-history --session <session-id> --step-sec 60
nodus session pause --session <session-id>
nodus session resume --session <session-id>
nodus session destroy --session <session-id>Agent-scoped keys remain supported when deterministic routing is required:
Create run.mjs:
import { createNodus } from "@nodus-ai/equile";
const nodus = createNodus({
baseUrl: process.env.NODUS_BASE_URL,
apiKey: process.env.NODUS_API_KEY,
requestTimeoutMs: 15 * 60_000
});
const run = await nodus.agentRuns.createAndWait({
agentId: process.env.NODUS_AGENT_ID,
instructions: "Review this project and return the three highest-priority issues.",
timeoutSeconds: 1800
});
console.log(run.status); // "completed"
console.log(run.finalOutput); // final assistant responseRun it:
node run.mjscreateAndWait() returns an AgentRun directly. create() returns { run }:
const { run } = await nodus.agentRuns.create({
agentId: process.env.NODUS_AGENT_ID,
instructions: "Perform the task."
});
console.log(run.id);Do not read result.id from create(). The ID is result.run.id.
Reliable create-then-wait pattern
Use this pattern for batch jobs and unreliable networks:
const { run } = await nodus.agentRuns.create({
agentId,
instructions,
idempotencyKey: `teacher:${teacherId}:review-v1`,
timeoutSeconds: 7200
});
const completed = await nodus.agentRuns.wait(run.id, {
timeoutMs: 10 * 60 * 60 * 1000,
pollIntervalMs: 5000
});nodus sdk (also available as nodus capabilities) prints the installed SDK
version, every callable client method, feature flags such as Workspace
credential failover and persistent session snapshots, plus the supported CLI
commands. It does not make an authenticated API request, so it also works
before NODUS_API_KEY is configured.
The Session namespace also exposes diff, ports, git, and
terminalTicket for workspace inspection and terminal clients. The Desktop
Runtime namespace exposes browse, read, ports, attach, reveal, and
terminalTicket in addition to lifecycle methods. These owner-wide methods
require a Workspace-scoped SDK key.
nodus workspace build excludes .git from the uploaded Docker context.
Keep generated files, local dependencies, and secrets out of the selected
context directory; the CLI does not reinterpret Docker ignore rules.
Rules:
- Generate a stable
idempotencyKeyfor each logical task. - After
create()returns a run ID, keep waiting on that same ID. - If polling fails, retry
wait(run.id); do not create a new run. - If the create response is lost, call
create()again with the sameidempotencyKey. - Do not log complete prompts or API keys.
Concurrency and queues
Submitting many promises only submits many runs. Actual execution is limited by the Agent's server-side maxConcurrency.
This is Run concurrency, not a sandbox count. Nodus may use one sandbox per Run or multiple isolated runtime sessions inside one sandbox. SDK behavior stays the same.
Railway replica count and NODUS_AGENT_RUN_WORKER_CAPACITY only control internal worker throughput. Postgres enforces maxConcurrency globally across all replicas.
await nodus.agents.update(agentId, { modelId: "gpt-5.5", reasoningEffort: "high" });
await nodus.agents.update(agentId, { modelId: null, reasoningEffort: null }); // Restore provider defaults
await nodus.agents.setRunConcurrency(agentId, 160); // Valid range: 1-160
const created = await Promise.all(
tasks.map((task) => nodus.agentRuns.create({
agentId,
instructions: task.instructions,
idempotencyKey: task.id
}))
);Runs above the available Agent concurrency remain queued and start automatically when a slot becomes available. Queue time does not consume the run's timeoutSeconds; the execution timer starts when Nodus claims the run.
Before starting a replacement batch, inspect existing work instead of blindly creating duplicates:
const queued = await nodus.agentRuns.list({ agentId, status: "queued" });
const running = await nodus.agentRuns.list({ agentId, status: "running" });
console.log({ queued: queued.runs.length, running: running.runs.length });List and cancel old runs
list() returns at most 100 lightweight run summaries. Use nextCursor to continue.
let cursor;
const queuedRunIds = [];
do {
const page = await nodus.agentRuns.list({
agentId,
status: "queued",
cursor,
limit: 100
});
queuedRunIds.push(...page.runs.map((run) => run.id));
cursor = page.nextCursor ?? undefined;
} while (cursor);
await nodus.agentRuns.cancelMany(queuedRunIds);Only runs belonging to Agents allowed by the SDK key are visible or cancellable.
Batch runner
The repository includes examples/batch-agent-runs.mjs. It:
- runs up to 80 tasks concurrently;
- uses a stable SHA-256
idempotencyKeyper task; - waits on the original run ID after creation;
- writes completed results to JSONL;
- can cancel old queued runs before starting.
Input file:
[
{
"instructions": "Review teacher 123 and return JSON.",
"timeoutSeconds": 7200,
"metadata": { "teacherId": "123" }
}
]Run:
node examples/batch-agent-runs.mjs tasks.json results.jsonlCancel old queued runs first:
NODUS_CANCEL_QUEUED=1 node examples/batch-agent-runs.mjs tasks.json results.jsonlFile inputs
Pass short-lived HTTPS URLs as task resources:
const run = await nodus.agentRuns.createAndWait({
agentId,
instructions: "Inspect the attached project.",
resources: [{
url: signedUrl,
path: "input/project.zip",
contentType: "application/zip"
}]
});Never place credentials in resource URLs, filenames, metadata, or instructions.
Send images and other inline files
Small binary payloads (screenshots, photos, PDFs) can be passed directly — no hosting required. Nodus stages the bytes privately and the agent reads the file from its workspace:
import { readFile } from "node:fs/promises";
const run = await nodus.agentRuns.createAndWait({
agentId,
instructions: "Describe what is wrong in the attached screenshot.",
resources: [{
contentBase64: (await readFile("screenshot.png")).toString("base64"),
path: "input/screenshot.png",
contentType: "image/png"
}]
});Sessions accept per-message attachments; the delivered turn references the staged file paths so the agent can open them with its file-reading tools:
await nodus.sessions.runAndWait(session.id, "Compare this design against the implementation.", {
attachments: [{ name: "design.png", contentBase64: (await readFile("design.png")).toString("base64") }]
});The CLI reads local files for you:
nodus run --agent "$NODUS_AGENT_ID" --instructions "Describe the screenshot." --attach ./screenshot.png --wait
nodus session run --session <session-id> --instructions "Look at this photo." --attach ./photo.jpg --waitLimits: about 7 MB per file (10,000,000 base64 characters), 20 MB of inline content per run, and 4 attachments per session message. Attachment names must look like name.ext (letters, digits, _, -). Inline blobs are deleted when the run is cleaned up; session staging blobs are deleted immediately after the file lands in the workspace.
Goal mode
Goal mode is supported only by Codex Agents:
const run = await nodus.agentRuns.createAndWait({
agentId,
mode: "goal",
instructions: "Implement and verify the requested change."
});For all other providers, omit mode or use "standard".
Artifacts
Completed runs can return output artifacts:
for (const artifact of run.artifacts) {
const { url, expiresAt } = await nodus.agentRuns.getArtifactDownloadUrl(run.id, artifact.id);
console.log(artifact.path, url, expiresAt);
}Artifact download URLs are short-lived. Artifact metadata reports available or expired.
Events and streaming
for await (const event of nodus.agentRuns.stream(runId)) {
console.log(event.type, event.payload);
console.log(event.raw?.provider, event.raw?.type, event.raw?.payload);
}Use wait() when only the final result matters. Use stream() when progress events are needed. Both wait for runtime cleanup before finishing.
SDK event methods request full event detail. event.payload is the stable,
normalized client contract; event.raw contains the complete persisted provider
event for rich desktop rendering. Raw payloads are not truncated, but they are
still recursively sanitized before persistence: credentials and internal
infrastructure identifiers are removed or redacted.
Persistent sessions
Agent Runs are temporary and automatically cleaned up. Use Sessions only when the conversation must continue across multiple messages.
A Session is intentionally bound to one Agent identity. Automatic credential failover applies to Workspace-scheduled Agent Runs created with provider and no agentId; it does not silently switch an existing Session to a different Agent. If a fixed Session loses its credential, start a new Workspace-scheduled Run when cross-Agent continuation is required.
const { session } = await nodus.sessions.create({ agentId });
const first = await nodus.sessions.runAndWait(session.id, "Inspect the repository.");
const second = await nodus.sessions.runAndWait(session.id, "Now implement the fix.");
const live = await nodus.sessions.telemetry(session.id);
const history = await nodus.sessions.telemetryHistory(session.id, { stepSec: 60 });
await nodus.sessions.pause(session.id);
await nodus.sessions.resume(session.id);
await nodus.sessions.destroy(session.id);Persistent sandboxes (Claude only)
By default a sandbox is a single-shot workflow: it is reaped when idle and an
agent run destroys it on completion. Pass persistent: true to keep the
sandbox alive as an ongoing workspace instead. Claude only — its tmux harness
accepts appended messages; other providers reject the flag.
// A session whose sandbox is never idle-reaped (max sandbox lifetime, 24h;
// pause/resume snapshots carry it beyond that):
const { session } = await nodus.sessions.create({ agentId, persistent: true });
// An agent run that leaves its workspace running. The prompt drops the
// "deliver files to an output folder" single-shot instructions and treats
// /workspace as an ongoing project. The completed run exposes `sessionId`,
// and follow-up persistent runs on the same agent continue the same live
// workspace:
const run = await nodus.agentRuns.createAndWait({
agentId,
instructions: "Scaffold the service.",
persistent: true
});
await nodus.sessions.runAndWait(run.sessionId, "Now add tests.");Persistent sessions are billed while live; pause(), resume() and
destroy() work as usual.
Errors
SDK failures throw NodusError:
import { NodusError } from "@nodus-ai/equile";
try {
await nodus.agentRuns.wait(runId);
} catch (error) {
if (error instanceof NodusError) {
console.error(error.status, error.code, error.retryAt, error.message, error.data);
}
throw error;
}Common statuses:
400: invalid request;401: invalid SDK key;403: SDK key is not allowed to use the Agent;404: run or session not found;409: runtime/auth conflict;504: the local SDK wait deadline elapsed. The remote run may still be active; callget()orwait()again before creating another run.
Agent Run creation also exposes stable business codes through NodusError.code:
workspace_auth_unavailable: every matching Workspace credential is invalid or unavailable;workspace_capacity_unavailable: usable credentials exist, but every matching Agent is at capacity;quota_exhausted: a specifically selected Agent is rate-limited;NodusError.retryAtcontains the reset time when known.
Session APIs expose stable codes for caller-actionable failures:
invalid_session_request,invalid_session_list_request,invalid_session_list_cursor,invalid_session_message,invalid_session_wait_request;session_not_found,session_turn_conflict,session_turn_not_found;session_not_live,session_not_paused,session_checkpoint_unavailable;desktop_runtime_offline,agent_auth_unavailable,workspace_revision_not_ready.
API reference
agents
list()update(agentId, { modelId?, reasoningEffort? })setRunConcurrency(agentId, maxConcurrency)setConcurrency(agentId, maxConcurrency)— compatibility aliasquota(agentId, { refresh? })
agentRuns
create(input)→{ run }createAndWait(input, waitOptions?)→AgentRunget(runId)list({ agentId?, status?, cursor?, limit? })wait(runId, options?)events(runId, { after? })→ normalized events plus sanitizedrawprovider payloadsstream(runId, options?)cancel(runId)cancelMany(runIds)getArtifactDownloadUrl(runId, artifactId)
sessions
list({ status?, limit?, cursor? })→{ sessions, nextCursor }; defaults to active Sessions onlycreate({ agentId })get(sessionId)telemetry(sessionId)/telemetryHistory(sessionId, { startMs?, endMs?, stepSec? })diff(sessionId, knownHash?)/ports(sessionId, knownHash?)git(sessionId, { action, message?, path? })terminalTicket(sessionId)run(sessionId, text, { attachments? })execWorkspace(sessionId, command, timeoutSeconds?)runAndWait(sessionId, text, options?)— options may includeattachmentswaitForIdle(sessionId, options)events(sessionId, { after? })→ normalized events plus sanitizedrawprovider payloadsstream(sessionId, options?)pause(sessionId)resume(sessionId)destroy(sessionId)
workspaces (runtime images and revisions)
This namespace manages immutable execution environments and interactive builders. Agent Workspace membership and routing are exposed through the Workspace-scoped key plus agents, agentRuns, and sessions.
These methods require a Workspace-scoped SDK key; fixed Agent-scoped keys cannot access owner-wide Runtime Workspace builders or revisions.
create({ name, defaultResourceProfile? })list()/get(workspaceId)/archive(workspaceId)/revisions(workspaceId)startBuilder(workspaceId, { baseRevisionId? })/getBuild(buildId)exec(buildId, command, timeoutSeconds?)listFiles(buildId, path?)/readFile(buildId, path)/writeFile(buildId, path, content)upload(buildId, path, content)/download(buildId, path)publish(buildId)/destroyBuilder(buildId)createContextUpload(workspaceId)buildFromDockerfile(workspaceId, input)/buildFromContext(workspaceId, input)
desktopRuntimes
These methods require a Workspace-scoped SDK key because Desktop Runtimes are owner-wide execution resources rather than properties of one Agent.
list()get(desktopRuntimeId)rename(desktopRuntimeId, name)grants(desktopRuntimeId)grant(desktopRuntimeId, agentId, "full" | "files_only")revokeGrant(desktopRuntimeId, agentId)browse(desktopRuntimeId, { path?, dirsOnly? })/read(desktopRuntimeId, path)ports(desktopRuntimeId, knownHash?)attach(desktopRuntimeId, { name, contentBase64 })reveal(desktopRuntimeId, { path, app? })terminalTicket(desktopRuntimeId)disconnect(desktopRuntimeId); disconnected runtimes are hidden fromlist()andget()
Terminal tickets are short-lived bearer credentials. Pass them directly to the WebSocket client and do not persist or log them.
The same paired computer can serve multiple Agents with independent permissions. files_only grants expose only list, read, and write operations; full grants additionally allow commands, PTYs, local skills/plugins, and local MCP. Desktop daemons use bounded concurrency through NODUS_DESKTOP_CONCURRENCY (default 4, maximum 16). Runs remain queued with desktopWaiting: true while the computer is offline, so execution timeout and Goal turns do not start until it reconnects.
Checklist for coding agents
Before writing a Nodus integration, verify all of these:
- Import from
@nodus-ai/equile, not a repository-relativesdk/distpath. - Read
create()results from{ run }. - Give every logical task a stable
idempotencyKey. - Keep application metadata under your own keys; Nodus rejects reserved runtime and failover metadata keys.
- Retry polling against the same run ID.
- Inspect/cancel old queued runs before replacing a failed batch.
- Treat
timeoutSecondsas execution time, not queue time. - Keep API keys and full prompts out of logs.
