Register machines that connect as remotes, inspect their status, and dispatch command executions with streamed output.
Requires API key scopes remotes:read (list, get, read exec output) and remotes:write (register, update, delete, heartbeat, WebSocket connect, dispatch execs, signal runs). Launching a harness chat also needs agents:execute.
Who can do what. A remote is its owner's machine, and whatever reaches it runs as the owner's account. Teammates see it through its visibility. Beyond that it is the owner's unless they share it:
| Action | Who |
|---|---|
| See it in listings and get it | Anyone its visibility admits |
| Operate it: dispatch and signal execs, launch and list harness sessions, place agents on it | The owner, or a use or write share |
| Manage it: update, rename, update belt, policy rules, attachments, delete, shares | The owner, or a write share |
| Heartbeat and the daemon WebSocket | The owner only |
A caller who can read the remote but lacks the grant gets 403 ("this remote is another person's machine and is not shared with you for this"). Anyone else gets 404. Workspace and org admins do not bypass use or write. Set remote tags is the exception: it is gated by manage_policy.
Concepts
| Resource | Description |
|---|---|
| Remote | A machine running belt remote start. It hosts agent harness profiles and optional shell or harness-session execution. |
| Profile | One harness slot on a remote (for example Claude Code). Profiles are created from daemon registration. There is no REST endpoint that creates them. |
| Exec run | One command executed on a remote, with sequenced stdout/stderr output stored for replay |
The daemon registers with POST /remotes, then keeps an outbound WebSocket open at wss://api.inference.sh/ws/remotes/{id} (or your configured API host). The API treats that connection as the source of truth for status; heartbeats only update heartbeat_at. Command output arrives over that socket as remote_exec_output frames and is stored per run.
Exec opt-in. exec_enabled on the remote reflects whether the daemon was started with belt remote start --exec. The API refuses dispatch when it is false, and the daemon refuses the command frame as well.
Agent sessions opt-in. Registration may include agents_enabled: true when the daemon was started with belt remote start --agents. The daemon enforces this flag: without it, harness session frames are refused on the host even if the API sends them.
Connecting a machine only registers harness profiles on the remote — it does not create workspace agents. The first time you launch a harness chat for a profile, the API creates one agent for that profile (for example title Claude Code · work-laptop) and reuses it on later launches. Run behavior is documented under Remote harness runs.
Register remote
POST /remotes
Called by belt remote start on startup. Registration keys on the daemon's public_key for your user and active team (from the API key or X-Team-ID). A machine that registers again with the same key in the same team reuses its existing remote and does not create a duplicate. Registering after a team switch creates a new remote for that team; older team rows age out through the heartbeat staleness sweep (disconnected, then stopped). Each registration also reconciles the harnesses probe results into profiles (one per harness kind) and returns the remote with profiles populated.
Request
| Field | Type | Description |
|---|---|---|
name | string | Display name, sent only with --name. Empty keeps the remote's current name; a new remote is named after system_info.hostname. |
public_key | string | Base64 ed25519 public key that identifies the machine |
remote_version | string | belt version string |
system_info | object | Host metadata (see below) |
exec_enabled | boolean | Whether this daemon accepts command execution (belt remote start --exec) |
agents_enabled | boolean | Whether this daemon hosts harness sessions (belt remote start --agents) |
home | string | Optional. Absolute POSIX home directory of the OS user the daemon runs as (for example /home/you or /Users/you). The API stores it for workspace policy: paths written as ~/… in rules or in remote exec/harness calls resolve against this directory on that machine. Windows-style or relative values are ignored. Not returned on RemoteDTO; re-registration replaces the stored value. Current belt daemons send it on each register. |
harnesses | array | Harnesses discovered on the host (see below) |
The handler does not validate any of these fields as required.
system_info
belt remote sends a subset of the API's SystemInfo object (same JSON shape engines use for host telemetry). Remotes omit engine-only fields such as gpus, docker, engine_version, and hf_cache. Omitted fields default to zero values server-side.
| Field | Type | Description |
|---|---|---|
hostname | string | Machine hostname |
os | string | OS label, for example Ubuntu 24.04 LTS or macOS |
cpus | array | One aggregate entry with name, cores, optional frequency, and usage (0–100, all cores) |
ram | object | total and used in bytes; usage is used memory as a percentage (0–100) |
volumes | array | Optional. Today one entry: the filesystem that holds the user's home directory (name is the home path; size, used, free, and usage in bytes / percent) |
1{2 "hostname": "work-laptop",3 "os": "macOS",4 "cpus": [{ "name": "Apple M3 Pro", "cores": 12, "usage": 14.2 }],5 "ram": { "total": 38654705664, "used": 19327352832, "usage": 50.0 },6 "volumes": [7 { "name": "/Users/you", "size": 994662584320, "used": 512000000000, "free": 482662584320, "usage": 51.5 }8 ]9}Registration includes a full snapshot with the usage fields above. While the daemon stays connected, it attaches a fresh snapshot to every third WebSocket remote_heartbeat frame (~15 seconds at the default 5 second heartbeat interval). The first heartbeat after connect omits system_info because registration already sent one; CPU usage on later snapshots reflects load since the previous collection. The API stores each heartbeat payload on the remote row, so GET /remotes/{id} can show live utilization without re-registering.
The HTTP POST /remotes/{id}/heartbeat endpoint records liveness only; it does not accept system_info.
harnesses entries
Each object describes one agent harness binary found on PATH:
| Field | Type | Description |
|---|---|---|
kind | string | Harness registry id (for example claude, codex, gemini, cursor) |
command | string | Resolved path to the harness binary |
version | string | Harness version from discovery. Optional, best effort. |
logged_in | boolean | true when the harness looks configured on the host, or when a usage probe reports authenticated; false when the probe reports unauthenticated. The daemon never reads credential files. |
capabilities | object | Session capabilities from the daemon's session driver for this harness (see Profile). Omitted when the harness cannot be driven from inference. |
account | object | Who the harness is signed in as (see Profile account). Omitted until a usage probe has run; registration does not wait for one. |
limits | object | Plan rate-limit windows (see Profile limits). Omitted until a usage probe has run. |
A harness kind that is missing from the array is not installed, or has no resolvable binary on PATH.
Profile reconciliation
On each registration, and on each remote_heartbeat that carries harnesses, the API reconciles harnesses into the remote's profiles:
| Input | Profile effect |
|---|---|
New kind | Creates a profile with harness_kind, command, and status |
Existing kind | Updates command, status, and capabilities in place (same profile id) |
logged_in: true | status becomes idle |
logged_in: false | status becomes unavailable |
| Harness no longer reported | The profile is marked unavailable. It is not deleted. |
capabilities omitted | Keeps the profile's previous capabilities (older daemons) |
account or limits present | Stored on the profile. A probe that failed (limits.unavailable.reason: probe_failed) keeps the last good windows. |
A connected daemon started with --agents rediscovers its harnesses, probes each one's account and plan limits (in parallel, 60 second timeout each) and sends the result on a heartbeat right after it connects and then every 5 minutes.
The API never receives harness credentials.
Response
RemoteDTO:
| Field | Type | Description |
|---|---|---|
id | string | Remote ID |
name | string | Display name |
status | string | Lifecycle status (see below) |
heartbeat_at | string | null | Time of the last heartbeat |
system_info | object | null | Host metadata from the daemon (registration and optional WebSocket heartbeat updates) |
remote_version | string | belt version the daemon reported |
exec_enabled | boolean | Whether the host opted in to command execution |
agents_enabled | boolean | Whether the host opted in to harness agent sessions |
tags | string[] | Policy grouping labels. A workspace rule selector tag:<name> applies to every remote that carries <name>. Omitted or empty when unset. |
profiles | array | Harness profiles (see below) |
last_update | object | The last update the remote was asked for and how it went. Omitted when never asked. |
public_key is not part of the response.
Profile
Each ProfileDTO in profiles:
| Field | Type | Description |
|---|---|---|
remote_id | string | Owning remote |
harness_kind | string | Harness registry id (claude, codex, ...) |
display_name | string | Product name from the harness registry (for example Claude Code) |
vendor | string | Vendor name from the registry |
name | string | User label for this slot on the remote |
command | string | Harness binary path on the remote |
args | string[] | Extra launch arguments |
status | string | idle, busy, or unavailable |
max_concurrent | integer | Concurrency cap for this profile |
capabilities | object | What hosted sessions can do (steer, approvals, interrupt, resume, tools, images, files). Omitted when not reported yet; treat as all false. |
account | object | Who the harness is signed in as, from the daemon's last probe or a running session. Omitted until known. |
limits | object | Plan rate-limit windows at checked_at. Omitted until known. |
Profile account
| Field | Type | Description |
|---|---|---|
status | string | authenticated, unauthenticated or unknown |
type | string | subscription, api_key or cloud |
plan | string | Vendor plan id, for example max |
plan_label | string | Display name, for example Max 20x |
email | string | Account email, when the harness reports one |
workspace | string | Organization or workspace name, when reported |
Profile limits
| Field | Type | Description |
|---|---|---|
checked_at | string (RFC 3339) | When the windows were read |
windows | array | One entry per rate-limit window |
unavailable | object | Why there are no windows: reason (unsupported or probe_failed) and optional message |
Each window:
| Field | Type | Description |
|---|---|---|
id | string | Stable id per harness, for example five_hour or seven_day |
kind | string | session, weekly, monthly or other |
label | string | Display name |
used_percent | number | 0 to 100 |
resets_at | string (RFC 3339) | When the window resets; omitted when unknown |
duration_minutes | integer | Window length, when known |
A running harness session can report a newer account and limits between probes.
Profile status | Meaning |
|---|---|
idle | Ready to take a run |
busy | At its concurrency limit |
unavailable | Logged out, removed from the host, rate-limited, or disabled |
Remote lifecycle status:
status | Meaning |
|---|---|
pending | Registered, not yet connected |
running | The daemon holds an open WebSocket to the API |
disconnected | The WebSocket closed. Reversible when the daemon reconnects. |
stopped | Finalized after staying disconnected for about 30 seconds |
Connection vs heartbeat. status follows the daemon's WebSocket connection, not heartbeat age. Opening the socket moves the remote to running from pending, disconnected, or stopped. Closing it (or losing the connection without a clean close) moves a running remote to disconnected. The API reconciles connection leases about every 10 seconds as a safety net: a remote that drops off presence for two rounds is marked disconnected; one that is present but not running is revived.
heartbeat_at is last seen only. WebSocket and HTTP heartbeats stamp it; they do not change status. Use it for display ("last seen") and for live system_info updates on the WebSocket path.
While disconnected, dispatch and harness launch return 409 remote_offline because the daemon is not connected. Exec runs and harness sessions already running on the host keep going through a brief disconnect (for example Wi‑Fi blip). They fail only when the remote reaches stopped — after the disconnect grace — or when you cancel them.
Harness version support
Each profile's support object describes how the installed version compares to versions inference has tested for that harness. The API computes it from the registry when serializing profiles — it does not run the harness binary.
support.level | Meaning |
|---|---|
supported | Inside the tested range |
newer-than-tested | Newer than anything tested. Sessions still run; the UI may warn. |
older-than-tested | Older than the tested range but above the driver's minimum. Sessions still run; the UI may warn. |
older-than-supported | Below a floor the session driver needs (for example a missing RPC or app-server API). Launch and new turns are refused until you upgrade on the host. |
unknown | Version could not be parsed, the harness is not in the registry, or inference has not published a tested range yet. Sessions may still run; treat as a warning. |
| Field | Description |
|---|---|
reason | Human-readable sentence explaining the verdict |
version | Parsed dotted version from the daemon's --version output |
tested_min, tested_max | Oldest and newest versions inference has tested for this harness |
requires | Minimum version the driver depends on, when defined |
upgrade_cmd | Shell command to install the latest release over the one on the machine (from the registry) |
The workspace Remotes machine page maps support onto each launcher card: version beside the vendor; yellow warnings for newer-than-tested, older-than-tested, and unknown on ready cards (sessions still start); a too old pill with reason and a copyable upgrade_cmd when level is older-than-supported (no launch until you upgrade on the host). API clients should mirror that split — warn on untested builds, block launch when below the driver floor. When level is older-than-supported, expect 409 harness_too_old from Launch harness session and 409 conflict from Send chat message on harness-backed chats — both include reason and how to upgrade. The remote daemon applies the same floor before opening a session: it re-reads the installed version at launch (so upgrades since registration count) and refuses with reason and upgrade_cmd when the CLI is still below requires (for example Pi < 0.80.4, Codex < 0.56.0).
List remotes
GET /remotes or POST /remotes/list
Cursor-paginated list of remotes. Requires remotes:read.
1curl -X POST https://api.inference.sh/remotes/list \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{"limit": 20}'Get remote
GET /remotes/{id}
Returns a RemoteDTO with profiles included. Requires remotes:read. Use the id that belt remote start prints when it connects, or one from the list endpoint.
List harness sessions
GET /remotes/{id}/sessions
Asks the connected daemon which harness sessions exist on the machine — ones inference started and ones a person started locally — and whether a process holds each session right now. Use the result to offer continue on an idle session or watch an active one. Requires remotes:read.
The handler forwards remote_sessions_list over the remote WebSocket and waits for remote_sessions_listed (up to 10 seconds). The remote must be running and registered with agents_enabled: true (belt remote start --agents).
Query
| Parameter | Description |
|---|---|
cwd | Optional. When set, only sessions whose working directory matches are returned (same filter as the WebSocket cwd field). |
1curl "https://api.inference.sh/remotes/remote_abc123/sessions?cwd=/home/you/project" \2 -H "Authorization: Bearer inf_your_key"Response
Same shape as the WebSocket remote_sessions_listed frame:
| Field | Type | Description |
|---|---|---|
request_id | string | Correlates with the daemon request |
sessions | array | Harness sessions on disk (metadata only; no transcript bodies). At most 500 entries, newest first. |
errors | array | Optional per-harness store read failures |
Each sessions[] entry:
| Field | Type | Description |
|---|---|---|
harness | string | Harness id (for example claude, codex) |
id | string | Harness session id on disk (pass as resume_session_id when launching) |
cwd | string | Working directory, when known |
title | string | Display title from the harness store, when known |
updated | string | Last metadata update (RFC 3339) |
live | object | Whether a process holds the session now (see below) |
live (SessionLiveness):
| Field | Type | Description |
|---|---|---|
state | string | active, idle, or unknown |
evidence | string | What decided liveness — for example lock-file, no-process, process-in-cwd, recent-write, or none |
heuristic | boolean | true when liveness is inferred rather than proven (for example from process cwd or recent writes) |
pid | integer | OS process id, when known |
detail | string | Extra context from the harness transcript layer |
Errors
| HTTP | Code | Cause |
|---|---|---|
409 | agents_disabled | The daemon was not started with --agents |
409 | remote_offline | The remote is not connected |
504 | remote_timeout | The daemon did not answer within 10 seconds |
Launch harness chat
POST /remotes/{id}/sessions
Opens a new chat with a harness on a connected remote: pick a profile, set the working directory on that machine, and optionally resume a session the harness already has (from List harness sessions). Requires remotes:write and agents:execute.
The remote must be running with agents_enabled: true (belt remote start --agents). The profile must belong to that remote and report non-null capabilities (the daemon can drive the harness).
Request
| Field | Type | Required | Description |
|---|---|---|---|
profile_id | string | Yes | Profile id from GET /remotes/{id} → profiles[].id |
work_dir | string | No | Folder on the remote where the harness session runs. Leading and trailing whitespace is trimmed. Empty uses the daemon default. Stored on the chat as work_dir (folder-bound sessions; see Get chat). |
resume_session_id | string | No | Harness session id from a list entry's id field — continues that session instead of starting fresh. Sets harness_session_id on the new chat. |
1curl -X POST "https://api.inference.sh/remotes/remote_abc123/sessions" \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{5 "profile_id": "prof_xyz789",6 "work_dir": "/home/you/projects/api",7 "resume_session_id": "sess_abc"8 }'Response
Returns a ChatDTO (same shape as Get chat) with agent_id set to the profile's harness agent, work_dir from the request, and harness_session_id when resume_session_id was sent. Send the first user message with POST /agents/run, POST /chats/{id}/messages, or open the chat in the workspace.
The API creates the backing agent on first launch for that profile (harness set to the profile's registry id, profile_id and remote_id bound to the machine). Reconnecting the daemon does not duplicate agents.
Errors
| HTTP | Code | Cause |
|---|---|---|
400 | invalid_request | Missing profile_id or invalid JSON body |
404 | profile_not_found | profile_id is not on this remote |
409 | agents_disabled | The daemon was not started with --agents |
409 | harness_not_drivable | The profile has no session capabilities (harness not driveable from inference) |
List harness catalog
GET /harnesses
Returns every agent harness inference can drive on a remote, from the agentprotocol harness registry. The list includes only registry entries that have a session driver (for example Claude Code, Codex, Pi, and ACP-backed CLIs). Registry entries with no driver — IDE-only agents and backends that only describe a harness — are omitted.
Use this to build launch UIs: show the full set of driveable harness kinds, then match each entry's id to GET /remotes/{id} → profiles[].harness_kind to show which ones that machine installed and whether each profile is idle.
Requires remotes:read.
1curl https://api.inference.sh/harnesses \2 -H "Authorization: Bearer inf_your_key"Response
An array of catalog entries:
| Field | Type | Description |
|---|---|---|
id | string | Harness registry id (claude, codex, gemini, …) |
display_name | string | Product name from the registry |
vendor | string | Vendor name |
driver | string | How the daemon drives sessions: claude-code, codex, pi, or acp |
capabilities | object | Driver defaults for this harness kind: steer, approvals, interrupt, resume, tools |
capabilities here come from the driver implementation, not from a specific machine. Per-host behavior and login state still come from daemon registration and each profile's capabilities and status.
Update remote
POST /remotes/{id}
Updates a remote you own and returns the updated RemoteDTO. Requires remotes:write.
The body is a full remote object. The update writes every mutable field, so a field you omit is reset to its zero value. Send the object from GET /remotes/{id} with your changes applied.
public_key is one of those fields and no response returns it. An update that omits it clears the key, and the next belt remote start on that machine registers a new remote.
belt remote start overwrites status, system_info, remote_version, exec_enabled, and agents_enabled on its next registration, and name only when started with --name.
Update belt on a remote
POST /remotes/{id}/update
Asks the remote's daemon to update belt to the latest release in place, the way an engine updates. Requires remotes:write and manage on the remote (its owner, or a write share). No request body.
The daemon checks the release and answers within a few seconds. When a newer belt is available it then:
- refuses new commands, terminals, agent sessions and prompts (
this remote is updating belt; try again in a minute) - waits for running commands and agent turns to finish, up to 10 minutes
- installs the release while still connected (download, SHA-256 check, replace its own binary)
- restarts with the same arguments the way it was started (detached, as a service or in the foreground), with the same name and settings. On Linux and macOS it restarts in the same process; on Windows it starts a new one.
It reconnects on the new version, which registration records as remote_version. Idle agent sessions and open terminals end with the restart. If the install fails, the daemon takes work again on its current version without restarting, and remote_version stays as it was.
The daemon fetches the release itself from the CLI release manifest; the API never names a binary.
The response is the daemon's first answer. What happens next shows in the remote's last_update:
| Field | Type | Description |
|---|---|---|
state | string | See below |
target | string | Engines: what was asked for (stable, canary or a version). Empty for remotes, which always take the latest release. |
from | string | Version the machine ran when asked |
to | string | Version it installs or was already on, when known |
error | string | Why, for refused and failed |
at | string | When state was reached |
| State | Meaning |
|---|---|
requested | Sent; the daemon hasn't answered. It stays here when the request times out (504 remote_timeout after 30 seconds). |
installing | A different release is being installed; the machine finishes its running work and restarts on it |
installed | The machine reconnected on to |
current | Already on to; nothing installed |
refused | The binary can't replace itself: a development build, a package manager's install, or a directory the machine's user can't write to. error says what to do on the machine. |
failed | The release couldn't be checked, downloaded or installed, or the request couldn't be sent to the daemon after the online check, so nothing will answer. error says why. The machine keeps its version. |
An install that fails after the updating answer turns last_update to failed with the reason (belt v1.28.4 and later). The remote's page shows it under last update.
Response
| Field | Type | Description |
|---|---|---|
request_id | string | Request id |
state | string | updating (draining, then installs and restarts), current (already on the latest), unsupported (installed by a package manager, a development build, or a binary in a directory the daemon's user cannot write), or failed (the release could not be checked) |
from | string | Version the daemon runs |
to | string | Latest release, when known |
error | string | Why it cannot update, for unsupported and failed |
Errors
| HTTP | Code | Cause |
|---|---|---|
409 | remote_offline | The remote is not connected |
504 | remote_timeout | The daemon did not answer within 30 seconds (belt older than the release that added updates in place) |
Set remote tags
PUT /remotes/{id}/tags
Replaces the remote's tags array in full. Requires remotes:write, read access to the remote, and the manage_policy capability on the remote's workspace (otherwise 403). Tags decide which workspace policy rules apply to the machine, so setting them is gated like editing policy rather than by ownership. An invalid tag returns 400 validation_error.
Request
| Field | Type | Description |
|---|---|---|
tags | string[] | Complete tag list for the remote |
Tags are normalized server-side: trimmed, lowercased, deduplicated, and sorted. Each tag must match ^[a-z0-9][a-z0-9_.-]{0,39}$ (lowercase letters, digits, -, _, or .). At most 20 tags per remote.
1{ "tags": ["prod", "eu-west"] }Response
RemoteDTO with the updated tags field.
From the CLI, belt remote tag <id> [tag...] reads the current tags with GET /remotes/{id}, merges additions and --rm removals, then sends this request. See Remotes guide — Tags.
Delete remote
DELETE /remotes/{id}
Deletes a remote you own. Requires remotes:write. From the CLI, belt remote rm <id> sends the same request (confirmation prompt unless --yes).
1belt remote rm remote_abc1232curl -X DELETE https://api.inference.sh/remotes/remote_abc123 \3 -H "Authorization: Bearer inf_your_key"Deleting a row does not stop a daemon still running on the host. Stopping or reinstalling a machine often leaves an old registration; belt remote list hints at belt remote rm when a remote has not been seen for over a week.
Launch harness session
POST /remotes/{id}/sessions
Opens a new chat with a harness profile on a connected remote — for example Claude Code, Cursor, or Pi on your laptop, in a project folder. Requires remotes:write and agents:execute. The remote must have agents_enabled: true, the profile must belong to that remote, and the daemon must report capabilities for the harness (undriveable harnesses are not offered).
The first launch for a profile creates a harness agent (for example Claude Code · work-laptop) and reuses it on later launches. Connecting belt remote alone does not create agents anymore.
Request
| Field | Type | Required | Description |
|---|---|---|---|
profile_id | string | Yes | Profile id from GET /remotes/{id} → profiles[].id |
work_dir | string | No | Folder the harness works in on the remote. Empty uses the daemon default. |
resume_session_id | string | No | Continue a harness session from List harness sessions instead of starting fresh |
Response
Returns a ChatDTO (same shape as Get chat), including:
| Field | Description |
|---|---|
name | Typically {display_name} · {remote name} |
work_dir | Normalized working directory for this chat |
harness_session_id | Set when resume_session_id was provided |
agent_id | The harness agent created or reused for this profile |
Send the first user message with POST /chats/:id/messages or POST /agents/run to start the harness turn.
Runtime on the remote
The chat exists in the API as soon as this endpoint returns; the harness process on the machine starts when the platform asks the daemon to open a session for that chat (typically on the first user message). Progress flows over the remote WebSocket (see Harness session frames):
| Daemon → server event | Meaning |
|---|---|
remote_session_opened | Harness process is ready (harness_session_id, resumed, seq) |
remote_session_event | Sequenced harness output (AgentEvent payload, monotonic seq) |
remote_session_closed | Session ended; error when open or a turn failed |
The daemon waits up to 60 seconds for the harness to finish opening. If the CLI is stuck on sign-in, an update prompt, or another interactive gate, it sends remote_session_closed with an error such as {display name} did not start a session within 1m0s. Check it works in a terminal on this machine: it may be waiting to sign in or to update. The API fails the active run with that reason. A harness that starts after the deadline is closed on the machine so the slot is freed.
When a user message cannot be delivered (no live session on the daemon), the daemon sends remote_session_closed so the run fails instead of staying in progress. When Prompt fails on a live session, the daemon emits an error agent event on the run before reporting failure.
Turn timeouts (belt with agentprotocol v0.12.1+). Opening a session is bounded by 60 seconds (above). Each user message is bounded only until the harness shows its first sign of work — an assistant message, tool call, permission request, plan update, or similar. Session bookkeeping and echoes of the user's prompt do not count. If nothing qualifies within 2 minutes, the daemon emits an error event with code no_response, cancels the in-flight prompt on the harness, and fails the run. After work has started, there is no fixed total prompt timeout; long tool loops can run until the harness completes the turn.
Harness stderr on failures. Session drivers keep the last 4 KiB of the harness process stderr. Open failures, no_response, process exit, and other driver errors append ; agent stderr: … to the error message on the error event and on remote_session_closed when applicable, so API clients and the workspace can surface CLI output without SSH access to the host.
Errors
| HTTP | Code | Cause |
|---|---|---|
400 | invalid_request | Missing or invalid body, or profile_id is empty |
404 | profile_not_found | No profile with that id on this remote |
409 | agents_disabled | The daemon was not started with --agents |
409 | harness_not_drivable | The profile has no reported capabilities |
409 | harness_too_old | Installed harness is below the driver's minimum (profiles[].support.level is older-than-supported). The error message includes what is missing and upgrade_cmd. |
1curl -X POST "https://api.inference.sh/remotes/remote_abc123/sessions" \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{"profile_id": "prof_xyz", "work_dir": "/home/you/project"}'Heartbeat
POST /remotes/{id}/heartbeat
Records a liveness ping and updates heartbeat_at. Requires remotes:write. belt remote sends its heartbeats over the WebSocket. This endpoint is the HTTP equivalent. The body is optional. The response is a bare empty JSON object without the standard envelope:
1{}revived is always false: heartbeats no longer change lifecycle status. Reconnection over the WebSocket moves a remote back to running.
To refresh system_info with live CPU, RAM, and home-disk usage, the daemon sends system_info on every third WebSocket remote_heartbeat frame instead of this HTTP route.
Dispatch exec
POST /remotes/{id}/execs
Runs a command on a connected remote. Returns an ExecRunDTO immediately. Output is read separately. The Exec Runs API page lists every response field.
Request
| Field | Type | Description |
|---|---|---|
command | string | Executable name or path (required) |
args | string[] | Arguments |
cwd | string | Working directory on the remote |
env | string[] | Extra KEY=VALUE entries |
pty | boolean | Run under a pseudo-terminal |
timeout_ms | integer | Timeout in milliseconds (0 = no limit) |
1curl -X POST "https://api.inference.sh/remotes/remote_abc123/execs" \2 -H "Authorization: Bearer inf_your_key" \3 -H "Content-Type: application/json" \4 -d '{"command": "uname", "args": ["-a"]}'Errors
| HTTP | Code | Cause |
|---|---|---|
400 | invalid_request | command is empty or the body is not valid JSON |
403 | exec_disabled | The daemon was not started with belt remote start --exec |
409 | remote_offline | The remote is not connected |
Get exec run
GET /execs/{id}
Returns an ExecRunDTO: command, status, exit code, timing, and last_seq (highest output sequence number).
status | Terminal? | Meaning |
|---|---|---|
pending | No | Created, not yet sent to the host |
running | No | Sent to the host |
exited | Yes | Process ended. exit_code is set. |
killed | Yes | Ended without an exit code: signal, timeout, cancel, remote disconnect, or the daemon refused the run |
denied | Yes | The API could not deliver the command to the daemon |
List exec runs
GET /execs or POST /execs/list
Cursor-paginated list of exec runs. Requires remotes:read.
Read exec output
GET /execs/{id}/output?after_seq={n}
Returns an array of ExecRunOutputDTO chunks with seq > after_seq, in order, up to 2000 per call. Poll with an increasing after_seq until the run reaches a terminal status.
| Field | Type | Description |
|---|---|---|
seq | integer | Sequence number within the run |
stream | string | stdout or stderr |
data | string | Output bytes, base64-encoded |
1curl "https://api.inference.sh/execs/exec_abc123/output?after_seq=0" \2 -H "Authorization: Bearer inf_your_key"Signal exec
POST /execs/{id}/signal
Kills a running exec. Returns {"ok": true}. A run that is already terminal returns the same response and nothing is sent.
1curl -X POST "https://api.inference.sh/execs/exec_abc123/signal" \2 -H "Authorization: Bearer inf_your_key"WebSocket protocol
The daemon connects to /ws/remotes/{id} with Authorization: Bearer <api-key> and optional X-Team-ID. Requires remotes:write.
Server to remote:
| Event | Purpose |
|---|---|
remote_exec_start | Start a command (exec_run_id, command, args, cwd, env, pty, timeout_ms) |
remote_exec_signal | Kill a run |
remote_session_open | Start or resume a harness session for a chat (chat_id, harness, cwd, optional resume_session_id, and tools when the profile has capabilities.tools: the platform catalog tool definitions) |
remote_session_tool_result | Answer a catalog tool call from the harness (chat_id, call_id, text, optional is_error). See Platform catalog tools |
remote_session_prompt | Send a user message for a turn (chat_id, run_id, text, optional files: each {uri, filename, content_type, size}). Daemons older than belt v1.21.0 read only text |
remote_session_interrupt | Cancel the turn in flight, keep the session (chat_id) |
remote_session_resolve | Answer a harness permission request (chat_id, request_id, decision, optional scope, reason). scope is once (one prompt) or session (the chat already allows that tool, or the person chose always allow; the harness may skip further prompts for that tool this session). See Remote harness runs. |
remote_session_close | End the chat's session (chat_id) |
remote_session_ack | Drop buffered events stored through seq (chat_id, seq) |
remote_session_replay | Resend buffered events after after_seq (chat_id, after_seq) |
remote_sessions_list | List harness sessions on disk (request_id, optional cwd filter) |
remote_update | Update belt in place (request_id); see Update belt on a remote |
Remote to server:
| Event | Purpose |
|---|---|
remote_heartbeat | Last-seen ping (updates heartbeat_at). Optional system_info on every third beat (~15s at default cadence) with live CPU, RAM, and home volume usage; stored on the remote when present. Optional harnesses with probed account and limits, right after connect and every 5 minutes, reconciled into profiles as at registration. Lifecycle status follows the socket, not each beat. |
remote_exec_output | Sequenced stdout/stderr chunk |
remote_updating | Answer to remote_update (request_id, state, from, to, error) |
remote_exec_exit | Run finished (exit_code, error, timed_out, duration_ms) |
remote_session_opened | Session is live (chat_id, harness_session_id, resumed, seq) — seq is the last event number the session has produced (0 for a process just launched; resets when the harness process restarts) |
remote_session_event | One harness event (chat_id, seq, event, optional gap on replay) |
remote_session_closed | Session ended (chat_id, optional error, for example when agents_enabled is false) |
remote_session_tool_call | The harness called a platform catalog tool (chat_id, run_id, call_id, name, arguments, optional tool_invocation_id). The API runs it and answers with remote_session_tool_result |
remote_sessions_listed | Answer to remote_sessions_list (request_id, sessions[], optional per-harness errors[]) |
Each entry in sessions includes harness, id, cwd, title, updated, and live (whether a process holds the session, with evidence and heuristic flags from the harness transcript layer). Listing reads metadata only, never transcript bodies.
Harness session frames require agents_enabled: true on the remote (set when the daemon starts with belt remote start --agents). The protocol also defines remote_terminal_* frames for interactive PTY sessions on the host. Opening, prompting, and closing harness sessions happens only over this socket; GET /remotes/{id}/sessions is the HTTP wrapper for listing metadata.
Ordering. The daemon handles incoming frames in parallel lanes keyed by chat_id, exec_run_id, or terminal session_id, so one slow harness session does not block exec output or another chat. Outbound frames use the same keys so session and exec events stay in order within each lane.
Open vs launch. remote_session_open reserves the chat's slot before the daemon replies with remote_session_opened; the harness process starts in the background. A remote_session_prompt sent immediately after open waits for that launch instead of racing an empty slot.
Persistence and replay. After each stored remote_session_event, the API sends remote_session_ack with the persisted sequence so the daemon can trim its replay buffer. On reconnect, the API sends remote_session_replay with after_seq set to the chat's stored position (harness_event_seq); events at or below that sequence are ignored. If the daemon restarts and reports a lower sequence in remote_session_opened, the stored position resets.
When a remote transitions to disconnected or stopped, unfinished exec runs and active harness agent runs on that remote fail (not retried). See Exec Runs API — Status and Remote harness runs.
→ Remotes guide — Agent harness sessions
Harness session frames
When agents_enabled is true, the API and daemon exchange harness chat control on the same WebSocket:
Server → remote
| Event | Purpose |
|---|---|
remote_session_open | Start or resume a harness for a chat (chat_id, harness kind, cwd, optional resume_session_id, tools for capabilities.tools profiles) |
remote_session_tool_result | Catalog tool result for a prior remote_session_tool_call |
remote_session_prompt | User message for a turn (chat_id, run_id, text, attached files) |
remote_session_interrupt | Cancel the turn in flight |
remote_session_resolve | Answer a permission prompt (request_id, decision, optional scope) |
remote_session_close | End the chat's harness session |
remote_session_ack | Drop buffered events through seq |
remote_session_replay | Resend events after after_seq |
remote_sessions_list | List harness sessions on the host (HTTP GET /remotes/{id}/sessions uses the same path) |
Remote → server
| Event | Purpose |
|---|---|
remote_session_opened | Open succeeded |
remote_session_event | One sequenced harness event |
remote_session_closed | Session ended (error when not a clean close) |
remote_session_tool_call | The harness called a catalog tool; the API runs it and returns remote_session_tool_result |
remote_sessions_listed | Answer to remote_sessions_list |
Open is bounded by a 60 second deadline on the daemon. See Launch harness session — Runtime on the remote.
Scopes
| Scope | Access |
|---|---|
remotes:read | List and get remotes, list harness sessions, list and get exec runs, read output |
remotes:write | Register, update, and delete remotes, heartbeat, WebSocket connect, dispatch execs, signal runs, launch harness chats (also needs agents:execute), list, add and remove shares (GET /remotes/{id}/shares, POST /remotes/{id}/share, DELETE /remotes/{id}/share/{userId}, DELETE /remotes/{id}/team-share, DELETE /remotes/{id}/org-share) |
agents:execute | Required together with remotes:write for POST /remotes/{id}/sessions |