Connect a Remote Agent
The intended end-to-end runbook for enrolling a machine, minting a token, claiming a task and writing results back
Nothing in this guide works yet
There is no MCP endpoint, no /api/v1 REST surface, no Machine, Agent, AgentToken, Task
or Run table. Following these steps today fails at step one. This page documents the intended
runbook so the design can be reviewed and built against. Every command below is a proposed shape,
not a working call.
OPB Brain does not execute anything. The work happens on a remote workstation running Claude Code or Codex, which reads context out of OPB Brain and writes status, events and artifacts back. This runbook is the full loop for one such workstation.
1. Enroll the machine
A Machine is a physical or remote workstation that can host a runtime. Enrollment records its
identity and capabilities so tasks can be routed to it.
curl -X POST https://api.opb.brain/api/v1/machines \
-H "Authorization: Bearer $OPB_ADMIN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"hostname": "workstation-01",
"os": "linux",
"arch": "x86_64",
"connection": "mcp",
"capabilities": ["rust", "next.js", "browser"]
}'The response carries the machine id (mch_01J…) and its heartbeatTtl. A machine silent past that
TTL marks its agents offline and releases their leases.
2. Mint an AgentToken
Tokens are scoped per agent and per machine so one can be revoked without touching the others. The token value is returned once, at creation, and is hashed at rest. There is no way to read it back.
curl -X POST https://api.opb.brain/api/v1/agent-tokens \
-H "Authorization: Bearer $OPB_ADMIN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"agentId": "agt_01J...",
"machineId": "mch_01J...",
"scopes": ["tasks:read", "tasks:claim", "runs:write", "artifacts:write"],
"expiresAt": "2026-12-31T23:59:59Z"
}'Store it on the workstation, never in the repository:
export OPB_AGENT_TOKEN="opb_at_..."The token carries the agent's toolAllowlist. The MCP server refuses tools outside it rather than
hiding them, so a call to a disallowed tool returns an error, not silence.
3. Point Claude Code at the MCP endpoint
The MCP server is a streamable-HTTP endpoint at /mcp, authenticated with the AgentToken.
claude mcp add --transport http opb-brain https://api.opb.brain/mcp \
--header "Authorization: Bearer $OPB_AGENT_TOKEN"Confirm the identity the server resolved before doing anything else:
> use the opb-brain whoami toolwhoami returns the calling agent, its capabilities, its tool allowlist and any leases it already
holds. If it returns leases, the session is a resume, not a fresh start. Go to step 8.
4. Read the rules instead of guessing them
The specs are exposed as MCP resources, so the rules the server enforces are the rules the agent can read:
spec://domain-model entities, state machine, invariants
spec://state-machine legal task transitions
spec://conventions ids, slugs, tenancy, versioning, provenance
spec://graphql-schema the committed SDLCanonical workflows ship as MCP prompts: start-work, hand-off, report-blocked, close-out.
Run the prompt rather than reinventing the sequence.
5. Claim a task
tasks_claim is an atomic claim of the next ready task. The server picks the highest-priority ready
task matching the capability filter, sets status=in_progress, creates a Run, and returns a lease
with a TTL plus the full working context: brief, acceptance criteria, and the contextRefs the
agent must read before starting.
> use tasks_claim with capabilities ["rust"] and projectId "prj_01J..."The REST equivalent, for a runtime without MCP:
curl -X POST https://api.opb.brain/api/v1/tasks/claim \
-H "Authorization: Bearer $OPB_AGENT_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"agentId": "agt_01J...", "capabilities": ["rust"]}'Two agents claiming at the same moment can never both win. The claim is a single conditional update.
6. Heartbeat while working
Renew the lease at least every leaseTtl / 3, with a one-line human-readable status and a progress
percentage.
> use runs_heartbeat with runId "run_01J...", progress 40,
status "cargo test green; wiring the CLI flag"A runner that stops heartbeating is assumed dead, not done. The lease expires, the Run is marked
abandoned, the task returns to todo and attemptCount increments. Nothing is lost and nothing
double-runs.
7. Write back as you go
comments_poston any decision a human would want to see, and on every blocker. A comment withblocking: trueand no answer moves the task toblocked.artifacts_attachfor the diff, the PR link, and any screenshot proving the work.approvals_requestbefore a destructive or outward-facing action. The task sits inblockeduntil a human decides.documents_upsertfor anything the next agent would otherwise have to rediscover.
8. Resume after a crash
No human help required, and no in-memory state:
whoami → open leases → tasks_get → events_stream?since=<lastEventId> → back to workevents_stream is the append-only timeline. Replaying from the last event id the session saw
reconstructs everything that happened while it was gone.
9. Close the run out honestly
runs_finish records the true outcome: the exit reason, usage, and the PR url. The acceptance
criteria checkboxes must reflect what was actually verified.
> use runs_finish with runId "run_01J...", outcome "succeeded",
prUrl "https://github.com/org/repo/pull/128"A failed run reported as failed is worth more than a green lie. Report failed with the exit reason
and post a comment saying what blocked it.
The state machine will refuse dishonest shortcuts anyway:
in_reviewrequires anexternalRefof kindgithub_pr.ready_to_mergecan only be set by an actor other than the one who setin_review. The implementer never promotes its own work.donerequires every acceptance criterion checked.- Every transition writes an Event. There are no exceptions.
An illegal transition returns 422 invalid_transition with the legal alternatives in the body, so
the next call is always knowable from the last failure.