# Agent experiment API: current room HTTP and MCP

Actuator Science's product direction is a physical experimentation API for agents.
An experiment request should express capabilities, state guarantees, model/runtime
needs and a total authorized cap, then return original evidence, costs and expiry.
The reference below documents implemented room/booking operations in the local
software-simulation prototype. A complete experiment-oriented interface is
planned over these services; no new physical endpoint is delivered by this doc.

Discover published room contracts before booking. For control, read the confirmed
booking's starter descriptor, interface, clocks and actual image before selecting
targets. All private calls use the existing
project-scoped bearer credential; no tool grants credits or hardware access.

## Planned experiment journey and authorization

The target journey is discover capabilities → inspect compatibility/availability →
all-in quote → trusted owner approval → reserve/preflight → bounded run →
results/export → agent analysis. An expert can pin a compatible room; an agent
should be able to request the capability without knowing a numbered room first.
MCP/HTTP/Python share one backend. A human console handles approval and billing.
Installing an integration and its discoverability across agent hosts remain
separate distribution work, not guaranteed automatic model behavior.

The supplied Pro tool names and sample manifest are illustrative designs, not
callable endpoints or a new schema version. Current saved experiment templates,
quotes, holds, bookings, jobs and publication must remain the authority. A future
experiment request pins capability/state/control/runtime/limit identities and
the complete price; changes outside approved scope need re-quote/reapproval.
Trusted approval may be per-experiment or a later standing owner policy. An agent
cannot create approval by calling a tool or writing an approval string. A US$20
total cap is distinct from today's US$20/hour preview and development credits.

Acceptance includes atomic capped reservations under parallel agents/retries,
idempotent start/stop/cancel and auditable cancellation/refund rules, explicit
unusable-scene outcomes, independent stops, applied-action evidence, failures,
interventions, data provenance, reconciled cost and a stated export deadline.
State/compute admission remains mandatory. The supplied 72-hour TTL does not
replace the existing seven-day hosted proposal or preserved local evidence.
Use a shared experiment envelope with explicit simulation/physical mode, numerical
or hardware profile versions and qualification limits; shared structure does not
make simulated and physical evidence equivalent.
Follow [the active package](NEXT_IMPLEMENTATION.md#agent-experiment-contract-within-the-existing-packages)
and [state classes](ROOM_TYPES.md#planned-physical-experiment-classes).

## Workspace and access

The local [customer console](GETTING_STARTED.md#local-customer-workspace) uses
owner sessions. API/SDK callers use bearer credentials; a console cookie grants
no authority on these endpoints. `pal_` service keys and bounded `wsv_` project
tokens preserve their existing semantics. Named `ask_` keys add explicit scopes:

| Grant | Authority |
| --- | --- |
| `read`, one project | Read that project's data, images and retained results; no commands. |
| `read` + `run`, one project | Read, prepare and control experiments, quote and book within the key's existing aggregate development-credit cap. |
| `projects:read` / `projects:create`, workspace | List projects / create projects; no execution or key administration. |
| `manage`, workspace | Read/administer projects and keys, including delegation and revocation; no direct execution, funding or operator controls. |

`read` may also be granted workspace-wide by an authorized issuer. Run grants
always name one project. A manager's child keys cannot extend its expiry or
per-key grant ceiling. Revoking a manager revokes its descendants. These are
shared development-credit limits, not separate wallets or USD payments.

| HTTP operation | Management SDK (`WorkshopClient`) |
| --- | --- |
| `GET /v1/workspace` | `workspace()` |
| `GET /v1/workshop/projects` | `list_projects()` |
| `POST /v1/workshop/projects` | `create_project(name)` |
| `GET /v1/workspace/keys` | `list_api_keys()` |
| `POST /v1/workspace/keys` | `create_api_key(request)` |
| `POST /v1/workspace/keys/{key_id}/revoke` | `revoke_api_key(key_id)` |

Key creation accepts `name`, optional `project_id`, `scopes`, integer
`lifetime_s` (1–2,592,000) and `max_total_credits` (0–10,000). Only an owner/service
credential or workspace manager may issue keys. The secret is returned once;
listings expose metadata, expiry, revoked status and last-used time only. Rotate
by creating a replacement, updating the client, then revoking the old key.
Ordinary run keys cannot create keys, mint `wsv_` tokens, increase their own cap,
fund a workspace, modify inventory or use generic tenant endpoints such as
`/v1/jobs`; use the scoped workshop routes below.

Revocation cancels queued work and requests running work to stop through the
existing worker/lease authority. Expiry is checked at authentication, claim,
heartbeat and live control. Protected local NexArm execution retains stop
evidence and requires operator recovery. An owner or another authorized reader
can still inspect that evidence. The revoked/expired key itself cannot. Separate
host networking and physical stop behavior remain unqualified.

These management methods extend the existing `physical-ai-lab` SDK. The separate
`actuator-rooms-sdk` streaming client and default twelve-tool MCP profile retain
their booked-room scope. [Access architecture](architecture/0054-local-workspace-owners-and-access-keys.md).

## Room exploration and saved experiments

The operator explicitly publishes compatible configurations from reviewed room
quotes; old private scenes are not catalog entries. With `P` equal to
`/v1/workshop/projects/{project_id}`, use these scoped operations:

| Operation | `WorkshopClient` method |
| --- | --- |
| `GET P/farm/explore` | `explore_rooms(project_id)` |
| `POST P/farm/explore/search` with a UTC window | `explore_rooms(project_id, window)` |
| `POST P/farm/rooms/{room_id}/configurations/{configuration_id}/save` | `save_room_experiment(project_id, room_id, configuration_id, request)` |
| `GET P/farm/experiments` | Read this project's saved experiment templates. |
| `POST P/farm/experiments/{experiment_id}/prepare` | `prepare_room_experiment(project_id, experiment_id, request)` |
| `POST P/farm/experiments/{experiment_id}/quotes` | `quote_room_experiment(project_id, experiment_id, request)` |
| `GET P/farm/rooms/{room_id}/calendar/bookings/{booking_id}/observe` | `observe_room_booking(project_id, room_id, booking_id)` |
| `GET P/farm/rooms/{room_id}/calendar/bookings/{booking_id}/export` | `export_room_booking(project_id, room_id, booking_id)` |

Save supplies `name`, current `expected_revision_id`, optional `branch`,
`max_wall_s`, `max_actions`, `max_credits` and `idempotency_key`. It atomically
creates an immutable project revision/template against that head. Prepare takes
an `idempotency_key`; it creates/reuses the compatible private generation or
returns null state for continuous fresh-job execution. Quote takes `trial_id`,
`expected_state_id` and `expected_head_job_id`. Copy the latter two from Prepare;
continuous values must be null. New continuous work uses a new trial identity.
For paused generations, the retained state quote service generates the native
trial identity; the setup request does not override that existing behavior.
Quotes are nonspending; repeated requests need not retain the same quote ID.

Use the existing `find_farm_capacity`, `hold_farm_capacity`, `confirm_farm_hold`,
`get_robot_starter`, command, close and cancellation SDK methods after quoting.
Retain exact hold/confirm bodies for retries. Confirmation's Store idempotency
key belongs to the workspace across workshop projects: use a fresh UUID for new
work, and the same UUID only to recover that original operation. A run key must
use its own quote and remains subject to its existing aggregate credit grant.
Saved limits cannot enlarge that grant or the workspace balance.

The bearer API has no cookie fallback. Local owners use explicit
`/v1/console/projects/{project_id}/farm/...` adapters over the same services with
Origin/CSRF checks. These include browser commands, close, holds, confirmation
and cancellation, without creating a long-lived bearer credential or putting
one in a connection URL. Export returns the original binding/task/run and the
existing hashed video/sensing/view/protection exports; failed outcomes stay failed.

Explicit `PAL_MCP_TOOL_PROFILE=onboarding` exposes typed project, configuration,
saved experiment and calendar operations alongside the twelve booked-room tools.
`room` remains the default twelve-tool profile; `full` retains the legacy catalog.
All tool profiles rely on server authorization and qualify no hardware or WAN
access. [Architecture](architecture/0055-published-room-configurations-and-customer-booking.md).

## Room capabilities and independent observations

With the same project prefix `P` and `read` authority:

| Operation | Client |
| --- | --- |
| `GET P/farm/capabilities?limit=25&offset=0` | `WorkshopClient.room_catalog(project_id, limit=25, offset=0)` / `RoomDirectory.catalog(...)` |
| `GET P/farm/rooms/{room_id}/configurations/{configuration_id}/capabilities` | `room_capabilities(project_id, room_id, configuration_id)` / `RoomDirectory.capabilities(...)` |
| `GET P/farm/documentation/{doc_id}` | `room_documentation(project_id, doc_id)` / `RoomDirectory.documentation(...)` |
| `GET P/observations/{job_id}/cameras/{camera_id}` | `ObservationClient.latest(job_id, camera_id)` |

The standalone clients are in `actuator-rooms-sdk` 0.1.6. Catalog pages contain
at most 100 candidates (default 25); `pagination` supplies offset, limit, total
and `has_more`. The returned preview window is five to 130 seconds after capture.
Calendar search remains authoritative for any caller-selected window. Published
configuration/scene identities, six/twelve named control channels, actual clocks,
camera sources/calibration limits, state promises and support gaps are explicit.
Fit, current qualification, readiness and availability are separate facts. A
descriptor remains readable when qualification is unavailable; it grants no
execution authority. Operator publication remains required to share a scene.

Client-support descriptions now distinguish the generic paused loop from the
legacy continuous runner and name SDK 0.1.9's paused NexArm sensing reader.
Packaged discovery guidance revision `room-discovery-2026-10-22@2` explains the
source-associated drive anchor. The existing sensing route is unchanged;
[adapter guidance](SDK_ADAPTERS.md#customer-deltas-and-a-verified-drive-anchor)
shows the read-only composition. `native_target_hz` is a nominal target, not a
delivered minimum; the model fixture's strict 2 Hz requirement remains unqualified.

For NexArm, `cameras[].mount` names the actual compiled parent body: `world`,
`left_gripper_link` or `right_gripper_link`. Native conformance tests bind these
labels to the compiled cameras. A mount label does not supply an extrinsic pose
or qualify physical calibration; retain the descriptor's calibration limits.

Documents use the allowlisted IDs `selection`, `so100`, `nexarm-pair`,
`nexarm-continuous`, `python-policy`, revision `room-discovery-2026-10-22@1`,
and list their exact execution capabilities. These reads acquire no allocation,
quote, hold, job or spending reservation. They also have explicit owner-session
adapters at `/v1/console/projects/{project_id}/...` over the same services.

The observation envelope `actuator.vision-read@1` delivers the latest original
RGB PNG with source schema/digest/sequence, capture/simulation time, control step,
PNG/RGB digests and declared calibration. It has no mandatory robot-state array.
Before the first record, `frame` is null; an undeclared camera is
`camera_unavailable` (404), and corrupt original evidence fails (409). Existing
SO100 capture/decision, paired sensing and continuous video journals supply the
bytes without rendering a replacement. Terminal status and provisional outcome
remain explicit. Read authority alone never permits booking or motion.

The onboarding MCP profile now exposes 35 tools. Added read tools are
`workshop_room_onboarding`, `workshop_room_catalog`, `workshop_room_capabilities`
and `workshop_room_documentation`. With `PAL_API_URL` configured but no key,
onboarding guidance works offline; API tools return `credential_required` and
make no HTTP request. An owner supplies the missing scope, finite execution cap
and consent. A credential/tool is not approval to spend. The default 12-tool
booked-room profile and existing numerical versions retain their meanings.

## Usage, result detail and verified delivery copies

With `P = /v1/workshop/projects/{project_id}`:

| HTTP operation | `WorkshopClient` method |
| --- | --- |
| `GET P/usage` | `usage(project_id, filters=None)` |
| `GET P/usage/snapshot` | The same read projection, suitable for passive polling. |
| `GET P/usage.csv` | Complete filtered CSV, limited to 10,000 runs. |
| `GET P/results/{job_id}` | `result_detail(project_id, job_id)` |
| `GET P/results/{job_id}/snapshot` | Passive result-detail polling. |
| `POST P/runs/{job_id}/exports` with `{}` | `prepare_result_export(project_id, job_id)` |
| `GET P/exports/{export_id}` | `get_result_export(project_id, export_id)` |
| `GET P/exports/{export_id}/download` | `download_result_export(project_id, export_id)` returns verified exact bytes. |

Filters are `room_id`, `key_id`, execution `status`, finite `since_unix` and
exclusive `until_unix`, plus integer `limit` (1–100) and `offset`. Dates and daily
cohorts use UTC job creation time; displayed settlement is current. CSV exports
all matching rows up to its bound, irrespective of page offset. Original events
reconcile each run's authorized, reserved, settled and released development
credits. Missing/mismatched events remain visible. Reserved room windows, worker
elapsed time and native measured wall time have separate meanings; no measured
USD bill or collected revenue is inferred. Pagination is a live read view, not a
historical settlement snapshot.

Result detail includes original inputs, manifest, measurements, provenance,
recorded native image previews, execution errors and scoped stop/recovery
records. Published computation can have failed or unfinished task criteria.
Failed publication has no invented successful result. The bundle adapter covers
the five existing booked-room capabilities; other legacy exports remain available.

Read-authorized scoped keys may prepare a bounded delivery copy; this starts no
execution and grants no new authority. One copy per job/policy is immutable and
idempotent. Metadata exposes byte count, SHA-256, state, absolute expiry and
blocking dependencies. The authenticated download checks original bytes and
binding; it returns an attachment and ETag, with no signed/public URL or redirect.
The SDK enforces the byte bound and verifies the envelope and original manifests.
The independent [reader](../src/physical_ai_lab/workshop/results/reader.py) also
runs directly using only Python's standard library. Verification establishes
integrity and binding, not a publisher signature or physical accuracy.

Normal copies use `local_preserved` and have no automatic expiry. The local
operator's explicitly attested `disposable_fixture_7d` copies expire seven days
from the original terminal record. At expiry, downloads return HTTP 410 even if
a pin postpones deletion. An expired copy stays a tombstone and is never rebuilt
under that identity. Separate preserved local copies/originals retain their own
policy. There is no active hosted policy or automatic sweep. Existing local
results, checkpoints, journals, configurations and financial records are excluded.

Limits are declared in usage/detail: 64 MiB per bundle, 256 MiB per workspace,
1 GiB globally, 256 workspace and 4,096 global metadata records. Oversized copies
are rejected without trimming evidence. Tombstones count against record quotas;
no twelve-month summary deletion rule is active. The local-owner console uses
cookie-only `/v1/console/projects/{project_id}/...` adapters with Origin/CSRF
checks. Its shared balance comes from the same usage read snapshot; it is absent
from scoped bearer responses. Passive snapshots do not renew idle sessions.

The explicit `onboarding` MCP profile adds `workshop_usage`,
`workshop_result_detail`, `workshop_prepare_result_export` and
`workshop_get_result_export`. The default twelve tools stay unchanged.
[Retention architecture](architecture/0056-usage-results-and-managed-export-copies.md).

## Booked-room control

The MCP server defaults to `room`. Set `PAL_MCP_TOOL_PROFILE=full` for the retained
operator/legacy catalog. This changes discovery, not API authorization. HTTP
routes and existing full-profile schemas remain available. The booked-room
server starts without querying the global capability catalog; the API still
checks each operation against the booking and its authority.

Scoped incident records are readable at
`GET P/farm/jobs/{job_id}/protection`, including after failed publication. It
accepts no writes or recovery actions from customer/viewer keys. A blocked pool
reports `resource_health_blocked` through availability and admission. The local
operator uses `physical-lab farm --db PATH recover-health --pool-id ID --document
recovery.json` after process reconciliation, with fresh profile/feedback and the
current incident/version. Pending local incidents are marked `local_pending`
until SQL accepts them. `GET P/farm/jobs/{job_id}/protection/export` returns the
original reports and a content hash even when result publication failed.
`farm reconcile-protection --pool-id ID` persists retained local obligations
after job/process reconciliation; it does not clear resource health.

Automatic guards are enabled only for separately qualified
`workshop.nexarm-continuous.run@2`. Select that capability explicitly in SDK
configuration. Its interface/status includes `protection`, with the original
fault, applied prefix, verified/unknown stop outcome, persistence state and next
steps. Existing accepted retries remain original admission receipts after a stop.
Continuous bookings use connection/starter/task descriptors version 2, with
`fresh_job_no_resume`, null state/head references and an explicit continuously
advancing clock. Version 1 retains paused-generation semantics. The updated
starter supports inspect, observe, bounded act, exact retry, close and export;
streaming forecasts use the SDK. See [client verification](results/PROTECTION_CLIENTS-2026-10-21.md)
and [safety status](SAFETY.md).

For HTTP paths below, `P` is `/v1/workshop/projects/{project_id}` and `L` is
`P/live/{job_id}`. Copy identifiers and request schemas from the descriptor or
`/openapi.json`; do not guess a capability version or reuse another tenant's ID.

| Default MCP tool | HTTP operation | Purpose |
| --- | --- | --- |
| `workshop_get_robot_starter` | `GET P/farm/rooms/{room_id}/calendar/bookings/{booking_id}/starter` | Task, connection, limits and booking clocks. |
| `workshop_get_robot_live` | `GET L` | Decision observation and current action sequence. |
| `workshop_get_robot_interface` | `GET L/interface` | Joint order, bounds, command and media contracts. |
| `workshop_get_robot_live_camera` | `GET L` → `observation.cameras[camera_id]` | Latest decision-camera image. |
| `workshop_get_robot_live_capture` | `GET L/camera` | Latest passive capture and its separate sequence. |
| `workshop_command_robot` | `POST L/interface/commands` | Bounded joint targets tied to current feedback. |
| `workshop_move_robot_camera` | `POST L/interface/views` | Bounded inspection-camera pose request. |
| `workshop_get_robot_camera_view` | `GET L/interface/views/{view_seq}` | Original image from an accepted camera request. |
| `workshop_configure_robot_sensing` | `POST L/interface/sensing` | Supported continuing camera/measurement configuration. |
| `workshop_get_robot_sensing` | `GET L/interface/sensing` | Latest native sensing sample and metadata. |
| `workshop_close_robot_live` | `POST L/close` | Request closure of this job. |
| `workshop_get_run` | `GET P/runs/{job_id}` | Actual publication, result and available exports. |

The selected room determines which optional camera/sensing operations it supports.
MCP images and HTTP bytes are original native observations. Inspect their exposure
time, control tick and sequence; an image can precede the latest decision state.

## Commands and retries

For the paused SO-100 starter, targets contain one to five rows of six joint
angles in radians, in the declared order. NexArm has twelve channels and different
limits. Use the room's typed contract; a six-channel request is not a NexArm action.
Camera positions use declared Z-up world metres.

A fresh command binds the expected action sequence, observation identity,
idempotency key and server acceptance deadline. If its reply is lost, recover
the original request exactly, including deadline. Do not manufacture a new key
or issue a replacement action until you know the original outcome. Inspect the
fresh feedback generation after reconnect.

Close with enough wall time remaining. Accepted Close is not proof of successful
publication or a passed task. Inspect the actual run and export after completion.
Task success can be `true`, `false` or inconclusive; a published job can contain a
failed task. [Starter walkthrough](TRY_A_ROBOT_ROOM.md).

## Booking, continuous SDK and operator operations

The twelve tools operate an existing booking. Calendar availability, hold,
confirmation, cancellation and USD pricing remain HTTP/SDK/operator operations
over the same authority. The [customer SDK](ROBOT_CUSTOMER_SDK.md) handles its own
supported preflight and booking, continuous video, forecast submission, reconnect
and export. The booked-room MCP view does not imply complete SDK transport parity.

Anonymous guides are `/v1/workshop/robot-starter` and `/v1/capabilities`.
`/docs` and `/openapi.json` describe the complete installed HTTP surface.
The [architecture](ARCHITECTURE.md) and [room types](ROOM_TYPES.md) explain scope.
