devices
Device consent layer (M13) — robot-side runners, approval requests an agent may make, approvals only a human session may sign, and the driver's telemetry summaries. No API key can put a policy on a robot.
GET /v1/devices/signing-key
The hub's approval verification key
The public half of the key the hub signs device approvals with, as PEM. A robot-side driver (lucen-device) pins this at registration and verifies every approval token offline against it — signature, expiry, audience and the device binding — so a compromised network path between hub and robot cannot forge an approval. Public: a key nobody can fetch is a key nobody can verify against. ephemeral is true when the deployment runs without DEVICE_APPROVAL_SIGNING_KEY (dev / CI): the key was generated at process start, and every approval it signs dies with the process. (M13)
Responses
| Status | Description | Body |
|---|---|---|
200 | The verification key. | DeviceSigningKey |
401 | Missing or invalid credentials. | Problem |
GET /v1/devices
List devices
Devices the caller may see: their own and those owned by an org they belong to. No device is public. Newest first; an unknown owner is an empty page, not a 404. (M13)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
owner | query | Handle | Only devices owned by this user or org handle. |
limit | query | integer | Page size. |
cursor | query | string | Opaque cursor from the previous page's |
Responses
| Status | Description | Body |
|---|---|---|
200 | Page of devices. | DevicePage |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
422 | Request failed validation. | Problem |
POST /v1/devices
Register a robot-side runner
A robot box declares itself: a name (unique among the owner's connected devices — a disconnected one gives its name back), the robot repo it embodies (must be visible to the caller and of kind robot), the adapter it drives, the io_contract fingerprints it accepts (sha256 of the canonical contract block of a stamped ONNX manifest) and the public half of the Ed25519 keypair the driver generated locally — the hub verifies the driver's telemetry signatures against it. Owner is a user or an org, like a repo. Nothing here grants the device anything: a policy reaches it only through an approval a human signs in the web app. Audited as device.register. (M13)
Request body
| Field | Type | Description |
|---|---|---|
name | Slug | |
robot_repo | string |
|
owner | Handle | null | Owner handle (user or org). Omit to own it yourself. |
adapter | DeviceAdapter | |
accepted_contracts | array of ContractFingerprint | At least one in trust mode |
public_key_pem | Ed25519PublicKeyPem | |
endpoint_capable | boolean | Declare that the adapter implements hold / stop and may consume an endpoint (M15). The driver sets this from the adapter's own code — the |
adapter_info | DeviceAdapterInfo | null | Where the adapter's code came from and which protocols it implements (M17d). Optional; older drivers omit it. |
trust_mode | DeviceTrustMode | |
interface_fingerprint | string | null | The robot card's interface fingerprint the driver pinned before registering (K1). When given it must equal the card's current one ( |
description | string | null |
Responses
| Status | Description | Body |
|---|---|---|
201 | Device registered. | Device |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
422 | Request failed validation. | Problem |
POST /v1/device-pairings
Start pairing a robot from the platform
A person (or the owner's write key) starts connecting a robot to a robot repo and gets a short code (XXXX-XXXX, Crockford base32, about 40 bits) that expires after ten minutes and works once. On the robot, lucen-device pair CODE --hub URL generates the device's Ed25519 key locally and claims the code (claimDevicePairing); no person's API key is ever typed on the robot. What the robot declared then waits for a signed-in person to confirm it (confirmDevicePairing). Owner is a user or an org, like a repo; name, when given, names the device. A device credential cannot start a pairing. (K1)
Request body
| Field | Type | Description |
|---|---|---|
robot_repo | string |
|
owner | Handle | null | Owner handle (user or org) of the device-to-be. Omit to own it yourself. |
name | Slug | null | The device's name; else it is named at confirmation. |
Responses
| Status | Description | Body |
|---|---|---|
201 | Pairing started; | DevicePairing |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
422 | Request failed validation. | Problem |
POST /v1/device-pairings/claim
Declare this robot to a pairing — with a code, or to get one
The robot's driver declares itself: the public half of the key it generated, the adapter and its provenance, the fail-safe probe, the capability and any contracts it registers — what registerDevice takes, minus the robot repo, owner and trust mode a person decides. With code it joins a pairing a person started on the hub; the code is the only credential for this call — a used, expired or unknown code is the same 404. Without code (a robot that ships with the driver) the hub starts the pairing and answers with a code for the robot to show; a signed-in person opens it on the hub. Either way the pairing is then claimed and waits for a person's confirmation; the driver polls collectDevicePairing. Unauthenticated, so counted per caller address (429 past ten attempts a minute) on top of the anonymous rate limit. Nothing is created until a person confirms. (K1)
Request body
| Field | Type | Description |
|---|---|---|
code | string | null | The pairing code, any case, dash optional. Omit to have the hub issue one for the robot to show. |
name | Slug | null | A suggested name; the pairing's or the confirming person's wins. |
adapter | DeviceAdapter | |
adapter_info | DeviceAdapterInfo | null | |
accepted_contracts | array of ContractFingerprint | |
public_key_pem | Ed25519PublicKeyPem | |
endpoint_capable | boolean | |
description | string | null |
Responses
| Status | Description | Body |
|---|---|---|
201 | Claimed; waiting for a person. | DevicePairing |
404 | Resource not found (or hidden from the caller). | Problem |
422 | Request failed validation. | Problem |
429 | Too many claim attempts from this address; retry after | Problem |
GET /v1/device-pairings/{pairing}
Get a pairing — has the robot joined, what did it declare?
By id: the person who started it, the owner user or an owner-org member. By code: any signed-in caller holding the code, while the pairing is not yet confirmed — that is how the robot-issued direction is opened on the hub. Carries what the robot declared (adapter and provenance, probe, capability, key fingerprint) and the interface fingerprint it will pin, so a person can compare the robot's screen with the page before confirming. Anything else is 404. (K1)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
pairing | path | string | A pairing id ( |
Responses
| Status | Description | Body |
|---|---|---|
200 | The pairing. | DevicePairing |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
DELETE /v1/device-pairings/{pairing}
Drop a pairing — "Not my robot"
Ends a pairing that is not yet confirmed: an unused code, or a claim nobody recognises. The robot's next poll is 404. Addressed like getDevicePairing; 409 once confirmed (disconnect the device instead). Not audited: nothing was created. (K1)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
pairing | path | string | A pairing id ( |
Responses
| Status | Description | Body |
|---|---|---|
200 | Rejected. | DevicePairing |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
POST /v1/device-pairings/{pairing}/confirm
Confirm the robot that claimed a pairing (web session only)
session scope: a person looks at what the robot declared — its key fingerprint on the robot's own screen, its adapter, its probe — and says "this is my robot", which no key can look at. Confirming creates the device (audited device.register, by this person), pins the robot card's interface fingerprint on it, and sets its trust mode — chosen here, delivered to the robot when it collects, and afterwards changeable only by the robot itself (setDeviceTrustMode). A robot-issued pairing also takes its robot_repo (and owner) here. 409 unless the pairing is claimed, or when the name is taken; 422 for trust mode registered with no registered contract. (K1)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
pairing | path | string | A pairing id ( |
Request body
| Field | Type | Description |
|---|---|---|
trust_mode | DeviceTrustMode | |
name | Slug | null | The device's name; else the pairing's, else the robot's suggestion. |
robot_repo | string | null | Required for a robot-issued pairing (a robot repo visible to you); must match for a person-issued one. |
owner | Handle | null | For a robot-issued pairing; omit to own it yourself. |
Responses
| Status | Description | Body |
|---|---|---|
200 | Confirmed; | DevicePairing |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
422 | Request failed validation. | Problem |
POST /v1/device-pairings/{pairing_id}/collect
The robot picks up what a person confirmed
The driver polls with a signature by the key it declared, over the canonical JSON of {pairing_id, ts} (ts within five minutes): 202 with the pairing while a person has not confirmed; once confirmed, 200 once with the device (its trust mode included), the hub's approval signing key and the robot interface to pin, and the device credential — an API key bound to this device, shown this once, that can act only as the device (read itself and its approvals, post its signed events, pull the model repos its live approvals name, read its robot card, open its endpoint sessions, upload its trial logs, change its own trust mode); anything else is 403, and disconnectDevice revokes it. Rejected or expired: 404; collected already: 409. (K1)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
pairing_id | path | Id | Pairing id ( |
Request body
| Field | Type | Description |
|---|---|---|
ts | string (date-time) | The robot's clock; within five minutes of the hub's. |
signature | string | base64url Ed25519 signature, by the key the robot declared, over the canonical JSON of |
Responses
| Status | Description | Body |
|---|---|---|
200 | Connected; | DevicePairingClaimed |
202 | Waiting for a person to confirm. | DevicePairing |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
422 | Request failed validation. | Problem |
GET /v1/devices/{device_id}
Get a device
One device. A device the caller may not see is 404, never 403 (docs/API.md, Visibility). Carries what a live page needs without another call (K1): online (a signed event within DEVICE_ONLINE_SECONDS), running_approval_id, the trust mode, the pinned interface fingerprint, the device credential's prefix and last use, and disconnected_at. (M13)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
device_id | path | Id | Device id ( |
Responses
POST /v1/devices/{device_id}/disconnect
Disconnect a device — revoke its credential
The owner (a session, or a write key; never an agent session's key or the device's own) cuts the device off: its device credential is revoked at once (every later call with it is 401), the device is marked disconnected_at, a running approval is asked to stop, and no request, approval or event is accepted for it again (409). A driver that meets the 401 mid-run stops its adapter. The trail stays, and the name is free again: the same robot can be connected under it as a new device. Idempotent. Audited as device.disconnect. (K1)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
device_id | path | Id | Device id ( |
Responses
POST /v1/devices/{device_id}/trust-mode
The robot changes its own trust mode
Only the device: its own credential, and a signature by its own key over the canonical JSON of {device_id, trust_mode, ts} (ts within five minutes) — lucen-device trust MODE on the robot. A person chose the mode when confirming the pairing; after that the hub never flips it: a session or any other key is 403. Switching to registered with no registered contract is 422. Audited as device.trust_mode. (K1)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
device_id | path | Id | Device id ( |
Request body
| Field | Type | Description |
|---|---|---|
trust_mode | DeviceTrustMode | |
ts | string (date-time) | |
signature | string | base64url Ed25519 signature, by the device's registered key, over the canonical JSON of |
Responses
| Status | Description | Body |
|---|---|---|
200 | Changed (or already so). | Device |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
422 | Request failed validation. | Problem |
GET /v1/devices/{device_id}/approval-requests
List a device's approval requests
Newest first; status narrows to one state. (M13)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
device_id | path | Id | Device id ( |
status | query | ApprovalRequestStatus | |
limit | query | integer | Page size. |
cursor | query | string | Opaque cursor from the previous page's |
Responses
| Status | Description | Body |
|---|---|---|
200 | Page of requests. | ApprovalRequestPage |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
422 | Request failed validation. | Problem |
POST /v1/devices/{device_id}/approval-requests
Ask for a policy to be put on a device
An agent or a human asks: "model repo X onto device Y for T minutes". The hub pins the request to a specific ONNX — onnx_path (or the repo's single *.onnx when omitted) and its current sha256 out of the repo's own file rows — so what the human approves is bytes, not a name that could be re-pushed underneath them. When the model repo carries an io_contract.json sidecar, the hub fingerprints its contract block and refuses (409) a request the device has already said it will not accept — a human should not be asked to approve what the robot will refuse. Without a sidecar the fingerprint is null and the driver's own check on the ONNX manifest decides. A device in trust mode approved (K1) also runs a contract it never registered when a signed approval names it and the policy was built for the robot interface the device pinned: the hub then answers 201 when the model's recorded embodiment_fingerprint equals the device's interface_fingerprint, and also for an unstamped policy, whose approval then requires the approver's explicit unstamped_acknowledged (the device additionally compares the policy's joint order with its pinned interface); a policy built for another embodiment, or a device that pinned no interface, is still 409. checks[] says which way the contract was established. A disconnected device is 409. The request appears in the web app as a pending card; nothing moves until a person approves it there. key:write — it is a data mutation, and it grants nothing; a session files and then approves in two calls (one-step deploy by a person). Audited as device.request. With dry_run: true (K1) nothing is filed or audited: the answer is 200 with the request as it would be, checks[], device and model filled — what a deploy page shows before anything exists; a contract the device would refuse is a fail check there instead of the 409. (M13)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
device_id | path | Id | Device id ( |
Request body
| Field | Type | Description |
|---|---|---|
model_repo | string |
|
onnx_path | FilePath | null | Which file in the repo. Omit when the repo holds exactly one |
onnx_sha256 | Sha256 | null | Optional assertion. When given and the repo's current file has a different digest the request is |
minutes | integer | How long the policy may run once approved. The window starts at approval, not at pickup. |
reason | string | null | Why — shown to the human on the pending card. |
endpoint_id | Id | null | M15: ask for the device to consume a hub inference endpoint instead of running the policy itself. The endpoint must be visible to the caller and serve |
dry_run | boolean | K1 — check, do not file: |
Responses
| Status | Description | Body |
|---|---|---|
200 |
| ApprovalRequest |
201 | Request recorded, status | ApprovalRequest |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
422 | Request failed validation. | Problem |
GET /v1/devices/{device_id}/approval-requests/{request_id}
Get an approval request
The consent page in one call (UI-API). Besides the request itself the answer carries who asked and with which credential (requester, plan when a plan's planner filed it), the device it is for (device), what would run (model, and endpoint when the request is endpoint-bound and the endpoint is visible to the caller), and checks[] — what the hub verified against the current state of the hub, computed now: the device's accepted contracts, the control class and where it would run, the device's declared fail-safe probe, the latest sim-gate scorecard for this model on this robot, the policy's embodiment fingerprint against the robot's interface, and whether the pinned bytes are still what the repo (or endpoint) serves. Absent evidence is reported as absent, never as a pass. Checks inform the person deciding; none of them approves or blocks anything.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
device_id | path | Id | Device id ( |
request_id | path | Id | Approval request id ( |
Responses
| Status | Description | Body |
|---|---|---|
200 | The request. | ApprovalRequest |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
POST /v1/devices/{device_id}/approval-requests/{request_id}/deny
Deny an approval request (web session only)
A human says no. session scope: denying is a decision on the same footing as approving, and no API key of any scope may take it — otherwise an agent could clear its own trail of refused requests. 409 when the request is no longer pending. Audited as device.deny. (M13)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
device_id | path | Id | Device id ( |
request_id | path | Id | Approval request id ( |
Request body
| Field | Type | Description |
|---|---|---|
reason | string | null |
Responses
| Status | Description | Body |
|---|---|---|
200 | Denied. | ApprovalRequest |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
422 | Request failed validation. | Problem |
GET /v1/devices/{device_id}/approvals
List a device's approvals
Newest first. status=issued is what a driver polls: approvals a human has signed that have neither been picked up nor expired. The token is returned on every read — it is a signed statement about one device, not a hub credential, and it is useless to anything but that device's driver. device_id may be - for every device the caller can see, and model_repo narrows to one model — where a model has run (K1). (M13)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
device_id | path | Id | Device id ( |
status | query | ApprovalStatus | |
model_repo | query | string |
|
limit | query | integer | Page size. |
cursor | query | string | Opaque cursor from the previous page's |
Responses
| Status | Description | Body |
|---|---|---|
200 | Page of approvals. | ApprovalPage |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
422 | Request failed validation. | Problem |
POST /v1/devices/{device_id}/approvals
Approve a pending request and mint the signed token (web session only)
session scope — a human, signed in to the web app. No API key of any scope can call this, which is the whole invariant of the consent layer: the hub's programmatic credential can browse, push and ask, and cannot put a policy on a robot. Approving a pending request mints a short-lived EdDSA (Ed25519) JWT bound to the device id (sub), the model repo, the ONNX path and its sha256, the contract fingerprint, the approver and exp = now + minutes; the driver verifies it offline against GET /v1/devices/signing-key. The approval's window is the run window: a policy picked up late runs for what is left of it, and the driver stops it at expires_at on its own clock. 409 when the request is not pending or the device is disconnected. On a trust-mode-approved device whose request rests on an unstamped policy (K1), unstamped_acknowledged: true is required (422 without it) and is signed into the token. Audited as device.approve. (M13)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
device_id | path | Id | Device id ( |
Request body
| Field | Type | Description |
|---|---|---|
request_id | Id | |
safety_acknowledged | boolean | P2 — the approver confirmed, on the page where they approved, that they are at the robot or someone is, with a hardware emergency stop within reach. Recorded in the |
unstamped_acknowledged | boolean | K1 — the approver read that the policy records no embodiment fingerprint and approved it onto this trust-mode- |
Responses
| Status | Description | Body |
|---|---|---|
201 | Approved; | Approval |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
422 | Request failed validation. | Problem |
GET /v1/devices/{device_id}/approvals/{approval_id}
Get an approval
The approval, token included, plus what a live page polls (K1): seconds_left in the window, stop_requested_at, and the latest heartbeat while it runs.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
device_id | path | Id | Device id ( |
approval_id | path | Id | Approval id ( |
Responses
POST /v1/devices/{device_id}/approvals/{approval_id}/stop
Stop an approval now
Any signed-in person who can see the device, or the device owner's write key, ends an approval before its window does — never an agent session's key, and never the device's own credential (agents neither start nor stop robots; there is no MCP tool for it). An issued approval ends at once (stopped; it can never start). A running one gets stop_requested_at, the pattern cancelRun uses: the driver reads it off its next heartbeat's answer (every two seconds by default), calls the adapter's stop() and posts a signed stopped with reason stop_requested. This is not a safety system: it reaches the robot only through the network and the driver's own loop, in seconds at best — a person at a hardware emergency stop is the safety system. Idempotent while running; 409 on an approval that already ended. Audited as device.stop. (K1)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
device_id | path | Id | Device id ( |
approval_id | path | Id | Approval id ( |
Request body
| Field | Type | Description |
|---|---|---|
reason | string | null |
Responses
| Status | Description | Body |
|---|---|---|
200 | Stopped ( | Approval |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
GET /v1/devices/{device_id}/events
List a device's events
Newest first; approval_id narrows to one approval's trail. (M13)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
device_id | path | Id | Device id ( |
approval_id | query | Id | |
kind | query | DeviceEventKind | |
limit | query | integer | Page size. |
cursor | query | string | Opaque cursor from the previous page's |
Responses
| Status | Description | Body |
|---|---|---|
200 | Page of events. | DeviceEventPage |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
422 | Request failed validation. | Problem |
POST /v1/devices/{device_id}/events
Record a telemetry summary from the driver
The driver's four words back to the hub about one approval: started, heartbeat (a few scalars), stopped, or refused with the reason (bad signature, expired, wrong device, digest mismatch, contract mismatch). Summaries, never control: the body is capped at 64 KiB and nothing here reaches the robot. Every event is signed with the device's own Ed25519 key (signature over the canonical payload) and verified against the key registered for the device, so a started row is evidence from the box that holds the private key, not just from whoever holds the hub API key. Transitions are checked: started needs an issued, unexpired approval; heartbeat and stopped need running; refused needs issued. The answer carries the approval's stop_requested_at (K1): a driver reads a person's Stop off its next heartbeat. key:write — a row is a data mutation, and the driver's key still cannot approve anything. started/stopped/refused are audited (device.started / device.stopped / device.refused); heartbeats are not. (M13)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
device_id | path | Id | Device id ( |
Request body
| Field | Type | Description |
|---|---|---|
approval_id | Id | |
kind | DeviceEventKind | |
ts | string (date-time) | The driver's clock when the event happened; part of the signed payload. |
summary | object | A few scalars ( |
message | string | null | |
signature | string | base64url Ed25519 signature, by the device's registered key, over the canonical JSON of |
Responses
| Status | Description | Body |
|---|---|---|
201 | Event recorded. | DeviceEvent |
401 | Missing or invalid credentials. | Problem |
403 | Authenticated but not allowed (visibility, membership or scope). | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
409 | State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...). | Problem |
413 | The summary exceeds 64 KiB. | Problem |
422 | Request failed validation. | Problem |
GET /v1/approval-requests/{request_id}
Get an approval request by its id alone
UI2 — getApprovalRequest without the device in the path: the consent page has a URL of its own (/inbox/requests/{id}) and a link to it carries only the request id. Same body (device, model, endpoint and checks[] computed now) and the same rule — a request on a device the caller cannot see is 404, never 403.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
request_id | path | Id | Approval request id ( |
Responses
| Status | Description | Body |
|---|---|---|
200 | The request, with everything a person needs before deciding. | ApprovalRequest |
401 | Missing or invalid credentials. | Problem |
404 | Resource not found (or hidden from the caller). | Problem |
Schemas (45)
The schemas these operations reach before any other tag’s do. A type that links elsewhere is rendered on that tag’s page.
DeviceSigningKey
| Field | Type | Description |
|---|---|---|
alg | string | |
kid | string | Key id carried in every approval token's header. |
public_key_pem | Ed25519PublicKeyPem | |
issuer | string | The |
audience | string | The |
ephemeral | boolean | True when |
DevicePage
| Field | Type | Description |
|---|---|---|
items | array of Device | |
next_cursor | string | null |
DeviceRegister
| Field | Type | Description |
|---|---|---|
name | Slug | |
robot_repo | string |
|
owner | Handle | null | Owner handle (user or org). Omit to own it yourself. |
adapter | DeviceAdapter | |
accepted_contracts | array of ContractFingerprint | At least one in trust mode |
public_key_pem | Ed25519PublicKeyPem | |
endpoint_capable | boolean | Declare that the adapter implements hold / stop and may consume an endpoint (M15). The driver sets this from the adapter's own code — the |
adapter_info | DeviceAdapterInfo | null | Where the adapter's code came from and which protocols it implements (M17d). Optional; older drivers omit it. |
trust_mode | DeviceTrustMode | |
interface_fingerprint | string | null | The robot card's interface fingerprint the driver pinned before registering (K1). When given it must equal the card's current one ( |
description | string | null |
Device
| Field | Type | Description |
|---|---|---|
id | Id | |
name | Slug | |
owner | RepoOwner | |
robot_repo | RepoRef | null | The robot repo this device embodies; null once that repo is deleted. |
adapter | DeviceAdapter | |
accepted_contracts | array of ContractFingerprint | The IO-contract fingerprints this device registered; in trust mode |
trust_mode | DeviceTrustMode | |
interface_fingerprint | string | null | The robot card's embodiment interface fingerprint this device pinned (K1) — what an |
online | boolean |
|
key_fingerprint | string | sha256 (hex) of the device's raw 32-byte Ed25519 public key — what |
running_approval | Approval | null | The approval this device reported |
credential | DeviceCredential | null | The device's own credential when it was paired (K1); null for a device registered with a person's API key. |
disconnected_at | string (date-time) | null (date-time) | When the owner disconnected it (K1); nothing is accepted for it after. |
public_key_pem | Ed25519PublicKeyPem | |
endpoint_capable | boolean | Whether this device may consume a hub inference endpoint (M15): true only when its adapter implements the mandatory local fail-safe (hold / stop on a missed chunk deadline or a lost link). A request naming an |
adapter_info | DeviceAdapterInfo | null | What the driver declared about the adapter at registration (M17d); null for a device registered before it. |
description | string | null | |
registered_by | UserPublic | null | |
last_seen_at | string (date-time) | null (date-time) | When the hub last heard from the driver: its last signed event, or — for a paired device (K1) — its own credential polling, recorded at most every ten seconds, so an idle connected robot reads online. |
created_at | string (date-time) | |
updated_at | string (date-time) |
DevicePairingCreate
DevicePairing
| Field | Type | Description |
|---|---|---|
id | Id | |
status | DevicePairingStatus | |
initiated_by | string |
|
code | string | null | The code, |
owner | RepoOwner | null | Null for a robot-issued pairing until a person confirms it. |
robot_repo | RepoRef | null | |
name | string | null | |
declared | DevicePairingDeclared | null | What the robot declared; null until it claims. |
interface_fingerprint | string | null | The robot card's interface fingerprint the device will pin (the robot repo's, once known). |
expires_at | string (date-time) | |
claimed_at | string (date-time) | null (date-time) | |
confirmed_at | string (date-time) | null (date-time) | |
collected_at | string (date-time) | null (date-time) | |
device_id | Id | null | The device a person confirmed. |
created_by | UserPublic | null | |
created_at | string (date-time) |
DevicePairingClaim
What registerDevice takes, minus robot_repo, owner and trust_mode (a person decides those), plus the code when a person issued one.
| Field | Type | Description |
|---|---|---|
code | string | null | The pairing code, any case, dash optional. Omit to have the hub issue one for the robot to show. |
name | Slug | null | A suggested name; the pairing's or the confirming person's wins. |
adapter | DeviceAdapter | |
adapter_info | DeviceAdapterInfo | null | |
accepted_contracts | array of ContractFingerprint | |
public_key_pem | Ed25519PublicKeyPem | |
endpoint_capable | boolean | |
description | string | null |
DevicePairingConfirm
| Field | Type | Description |
|---|---|---|
trust_mode | DeviceTrustMode | |
name | Slug | null | The device's name; else the pairing's, else the robot's suggestion. |
robot_repo | string | null | Required for a robot-issued pairing (a robot repo visible to you); must match for a person-issued one. |
owner | Handle | null | For a robot-issued pairing; omit to own it yourself. |
DevicePairingCollect
| Field | Type | Description |
|---|---|---|
ts | string (date-time) | The robot's clock; within five minutes of the hub's. |
signature | string | base64url Ed25519 signature, by the key the robot declared, over the canonical JSON of |
DevicePairingClaimed
| Field | Type | Description |
|---|---|---|
device | Device | |
api_key | string | The device credential ( |
signing_key | DeviceSigningKey | |
robot_interface | object | null | The robot card's embodiment interface, to pin; its fingerprint is |
DeviceTrustModeChange
| Field | Type | Description |
|---|---|---|
trust_mode | DeviceTrustMode | |
ts | string (date-time) | |
signature | string | base64url Ed25519 signature, by the device's registered key, over the canonical JSON of |
ApprovalRequestStatus
ApprovalRequestPage
| Field | Type | Description |
|---|---|---|
items | array of ApprovalRequest | |
next_cursor | string | null |
ApprovalRequestCreate
| Field | Type | Description |
|---|---|---|
model_repo | string |
|
onnx_path | FilePath | null | Which file in the repo. Omit when the repo holds exactly one |
onnx_sha256 | Sha256 | null | Optional assertion. When given and the repo's current file has a different digest the request is |
minutes | integer | How long the policy may run once approved. The window starts at approval, not at pickup. |
reason | string | null | Why — shown to the human on the pending card. |
endpoint_id | Id | null | M15: ask for the device to consume a hub inference endpoint instead of running the policy itself. The endpoint must be visible to the caller and serve |
dry_run | boolean | K1 — check, do not file: |
ApprovalRequest
| Field | Type | Description |
|---|---|---|
id | Id | |
device_id | Id | |
status | ApprovalRequestStatus | |
model_repo | RepoRef | |
onnx_path | FilePath | |
onnx_sha256 | Sha256 | |
contract_fingerprint | ContractFingerprint | null | From the repo's |
minutes | integer | |
reason | string | null | |
requested_by | UserPublic | null | |
requested_via | string | Which kind of credential asked — an agent's key or a person's session. |
approval_id | Id | null | |
decided_by | UserPublic | null | |
decided_at | string (date-time) | null (date-time) | |
decision_reason | string | null | |
endpoint_id | Id | null | The endpoint this request asks the device to consume (M15); null for a run-it-yourself deployment. |
requester | ActivityActor | null | UI-API: who asked, and with which credential — a person's session or an API key (its name, prefix and scope) — and |
plan | ApprovalPlan | null | UI-API — the plan that filed this request, when a planner did. |
device | Device | null | UI-API — where it would run: the device the request is for. |
model | ApprovalModelFacts | null | UI-API — what would run: the artifact, its control class and where that class says it may run, and the embodiment the policy was built for. |
endpoint | Endpoint | null | UI-API — the endpoint an endpoint-bound request would consume, when it is visible to the caller (null otherwise; the |
checks | array of ApprovalCheck | null | UI-API — what the hub checked, computed from the hub's current state on every read. |
trial_id | string | null | C4 — the real-robot trial this request was filed for (its run sheet is locked when the request is approved); null otherwise. |
created_at | string (date-time) |
ApprovalDecision
| Field | Type | Description |
|---|---|---|
reason | string | null |
ApprovalStatus
issued — signed, not yet picked up; running — the driver reported started; stopped — the driver reported stopped, or a person stopped it before it started (K1); refused — the driver refused it and said why; expired — issued past expires_at (computed, never stored).
ApprovalPage
| Field | Type | Description |
|---|---|---|
items | array of Approval | |
next_cursor | string | null |
ApprovalCreate
| Field | Type | Description |
|---|---|---|
request_id | Id | |
safety_acknowledged | boolean | P2 — the approver confirmed, on the page where they approved, that they are at the robot or someone is, with a hardware emergency stop within reach. Recorded in the |
unstamped_acknowledged | boolean | K1 — the approver read that the policy records no embodiment fingerprint and approved it onto this trust-mode- |
Approval
| Field | Type | Description |
|---|---|---|
id | Id | |
device_id | Id | |
request_id | Id | |
status | ApprovalStatus | |
model_repo | RepoRef | |
onnx_path | FilePath | |
onnx_sha256 | Sha256 | |
contract_fingerprint | ContractFingerprint | null | |
minutes | integer | |
approved_by | UserPublic | null | |
issued_at | string (date-time) | |
expires_at | string (date-time) | When the policy must be stopped; |
token | string | The signed EdDSA JWT the driver verifies offline. |
kid | string | null | Which hub signing key signed it. |
started_at | string (date-time) | null (date-time) | |
stopped_at | string (date-time) | null (date-time) | |
endpoint_id | Id | null | Bound inside the token when the approved request named an endpoint (M15). |
trial_id | string | null | C4 — the trial this approval runs. When set, the token also binds |
safety_acknowledged | boolean | P2's e-stop confirmation, as the approver gave it; signed into the token (K1). |
unstamped_acknowledged | boolean | K1 — the approver acknowledged an unstamped policy; signed into the token. |
stop_requested_at | string (date-time) | null (date-time) | K1 — when a person (or the owner's key) asked a running approval to stop; the driver reads it off its next heartbeat and posts |
seconds_left | number | null | K1 — seconds left in the window for an |
latest_heartbeat | DeviceEvent | null | K1 — the newest heartbeat the driver posted while it runs; null otherwise. |
created_at | string (date-time) |
DeviceEventKind
started / heartbeat / stopped / refused (M13), plus from M15 degraded — the device's local fail-safe fired (two chunk deadlines missed or the endpoint link lost: it holds posture, applies nothing stale) — and recovered — the link returned and chunks flow again. Both are allowed from running and both are audited. From K1, verified — the token verified offline on the box — and pulled — the files pulled and the digest the token binds matched; both from issued, before started, neither audited nor changing the state.
DeviceEventPage
| Field | Type | Description |
|---|---|---|
items | array of DeviceEvent | |
next_cursor | string | null |
DeviceEventCreate
| Field | Type | Description |
|---|---|---|
approval_id | Id | |
kind | DeviceEventKind | |
ts | string (date-time) | The driver's clock when the event happened; part of the signed payload. |
summary | object | A few scalars ( |
message | string | null | |
signature | string | base64url Ed25519 signature, by the device's registered key, over the canonical JSON of |
DeviceEvent
| Field | Type | Description |
|---|---|---|
id | Id | |
device_id | Id | |
approval_id | Id | null | |
kind | DeviceEventKind | |
ts | string (date-time) | |
summary | object | |
message | string | null | |
stop_requested_at | string (date-time) | null (date-time) | K1 — on the |
created_at | string (date-time) |
Ed25519PublicKeyPem
An Ed25519 public key, PEM (SubjectPublicKeyInfo).
DeviceAdapter
The identifier of the adapter the driver hands an approved policy to — since M17d an open identifier, not a closed list: adapters are plugins on the robot box, discovered through the Python entry-point group lucen_device.adapters (or named as module:Class for an unpackaged script), and this is the entry-point name — or, for a module:Class adapter, the class's own name. The operator names it at lucen-device register; the driver never loads an adapter nobody asked for. Built in: virtual validates the contract and "runs" it for the approved window emitting heartbeats — it simulates no physics and runs no inference, it exists so the whole consent path runs in CI with no hardware; command shells out to the robot's own start/stop/status commands, which come from the driver's local config and never from the hub; lucen-legged is the deprecated alias of command and keeps working. The hub cannot see the code behind an identifier: what ran is recorded in adapter_info and in the signed started event.
ContractFingerprint
sha256 of the canonical JSON (sort_keys, no whitespace, UTF-8) of a stamped ONNX manifest's contract block — joint order, obs spec, action scales, control rate and the rest of the IO contract. Two policies with the same fingerprint drive the same plant the same way.
DeviceAdapterInfo
What the driver says about the adapter it loaded (M17d): where the code came from and which protocols it implements. A declaration by the robot box, recorded so the trail says what ran — the hub cannot verify code it cannot see, and does not pretend to.
| Field | Type | Description |
|---|---|---|
source | string |
|
target | string | null | The |
distribution | string | null | The installed distribution that provides the adapter; null for an unpackaged script. |
version | string | null | That distribution's version — or |
driver_version | string | null | The |
protocols | array of DeviceAdapterProtocol | |
fail_safe_probe | string | null | Outcome of the driver's registration probe of the local fail-safe ( |
DeviceTrustMode
Chosen by the person who confirms a pairing — or by the box itself at registerDevice — and afterwards changed only by the device (setDeviceTrustMode) (K1). registered — the box runs only IO contracts it registered (accepted_contracts), the M13 behaviour and the default. approved — the box also runs a contract it never registered when a signed approval names that contract and the policy's embodiment_fingerprint equals the robot interface fingerprint the box pinned; an unstamped policy additionally needs the approver's recorded acknowledgment in the token and a contract that fits the pinned interface — the same joint order and control rate, one action per joint — which the box checks itself.
DeviceCredential
The device's own API key (K1), minted when the robot collected a confirmed pairing and bound to the device: it can act only as the device. Only its prefix is ever shown again.
| Field | Type | Description |
|---|---|---|
prefix | string | |
created_at | string (date-time) | |
last_used_at | string (date-time) | null (date-time) | Coarse (about a minute) — answers "is the driver still polling?". |
revoked_at | string (date-time) | null (date-time) |
DevicePairingStatus
pending — a code a person issued, no robot yet; claimed — a robot declared itself and waits for a person; confirmed — a person confirmed, the device exists (collected_at says whether the robot picked up its credential); rejected — "Not my robot"; expired — ten minutes passed unconfirmed (computed).
DevicePairingDeclared
What the robot declared when it claimed (K1) — shown to the person who confirms.
| Field | Type | Description |
|---|---|---|
adapter | DeviceAdapter | |
adapter_info | DeviceAdapterInfo | null | |
accepted_contracts | array of ContractFingerprint | |
endpoint_capable | boolean | |
key_fingerprint | string | sha256 (hex) of the raw Ed25519 public key; the robot prints the same. |
name | string | null | The name the robot suggested. |
description | string | null |
ActivityActor
| Field | Type | Description |
|---|---|---|
kind | ActivityActorKind | |
user | UserPublic | null | The person the credential belongs to; null for |
key | ActivityActorKey | null | Set when |
plan_id | Id | null | Set when a plan's planner took the action, acting as the credential above (a plan acts as the credential that created it, narrowed to |
agent | ActivityAgent | null |
|
agent_session_id | Id | null | A1 — set when the row was written with a key the hub minted for this agent session, acting as the credential above. |
ApprovalPlan
The plan a planner-filed request came from (UI-API).
| Field | Type | Description |
|---|---|---|
id | Id | |
task | string | |
status | PlanStatus | |
run_id | Id | |
budget_usd | number | |
cost_usd | number | |
expires_at | string (date-time) | When the plan hands back whatever it is doing. |
ApprovalModelFacts
What would run if the request were approved (UI-API) — read from the endpoint for an endpoint-bound request, else from the model repo's ModelMeta.
| Field | Type | Description |
|---|---|---|
artifact | string |
|
control_class | ControlClass | null | |
control_class_source | string |
|
runs_on | string |
|
policy_type | string | null | |
robot | string | null | The robot the policy was built for ( |
embodiment_fingerprint | string | null |
Endpoint
| Field | Type | Description |
|---|---|---|
id | Id | |
name | string | null | |
status | EndpointStatus | |
executor | RunExecutor | |
model_repo | RepoRef | |
onnx_path | FilePath | The served artifact's path: the ONNX file, or — when |
model_sha256 | Sha256 | The digest every claim, session token and device approval binds to: the ONNX's sha256, or the directory's |
artifact | EndpointArtifact | |
policy_format | string | null | M17e. The manifest's |
base_model | string | null | M17e. What a PEFT adapter sits on (e.g. |
contract_fingerprint | ContractFingerprint | null | From the repo's |
control_class | ControlClass | |
params | integer | null | |
tier | string | The tier the model's size selected (the one it is priced on). |
rate_usd_per_hour | number | |
priced | boolean | |
owner | RepoOwner | |
created_by | UserPublic | null | |
url | string | null | The websocket URL the claiming server stated; null until claimed. |
claimed_by | string | null | The server name given to |
claimed_at | string (date-time) | null (date-time) | |
heartbeat_at | string (date-time) | null (date-time) | The server's last claim or report. |
stop_requested_at | string (date-time) | null (date-time) | Set by |
max_hours | number | null | |
budget_usd | number | null | |
cost_usd | number | null | Summed over the endpoint's closed sessions (4 decimal places). |
sessions_open | integer | |
sessions_total | integer | |
chunks_served | integer | Summed over every session, as the server reported. |
gpu_seconds | number | Summed over every session, as attributed by the server. |
service_ms_p95 | number | null | The worst p95 service time (receive → send) over the open sessions; with none open, the latest session that measured one. |
rtt_ms_p95 | number | null | The worst p95 round trip the devices reported over the open sessions; with none open, the latest session that reported one. |
modal_call_id | string | null | The Modal function call serving a |
dispatched_at | string (date-time) | null (date-time) | When the hub spawned the |
error | string | null | |
created_at | string (date-time) | |
started_at | string (date-time) | null (date-time) | |
stopped_at | string (date-time) | null (date-time) | |
updated_at | string (date-time) |
ApprovalCheck
One thing the hub verified before a person decides (UI-API). Codes: device.contract (the device registered this contract fingerprint), model.control_class (reflex runs only on the robot; only skill / planner may be served), device.fail_safe (the probe the device DECLARED — the hub cannot run it), sim_gate.scorecard (the latest scored rollout of this model on this device's robot that the caller can see), embodiment.fingerprint (the policy's recorded fingerprint against the robot's current interface), artifact.digest (the pinned bytes are still what the repo holds), endpoint.serving (the endpoint is claimed and not stopping). New codes may be added; render unknown ones by status and message.
| Field | Type | Description |
|---|---|---|
code | string | |
status | ApprovalCheckStatus | |
message | string | One sentence, safe to show verbatim. |
evidence | object | null | The facts behind the verdict (fingerprints, ids, counts) for a UI or an agent to show. |
DeviceAdapterProtocol
A protocol the adapter's class implements, as the driver found it by inspecting the code on the box (M17d). policy — run an approved file locally (start / stop / status / is_running). chunks — consume a hub endpoint (begin / observe / apply / hold / resume / stop), with hold() and stop() exercised once by the driver's registration probe. staged_start — the optional stage(name, interface) hook (safe state, then default pose, then the policy). safe_stop — the optional set_safe_stop(mode) hook that lets the robot card's interface.safety.safe_stop choose what hold / stop mean.
ActivityActorKind
Which credential wrote the row. person — a signed-in web session; key — an API key (the CLI, an SDK, an MCP server, a runner, a device driver: the hub cannot tell which, and does not guess); system — the hub on its own (the lease sweep, the verify worker, a dispatch, a plan ended by its window or budget).
ActivityActorKey
The API key that wrote the row — enough to recognise it, never enough to use it.
| Field | Type | Description |
|---|---|---|
id | Id | |
name | string | |
prefix | string | The key's display prefix ( |
scope | ApiKeyScope | |
revoked | boolean | True once the key has been revoked; the row still says it was used. |
ActivityAgent
An agent the hub itself knows took an action: planner — a plan's frontier model decided it (M16); agent — the hub's cloud agent (A1), because the row was written with a key the hub minted for an agent session (agent_session_id). A request that arrives with any other API key is the key's, whoever — or whatever — held it.
PlanStatus
active is the only non-terminal state (what the plan is waiting on is Plan.waiting_on). done — the planner handed back with the task complete; handed_back — control returned to the human for another reason (an out-of-vocabulary task, a denied or expired approval, the window, the decision cap, the quota); canceled — cancelPlan; budget_exhausted — model spend reached budget_usd or a step's cap; failed — a decision failed validation twice, or no model.
EndpointStatus
queued — recorded, no server has claimed it; running — a server claimed it and reports; stopped — ended (by stopEndpoint, or the server's own terminal report); failed — the server reported a failure. Stop on a running endpoint is a flag (stop_requested_at), not a status.
EndpointArtifact
M17e. What an endpoint serves. onnx — one ONNX file; onnx_path is the file and model_sha256 its sha256. checkpoint_dir — a policy checkpoint directory plus the lucen_manifest.json beside it; onnx_path is the directory and model_sha256 its lucen-checkpoint-v1 digest: sha256("lucen-checkpoint-v1\n" + lines) where lines is one "<sha256 hex> <path>\n" (the sha256sum format) per member — the manifest and every file under the directory, paths relative to the directory holding both — sorted by the path's UTF-8 bytes. The hub computes it from the repo's content-addressed file list, lucen serve from the pulled bytes; claims, session tokens and device approvals bind to it exactly as they bind to an ONNX's sha256.
ApprovalCheckStatus
pass — the evidence agrees; fail — it contradicts the request; warn — partial or weaker than a pass; absent — the evidence does not exist (said so, never counted as a pass); not_applicable — the check does not apply to this kind of request.