Skip to content
Docs menu

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.

23 operations · 45 schemas

GET /v1/devices/signing-key

The hub's approval verification key

getDeviceSigningKey · scope none

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

getDeviceSigningKey responses
StatusDescriptionBody
200

The verification key.

DeviceSigningKey
401

Missing or invalid credentials.

Problemapplication/problem+json

GET /v1/devices

List devices

listDevices · scope key:read

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

listDevices parameters
NameInTypeDescription
ownerqueryHandle

Only devices owned by this user or org handle.

limitqueryinteger

default 20 · ≥ 1 · ≤ 100

Page size.

cursorquerystring

length ≤ 512

Opaque cursor from the previous page's next_cursor.

Responses

listDevices responses
StatusDescriptionBody
200

Page of devices.

DevicePage
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
422

Request failed validation.

Problemapplication/problem+json

POST /v1/devices

Register a robot-side runner

registerDevice · scope key:write

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

application/json · required · DeviceRegister

registerDevice request body
FieldTypeDescription
namerequiredSlug
robot_reporequiredstring

length ≤ 129 · pattern ^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$

owner/slug of a robot repo visible to the caller.

ownerHandle | null

Owner handle (user or org). Omit to own it yourself.

adapterrequiredDeviceAdapter
accepted_contractsrequiredarray of ContractFingerprint

items 0–32

At least one in trust mode registered (422 otherwise: a device that accepts no contract can never be handed a policy); may be empty in trust mode approved.

public_key_pemrequiredEd25519PublicKeyPem
endpoint_capableboolean

default false

Declare that the adapter implements hold / stop and may consume an endpoint (M15). The driver sets this from the adapter's own code — the chunks protocol plus a passed fail-safe probe (M17d). The hub answers 422 when the declaration contradicts what it can know: the adapter is a built-in without a chunk interface (command, lucen-legged), or it is not a built-in and adapter_info does not declare chunks with a passed probe.

adapter_infoDeviceAdapterInfo | null

Where the adapter's code came from and which protocols it implements (M17d). Optional; older drivers omit it.

trust_modeDeviceTrustMode
interface_fingerprintstring | null

pattern ^[a-f0-9]{64}$

The robot card's interface fingerprint the driver pinned before registering (K1). When given it must equal the card's current one (409 otherwise — the card changed since the driver read it).

descriptionstring | null

length ≤ 1024

Responses

registerDevice responses
StatusDescriptionBody
201

Device registered.

Device
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json
409

State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...).

Problemapplication/problem+json
422

Request failed validation.

Problemapplication/problem+json

POST /v1/device-pairings

Start pairing a robot from the platform

createDevicePairing · scope key:write

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

application/json · required · DevicePairingCreate

createDevicePairing request body
FieldTypeDescription
robot_reporequiredstring

length ≤ 129 · pattern ^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$

owner/slug of a robot repo visible to the caller.

ownerHandle | null

Owner handle (user or org) of the device-to-be. Omit to own it yourself.

nameSlug | null

The device's name; else it is named at confirmation.

Responses

createDevicePairing responses
StatusDescriptionBody
201

Pairing started; code is what to type on the robot.

DevicePairing
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json
409

State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...).

Problemapplication/problem+json
422

Request failed validation.

Problemapplication/problem+json

POST /v1/device-pairings/claim

Declare this robot to a pairing — with a code, or to get one

claimDevicePairing · scope none

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

application/json · required · DevicePairingClaim

claimDevicePairing request body
FieldTypeDescription
codestring | null

length ≤ 16

The pairing code, any case, dash optional. Omit to have the hub issue one for the robot to show.

nameSlug | null

A suggested name; the pairing's or the confirming person's wins.

adapterrequiredDeviceAdapter
adapter_infoDeviceAdapterInfo | null
accepted_contractsarray of ContractFingerprint

items 0–32

public_key_pemrequiredEd25519PublicKeyPem
endpoint_capableboolean

default false

descriptionstring | null

length ≤ 1024

Responses

claimDevicePairing responses
StatusDescriptionBody
201

Claimed; waiting for a person. code is set when the robot asked for one.

DevicePairing
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json
422

Request failed validation.

Problemapplication/problem+json
429

Too many claim attempts from this address; retry after Retry-After.

Problemapplication/problem+json

GET /v1/device-pairings/{pairing}

Get a pairing — has the robot joined, what did it declare?

getDevicePairing · scope key:read

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

getDevicePairing parameters
NameInTypeDescription
pairingrequiredpathstring

length 8–64

A pairing id (dpair_...) or, while it is not yet confirmed, its code (XXXX-XXXX).

Responses

getDevicePairing responses
StatusDescriptionBody
200

The pairing.

DevicePairing
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json

DELETE /v1/device-pairings/{pairing}

Drop a pairing — "Not my robot"

rejectDevicePairing · scope key:write

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

rejectDevicePairing parameters
NameInTypeDescription
pairingrequiredpathstring

length 8–64

A pairing id (dpair_...) or, while it is not yet confirmed, its code (XXXX-XXXX).

Responses

rejectDevicePairing responses
StatusDescriptionBody
200

Rejected.

DevicePairing
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json
409

State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...).

Problemapplication/problem+json

POST /v1/device-pairings/{pairing}/confirm

Confirm the robot that claimed a pairing (web session only)

confirmDevicePairing · scope session

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

confirmDevicePairing parameters
NameInTypeDescription
pairingrequiredpathstring

length 8–64

A pairing id (dpair_...) or, while it is not yet confirmed, its code (XXXX-XXXX).

Request body

application/json · required · DevicePairingConfirm

confirmDevicePairing request body
FieldTypeDescription
trust_modeDeviceTrustMode
nameSlug | null

The device's name; else the pairing's, else the robot's suggestion.

robot_repostring | null

length ≤ 129

Required for a robot-issued pairing (a robot repo visible to you); must match for a person-issued one.

ownerHandle | null

For a robot-issued pairing; omit to own it yourself.

Responses

confirmDevicePairing responses
StatusDescriptionBody
200

Confirmed; device_id is the new device.

DevicePairing
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json
409

State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...).

Problemapplication/problem+json
422

Request failed validation.

Problemapplication/problem+json

POST /v1/device-pairings/{pairing_id}/collect

The robot picks up what a person confirmed

collectDevicePairing · scope none

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

collectDevicePairing parameters
NameInTypeDescription
pairing_idrequiredpathId

Pairing id (dpair_...), from the claim's answer.

Request body

application/json · required · DevicePairingCollect

collectDevicePairing request body
FieldTypeDescription
tsrequiredstring (date-time)

The robot's clock; within five minutes of the hub's.

signaturerequiredstring

length ≤ 128

base64url Ed25519 signature, by the key the robot declared, over the canonical JSON of {pairing_id, ts}.

Responses

collectDevicePairing responses
StatusDescriptionBody
200

Connected; api_key is shown this once.

DevicePairingClaimed
202

Waiting for a person to confirm.

DevicePairing
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json
409

State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...).

Problemapplication/problem+json
422

Request failed validation.

Problemapplication/problem+json

GET /v1/devices/{device_id}

Get a device

getDevice · scope key:read

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

getDevice parameters
NameInTypeDescription
device_idrequiredpathId

Device id (dev_...). listApprovals also takes -, every device you can see.

Responses

getDevice responses
StatusDescriptionBody
200

The device.

Device
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json

POST /v1/devices/{device_id}/disconnect

Disconnect a device — revoke its credential

disconnectDevice · scope key:write

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

disconnectDevice parameters
NameInTypeDescription
device_idrequiredpathId

Device id (dev_...). listApprovals also takes -, every device you can see.

Responses

disconnectDevice responses
StatusDescriptionBody
200

Disconnected.

Device
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json

POST /v1/devices/{device_id}/trust-mode

The robot changes its own trust mode

setDeviceTrustMode · scope key:write

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

setDeviceTrustMode parameters
NameInTypeDescription
device_idrequiredpathId

Device id (dev_...). listApprovals also takes -, every device you can see.

Request body

application/json · required · DeviceTrustModeChange

setDeviceTrustMode request body
FieldTypeDescription
trust_moderequiredDeviceTrustMode
tsrequiredstring (date-time)
signaturerequiredstring

length ≤ 128

base64url Ed25519 signature, by the device's registered key, over the canonical JSON of {device_id, trust_mode, ts}.

Responses

setDeviceTrustMode responses
StatusDescriptionBody
200

Changed (or already so).

Device
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json
409

State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...).

Problemapplication/problem+json
422

Request failed validation.

Problemapplication/problem+json

GET /v1/devices/{device_id}/approval-requests

List a device's approval requests

listApprovalRequests · scope key:read

Newest first; status narrows to one state. (M13)

Parameters

listApprovalRequests parameters
NameInTypeDescription
device_idrequiredpathId

Device id (dev_...). listApprovals also takes -, every device you can see.

statusqueryApprovalRequestStatus
limitqueryinteger

default 20 · ≥ 1 · ≤ 100

Page size.

cursorquerystring

length ≤ 512

Opaque cursor from the previous page's next_cursor.

Responses

listApprovalRequests responses
StatusDescriptionBody
200

Page of requests.

ApprovalRequestPage
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json
422

Request failed validation.

Problemapplication/problem+json

POST /v1/devices/{device_id}/approval-requests

Ask for a policy to be put on a device

createApprovalRequest · scope key:write

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

createApprovalRequest parameters
NameInTypeDescription
device_idrequiredpathId

Device id (dev_...). listApprovals also takes -, every device you can see.

Request body

application/json · required · ApprovalRequestCreate

createApprovalRequest request body
FieldTypeDescription
model_reporequiredstring

length ≤ 129 · pattern ^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$

owner/slug of a model repo visible to the caller.

onnx_pathFilePath | null

Which file in the repo. Omit when the repo holds exactly one *.onnx; required (422) when it holds several.

onnx_sha256Sha256 | null

Optional assertion. When given and the repo's current file has a different digest the request is 409 — the caller was looking at other bytes.

minutesrequiredinteger

≥ 1 · ≤ 1440

How long the policy may run once approved. The window starts at approval, not at pickup.

reasonstring | null

length ≤ 1024

Why — shown to the human on the pending card.

endpoint_idId | 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 model_repo (409 otherwise); the request is pinned to the endpoint's own onnx_path / sha256, the device must be endpoint_capable (422), and the minted token carries endpoint_id inside its signature. The driver then opens a session on the endpoint rather than pulling the model.

dry_runboolean

default false

K1 — check, do not file: 200 with the request as it would be and its checks[]; nothing is stored or audited.

Responses

createApprovalRequest responses
StatusDescriptionBody
200

dry_run: the request as it would be filed, with its checks; nothing stored.

ApprovalRequest
201

Request recorded, status pending.

ApprovalRequest
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json
409

State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...).

Problemapplication/problem+json
422

Request failed validation.

Problemapplication/problem+json

GET /v1/devices/{device_id}/approval-requests/{request_id}

Get an approval request

getApprovalRequest · scope key:read

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

getApprovalRequest parameters
NameInTypeDescription
device_idrequiredpathId

Device id (dev_...). listApprovals also takes -, every device you can see.

request_idrequiredpathId

Approval request id (dreq_...).

Responses

getApprovalRequest responses
StatusDescriptionBody
200

The request.

ApprovalRequest
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json

POST /v1/devices/{device_id}/approval-requests/{request_id}/deny

Deny an approval request (web session only)

denyApprovalRequest · scope session

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

denyApprovalRequest parameters
NameInTypeDescription
device_idrequiredpathId

Device id (dev_...). listApprovals also takes -, every device you can see.

request_idrequiredpathId

Approval request id (dreq_...).

Request body

application/json · ApprovalDecision

denyApprovalRequest request body
FieldTypeDescription
reasonstring | null

length ≤ 1024

Responses

denyApprovalRequest responses
StatusDescriptionBody
200

Denied.

ApprovalRequest
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json
409

State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...).

Problemapplication/problem+json
422

Request failed validation.

Problemapplication/problem+json

GET /v1/devices/{device_id}/approvals

List a device's approvals

listApprovals · scope key:read

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

listApprovals parameters
NameInTypeDescription
device_idrequiredpathId

Device id (dev_...). listApprovals also takes -, every device you can see.

statusqueryApprovalStatus
model_repoquerystring

length ≤ 129

owner/slug of a model repo; one you cannot read is an empty page.

limitqueryinteger

default 20 · ≥ 1 · ≤ 100

Page size.

cursorquerystring

length ≤ 512

Opaque cursor from the previous page's next_cursor.

Responses

listApprovals responses
StatusDescriptionBody
200

Page of approvals.

ApprovalPage
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json
422

Request failed validation.

Problemapplication/problem+json

POST /v1/devices/{device_id}/approvals

Approve a pending request and mint the signed token (web session only)

createApproval · scope session

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

createApproval parameters
NameInTypeDescription
device_idrequiredpathId

Device id (dev_...). listApprovals also takes -, every device you can see.

Request body

application/json · required · ApprovalCreate

createApproval request body
FieldTypeDescription
request_idrequiredId
safety_acknowledgedboolean

default false

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 device.approve audit row as safety_acknowledged and, since K1, signed into the token and shown on the approval. The web app does not let the hold-to-approve start until it is ticked; the API records the answer rather than refusing without it, so the record says which approvals were made without the confirmation.

unstamped_acknowledgedboolean

default false

K1 — the approver read that the policy records no embodiment fingerprint and approved it onto this trust-mode-approved device anyway. Required (422) exactly when the request rests on an unstamped policy whose contract the device never registered; signed into the token, where the driver requires it before it compares the policy's joint order with its pinned interface.

Responses

createApproval responses
StatusDescriptionBody
201

Approved; token is what the driver verifies.

Approval
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json
409

State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...).

Problemapplication/problem+json
422

Request failed validation.

Problemapplication/problem+json

GET /v1/devices/{device_id}/approvals/{approval_id}

Get an approval

getApproval · scope key:read

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

getApproval parameters
NameInTypeDescription
device_idrequiredpathId

Device id (dev_...). listApprovals also takes -, every device you can see.

approval_idrequiredpathId

Approval id (appr_...).

Responses

getApproval responses
StatusDescriptionBody
200

The approval, token included.

Approval
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json

POST /v1/devices/{device_id}/approvals/{approval_id}/stop

Stop an approval now

stopApproval · scope key:write

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

stopApproval parameters
NameInTypeDescription
device_idrequiredpathId

Device id (dev_...). listApprovals also takes -, every device you can see.

approval_idrequiredpathId

Approval id (appr_...).

Request body

application/json · ApprovalDecision

stopApproval request body
FieldTypeDescription
reasonstring | null

length ≤ 1024

Responses

stopApproval responses
StatusDescriptionBody
200

Stopped (issued) or stop requested (running).

Approval
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json
409

State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...).

Problemapplication/problem+json

GET /v1/devices/{device_id}/events

List a device's events

listDeviceEvents · scope key:read

Newest first; approval_id narrows to one approval's trail. (M13)

Parameters

listDeviceEvents parameters
NameInTypeDescription
device_idrequiredpathId

Device id (dev_...). listApprovals also takes -, every device you can see.

approval_idqueryId
kindqueryDeviceEventKind
limitqueryinteger

default 20 · ≥ 1 · ≤ 100

Page size.

cursorquerystring

length ≤ 512

Opaque cursor from the previous page's next_cursor.

Responses

listDeviceEvents responses
StatusDescriptionBody
200

Page of events.

DeviceEventPage
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json
422

Request failed validation.

Problemapplication/problem+json

POST /v1/devices/{device_id}/events

Record a telemetry summary from the driver

postDeviceEvent · scope key:write

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

postDeviceEvent parameters
NameInTypeDescription
device_idrequiredpathId

Device id (dev_...). listApprovals also takes -, every device you can see.

Request body

application/json · required · DeviceEventCreate

postDeviceEvent request body
FieldTypeDescription
approval_idrequiredId
kindrequiredDeviceEventKind
tsrequiredstring (date-time)

The driver's clock when the event happened; part of the signed payload.

summaryrequiredobject

A few scalars (tick, loop_hz, elapsed_s, …) or a reason. ≤ 64 KiB serialised.

messagestring | null

length ≤ 2048

signaturerequiredstring

length ≤ 128

base64url Ed25519 signature, by the device's registered key, over the canonical JSON of {approval_id, device_id, kind, summary, ts} (sorted keys, no whitespace).

Responses

postDeviceEvent responses
StatusDescriptionBody
201

Event recorded.

DeviceEvent
401

Missing or invalid credentials.

Problemapplication/problem+json
403

Authenticated but not allowed (visibility, membership or scope).

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json
409

State conflict (duplicate handle/slug, wrong repo kind, terminal job, ...).

Problemapplication/problem+json
413

The summary exceeds 64 KiB.

Problemapplication/problem+json
422

Request failed validation.

Problemapplication/problem+json

GET /v1/approval-requests/{request_id}

Get an approval request by its id alone

getApprovalRequestById · scope key:read

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

getApprovalRequestById parameters
NameInTypeDescription
request_idrequiredpathId

Approval request id (dreq_...).

Responses

getApprovalRequestById responses
StatusDescriptionBody
200

The request, with everything a person needs before deciding.

ApprovalRequest
401

Missing or invalid credentials.

Problemapplication/problem+json
404

Resource not found (or hidden from the caller).

Problemapplication/problem+json

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

object

DeviceSigningKey fields
FieldTypeDescription
algrequiredstring

one of EdDSA

kidrequiredstring

Key id carried in every approval token's header.

public_key_pemrequiredEd25519PublicKeyPem
issuerrequiredstring

The iss claim every approval carries.

audiencerequiredstring

The aud claim every approval carries.

ephemeralrequiredboolean

True when DEVICE_APPROVAL_SIGNING_KEY is unset and the key was generated at process start (dev / CI). Approvals it signed die with the process.

DevicePage

object

DevicePage fields
FieldTypeDescription
itemsrequiredarray of Device
next_cursorrequiredstring | null

DeviceRegister

object

DeviceRegister fields
FieldTypeDescription
namerequiredSlug
robot_reporequiredstring

length ≤ 129 · pattern ^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$

owner/slug of a robot repo visible to the caller.

ownerHandle | null

Owner handle (user or org). Omit to own it yourself.

adapterrequiredDeviceAdapter
accepted_contractsrequiredarray of ContractFingerprint

items 0–32

At least one in trust mode registered (422 otherwise: a device that accepts no contract can never be handed a policy); may be empty in trust mode approved.

public_key_pemrequiredEd25519PublicKeyPem
endpoint_capableboolean

default false

Declare that the adapter implements hold / stop and may consume an endpoint (M15). The driver sets this from the adapter's own code — the chunks protocol plus a passed fail-safe probe (M17d). The hub answers 422 when the declaration contradicts what it can know: the adapter is a built-in without a chunk interface (command, lucen-legged), or it is not a built-in and adapter_info does not declare chunks with a passed probe.

adapter_infoDeviceAdapterInfo | null

Where the adapter's code came from and which protocols it implements (M17d). Optional; older drivers omit it.

trust_modeDeviceTrustMode
interface_fingerprintstring | null

pattern ^[a-f0-9]{64}$

The robot card's interface fingerprint the driver pinned before registering (K1). When given it must equal the card's current one (409 otherwise — the card changed since the driver read it).

descriptionstring | null

length ≤ 1024

Device

object

Device fields
FieldTypeDescription
idrequiredId
namerequiredSlug
ownerrequiredRepoOwner
robot_repoRepoRef | null

The robot repo this device embodies; null once that repo is deleted.

adapterrequiredDeviceAdapter
accepted_contractsrequiredarray of ContractFingerprint

items 0–32

The IO-contract fingerprints this device registered; in trust mode registered anything else is refused on the box. May be empty for a trust-mode-approved device (K1).

trust_moderequiredDeviceTrustMode
interface_fingerprintstring | null

pattern ^[a-f0-9]{64}$

The robot card's embodiment interface fingerprint this device pinned (K1) — what an approved-mode device compares a policy's embodiment_fingerprint with. Null when the card declared none, or for a device registered before K1.

onlinerequiredboolean

last_seen_at within DEVICE_ONLINE_SECONDS (60 s) — the overview's rule (K1).

key_fingerprintrequiredstring

sha256 (hex) of the device's raw 32-byte Ed25519 public key — what lucen-device pair prints on the robot, to compare with the page (K1).

running_approvalApproval | null

The approval this device reported started and not yet stopped, inside its window, with its time left and latest heartbeat (K1).

credentialDeviceCredential | null

The device's own credential when it was paired (K1); null for a device registered with a person's API key.

disconnected_atstring (date-time) | null (date-time)

When the owner disconnected it (K1); nothing is accepted for it after.

public_key_pemrequiredEd25519PublicKeyPem
endpoint_capablerequiredboolean

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 endpoint_id on a device without it is 422. Since M17d this is the driver's declaration, made from the adapter's code and a probe of its hold() / stop(), not a list of adapter names the hub keeps.

adapter_infoDeviceAdapterInfo | null

What the driver declared about the adapter at registration (M17d); null for a device registered before it.

descriptionstring | null

length ≤ 1024

registered_byUserPublic | null
last_seen_atstring (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_atrequiredstring (date-time)
updated_atrequiredstring (date-time)

DevicePairingCreate

object

DevicePairingCreate fields
FieldTypeDescription
robot_reporequiredstring

length ≤ 129 · pattern ^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$

owner/slug of a robot repo visible to the caller.

ownerHandle | null

Owner handle (user or org) of the device-to-be. Omit to own it yourself.

nameSlug | null

The device's name; else it is named at confirmation.

DevicePairing

object

DevicePairing fields
FieldTypeDescription
idrequiredId
statusrequiredDevicePairingStatus
initiated_byrequiredstring

one of person · robot

person — a code issued on the hub; robot — a code the robot asked for and shows.

codestring | null

The code, XXXX-XXXX — only in the answer that created it (createDevicePairing for a person, claimDevicePairing without a code for a robot).

ownerRepoOwner | null

Null for a robot-issued pairing until a person confirms it.

robot_repoRepoRef | null
namestring | null
declaredDevicePairingDeclared | null

What the robot declared; null until it claims.

interface_fingerprintstring | null

The robot card's interface fingerprint the device will pin (the robot repo's, once known).

expires_atrequiredstring (date-time)
claimed_atstring (date-time) | null (date-time)
confirmed_atstring (date-time) | null (date-time)
collected_atstring (date-time) | null (date-time)
device_idId | null

The device a person confirmed.

created_byUserPublic | null
created_atrequiredstring (date-time)

DevicePairingClaim

object

What registerDevice takes, minus robot_repo, owner and trust_mode (a person decides those), plus the code when a person issued one.

DevicePairingClaim fields
FieldTypeDescription
codestring | null

length ≤ 16

The pairing code, any case, dash optional. Omit to have the hub issue one for the robot to show.

nameSlug | null

A suggested name; the pairing's or the confirming person's wins.

adapterrequiredDeviceAdapter
adapter_infoDeviceAdapterInfo | null
accepted_contractsarray of ContractFingerprint

items 0–32

public_key_pemrequiredEd25519PublicKeyPem
endpoint_capableboolean

default false

descriptionstring | null

length ≤ 1024

DevicePairingConfirm

object

DevicePairingConfirm fields
FieldTypeDescription
trust_modeDeviceTrustMode
nameSlug | null

The device's name; else the pairing's, else the robot's suggestion.

robot_repostring | null

length ≤ 129

Required for a robot-issued pairing (a robot repo visible to you); must match for a person-issued one.

ownerHandle | null

For a robot-issued pairing; omit to own it yourself.

DevicePairingCollect

object

DevicePairingCollect fields
FieldTypeDescription
tsrequiredstring (date-time)

The robot's clock; within five minutes of the hub's.

signaturerequiredstring

length ≤ 128

base64url Ed25519 signature, by the key the robot declared, over the canonical JSON of {pairing_id, ts}.

DevicePairingClaimed

object

DevicePairingClaimed fields
FieldTypeDescription
devicerequiredDevice
api_keyrequiredstring

The device credential (lucen_sk_...), shown this once. The driver keeps it beside its private key (mode 0600); it acts only as this device.

signing_keyrequiredDeviceSigningKey
robot_interfaceobject | null

The robot card's embodiment interface, to pin; its fingerprint is device.interface_fingerprint. Null when the card declares none.

DeviceTrustModeChange

object

DeviceTrustModeChange fields
FieldTypeDescription
trust_moderequiredDeviceTrustMode
tsrequiredstring (date-time)
signaturerequiredstring

length ≤ 128

base64url Ed25519 signature, by the device's registered key, over the canonical JSON of {device_id, trust_mode, ts}.

ApprovalRequestStatus

string

one of pending · approved · denied

ApprovalRequestPage

object

ApprovalRequestPage fields
FieldTypeDescription
itemsrequiredarray of ApprovalRequest
next_cursorrequiredstring | null

ApprovalRequestCreate

object

ApprovalRequestCreate fields
FieldTypeDescription
model_reporequiredstring

length ≤ 129 · pattern ^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$

owner/slug of a model repo visible to the caller.

onnx_pathFilePath | null

Which file in the repo. Omit when the repo holds exactly one *.onnx; required (422) when it holds several.

onnx_sha256Sha256 | null

Optional assertion. When given and the repo's current file has a different digest the request is 409 — the caller was looking at other bytes.

minutesrequiredinteger

≥ 1 · ≤ 1440

How long the policy may run once approved. The window starts at approval, not at pickup.

reasonstring | null

length ≤ 1024

Why — shown to the human on the pending card.

endpoint_idId | 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 model_repo (409 otherwise); the request is pinned to the endpoint's own onnx_path / sha256, the device must be endpoint_capable (422), and the minted token carries endpoint_id inside its signature. The driver then opens a session on the endpoint rather than pulling the model.

dry_runboolean

default false

K1 — check, do not file: 200 with the request as it would be and its checks[]; nothing is stored or audited.

ApprovalRequest

object

ApprovalRequest fields
FieldTypeDescription
idrequiredId
device_idrequiredId
statusrequiredApprovalRequestStatus
model_reporequiredRepoRef
onnx_pathrequiredFilePath
onnx_sha256requiredSha256
contract_fingerprintContractFingerprint | null

From the repo's io_contract.json; null when the repo has none.

minutesrequiredinteger
reasonstring | null
requested_byUserPublic | null
requested_viarequiredstring

one of api_key · session

Which kind of credential asked — an agent's key or a person's session.

approval_idId | null
decided_byUserPublic | null
decided_atstring (date-time) | null (date-time)
decision_reasonstring | null
endpoint_idId | null

The endpoint this request asks the device to consume (M15); null for a run-it-yourself deployment.

requesterActivityActor | null

UI-API: who asked, and with which credential — a person's session or an API key (its name, prefix and scope) — and plan_id / agent: planner when a plan's planner filed the request acting as that credential.

planApprovalPlan | null

UI-API — the plan that filed this request, when a planner did.

deviceDevice | null

UI-API — where it would run: the device the request is for. getApprovalRequest and getMyOverview only; null on list pages.

modelApprovalModelFacts | 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. getApprovalRequest and getMyOverview only.

endpointEndpoint | null

UI-API — the endpoint an endpoint-bound request would consume, when it is visible to the caller (null otherwise; the endpoint.serving check then says so). getApprovalRequest and getMyOverview only.

checksarray of ApprovalCheck | null

UI-API — what the hub checked, computed from the hub's current state on every read. getApprovalRequest and getMyOverview only; null on list pages.

trial_idread-onlystring | null

C4 — the real-robot trial this request was filed for (its run sheet is locked when the request is approved); null otherwise.

created_atrequiredstring (date-time)

ApprovalDecision

object

ApprovalDecision fields
FieldTypeDescription
reasonstring | null

length ≤ 1024

ApprovalStatus

string

one of issued · running · stopped · refused · expired

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

object

ApprovalPage fields
FieldTypeDescription
itemsrequiredarray of Approval
next_cursorrequiredstring | null

ApprovalCreate

object

ApprovalCreate fields
FieldTypeDescription
request_idrequiredId
safety_acknowledgedboolean

default false

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 device.approve audit row as safety_acknowledged and, since K1, signed into the token and shown on the approval. The web app does not let the hold-to-approve start until it is ticked; the API records the answer rather than refusing without it, so the record says which approvals were made without the confirmation.

unstamped_acknowledgedboolean

default false

K1 — the approver read that the policy records no embodiment fingerprint and approved it onto this trust-mode-approved device anyway. Required (422) exactly when the request rests on an unstamped policy whose contract the device never registered; signed into the token, where the driver requires it before it compares the policy's joint order with its pinned interface.

Approval

object

Approval fields
FieldTypeDescription
idrequiredId
device_idrequiredId
request_idrequiredId
statusrequiredApprovalStatus
model_reporequiredRepoRef
onnx_pathrequiredFilePath
onnx_sha256requiredSha256
contract_fingerprintContractFingerprint | null
minutesrequiredinteger
approved_byrequiredUserPublic | null
issued_atrequiredstring (date-time)
expires_atrequiredstring (date-time)

When the policy must be stopped; issued_at + minutes.

tokenrequiredstring

The signed EdDSA JWT the driver verifies offline.

kidstring | null

Which hub signing key signed it.

started_atstring (date-time) | null (date-time)
stopped_atstring (date-time) | null (date-time)
endpoint_idId | null

Bound inside the token when the approved request named an endpoint (M15).

trial_idread-onlystring | null

C4 — the trial this approval runs. When set, the token also binds trial_id and run_sheet_sha256 (the locked run sheet's digest), and the driver refuses a sheet that does not hash to it.

safety_acknowledgedboolean

P2's e-stop confirmation, as the approver gave it; signed into the token (K1).

unstamped_acknowledgedboolean

K1 — the approver acknowledged an unstamped policy; signed into the token.

stop_requested_atstring (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 stopped with reason stop_requested.

seconds_leftnumber | null

K1 — seconds left in the window for an issued or running approval; null otherwise.

latest_heartbeatDeviceEvent | null

K1 — the newest heartbeat the driver posted while it runs; null otherwise.

created_atrequiredstring (date-time)

DeviceEventKind

string

one of started · heartbeat · stopped · refused · degraded · recovered · verified · pulled

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

object

DeviceEventPage fields
FieldTypeDescription
itemsrequiredarray of DeviceEvent
next_cursorrequiredstring | null

DeviceEventCreate

object

DeviceEventCreate fields
FieldTypeDescription
approval_idrequiredId
kindrequiredDeviceEventKind
tsrequiredstring (date-time)

The driver's clock when the event happened; part of the signed payload.

summaryrequiredobject

A few scalars (tick, loop_hz, elapsed_s, …) or a reason. ≤ 64 KiB serialised.

messagestring | null

length ≤ 2048

signaturerequiredstring

length ≤ 128

base64url Ed25519 signature, by the device's registered key, over the canonical JSON of {approval_id, device_id, kind, summary, ts} (sorted keys, no whitespace).

DeviceEvent

object

DeviceEvent fields
FieldTypeDescription
idrequiredId
device_idrequiredId
approval_idrequiredId | null
kindrequiredDeviceEventKind
tsrequiredstring (date-time)
summaryrequiredobject
messagestring | null
stop_requested_atstring (date-time) | null (date-time)

K1 — on the postDeviceEvent answer only: the approval's stop_requested_at when a person asked it to stop. Null on list pages.

created_atrequiredstring (date-time)

Ed25519PublicKeyPem

string

length 60–400

An Ed25519 public key, PEM (SubjectPublicKeyInfo).

DeviceAdapter

string

length 1–64 · pattern ^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$

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

string

pattern ^[a-f0-9]{64}$

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

object

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.

DeviceAdapterInfo fields
FieldTypeDescription
sourcerequiredstring

one of builtin · entry_point · module · file

builtin (ships in lucen-device), entry_point (an installed distribution registered it), module (module:Class, named explicitly) or file (path/to/file.py:Class).

targetstring | null

length ≤ 256

The module:Class the identifier resolved to.

distributionstring | null

length ≤ 128

The installed distribution that provides the adapter; null for an unpackaged script.

versionstring | null

length ≤ 80

That distribution's version — or sha256:<hex> of the file for a file adapter.

driver_versionstring | null

length ≤ 32

The lucen-device version that inspected it.

protocolsrequiredarray of DeviceAdapterProtocol

items ≤ 8 · unique items

fail_safe_probestring | null

length ≤ 256

Outcome of the driver's registration probe of the local fail-safe (hold() then stop() on a fresh instance, before any hub contact): passed, failed: <why>, or null when the adapter has no chunk interface to probe.

DeviceTrustMode

string

one of registered · approved

default "registered"

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

object

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.

DeviceCredential fields
FieldTypeDescription
prefixrequiredstring
created_atrequiredstring (date-time)
last_used_atstring (date-time) | null (date-time)

Coarse (about a minute) — answers "is the driver still polling?".

revoked_atstring (date-time) | null (date-time)

DevicePairingStatus

string

one of pending · claimed · confirmed · rejected · expired

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

object

What the robot declared when it claimed (K1) — shown to the person who confirms.

DevicePairingDeclared fields
FieldTypeDescription
adapterrequiredDeviceAdapter
adapter_infoDeviceAdapterInfo | null
accepted_contractsrequiredarray of ContractFingerprint
endpoint_capablerequiredboolean
key_fingerprintrequiredstring

sha256 (hex) of the raw Ed25519 public key; the robot prints the same.

namestring | null

The name the robot suggested.

descriptionstring | null

ActivityActor

object

ActivityActor fields
FieldTypeDescription
kindrequiredActivityActorKind
userUserPublic | null

The person the credential belongs to; null for system.

keyActivityActorKey | null

Set when kind is key.

plan_idId | 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 write).

agentActivityAgent | null

planner exactly when plan_id is set, agent exactly when agent_session_id is set — the two cases in which the hub itself knows an agent decided. Everything else a key did is reported as the key, whoever held it.

agent_session_idId | null

A1 — set when the row was written with a key the hub minted for this agent session, acting as the credential above.

ApprovalPlan

object

The plan a planner-filed request came from (UI-API).

ApprovalPlan fields
FieldTypeDescription
idrequiredId
taskrequiredstring
statusrequiredPlanStatus
run_idrequiredId
budget_usdrequirednumber
cost_usdrequirednumber
expires_atrequiredstring (date-time)

When the plan hands back whatever it is doing.

ApprovalModelFacts

object

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.

ApprovalModelFacts fields
FieldTypeDescription
artifactrequiredstring

one of onnx · checkpoint_dir

onnx — one file; checkpoint_dir — policy/ + lucen_manifest.json (M17e).

control_classControlClass | null
control_class_sourcerequiredstring

one of endpoint · model_meta · undeclared

endpoint (fixed when the endpoint was created), model_meta, or undeclared.

runs_onrequiredstring

one of robot · hub_endpoint

robot — the device pulls the pinned bytes and runs them itself; hub_endpoint — the device consumes action chunks from the hub endpoint over the network, under its own local fail-safe.

policy_typestring | null
robotstring | null

The robot the policy was built for (ModelMeta.robot), when visible.

embodiment_fingerprintstring | null

pattern ^[0-9a-f]{64}$

Endpoint

object

Endpoint fields
FieldTypeDescription
idrequiredId
namestring | null
statusrequiredEndpointStatus
executorrequiredRunExecutor
model_reporequiredRepoRef
onnx_pathrequiredFilePath

The served artifact's path: the ONNX file, or — when artifact is checkpoint_dir (M17e) — the checkpoint directory (policy). The field keeps M15's name; it is frozen.

model_sha256requiredSha256

The digest every claim, session token and device approval binds to: the ONNX's sha256, or the directory's lucen-checkpoint-v1 digest (see EndpointArtifact).

artifactEndpointArtifact
policy_formatstring | null

length ≤ 32

M17e. The manifest's policy_format for a checkpoint directory (lerobot, lerobot-peft); null for an ONNX.

base_modelstring | null

length ≤ 256

M17e. What a PEFT adapter sits on (e.g. lerobot/smolvla_base) — the server fetches it from the Hugging Face Hub or its cache; null otherwise.

contract_fingerprintContractFingerprint | null

From the repo's io_contract.json / manifest — for a checkpoint directory, from lucen_manifest.json, which the digest covers; null when the repo carries none.

control_classrequiredControlClass
paramsinteger | null
tierrequiredstring

one of l4 · a10g · a100 · h100

The tier the model's size selected (the one it is priced on).

rate_usd_per_hourrequirednumber
pricedrequiredboolean
ownerrequiredRepoOwner
created_byUserPublic | null
urlstring | null

The websocket URL the claiming server stated; null until claimed.

claimed_bystring | null

The server name given to claimEndpoint.

claimed_atstring (date-time) | null (date-time)
heartbeat_atstring (date-time) | null (date-time)

The server's last claim or report.

stop_requested_atstring (date-time) | null (date-time)

Set by stopEndpoint on a running endpoint; the server stops on its next report.

max_hoursnumber | null
budget_usdnumber | null
cost_usdnumber | null

Summed over the endpoint's closed sessions (4 decimal places).

sessions_openrequiredinteger
sessions_totalrequiredinteger
chunks_servedrequiredinteger

Summed over every session, as the server reported.

gpu_secondsrequirednumber

Summed over every session, as attributed by the server.

service_ms_p95number | null

The worst p95 service time (receive → send) over the open sessions; with none open, the latest session that measured one.

rtt_ms_p95number | null

The worst p95 round trip the devices reported over the open sessions; with none open, the latest session that reported one.

modal_call_idstring | null

length ≤ 64

The Modal function call serving a modal endpoint (W13), set by the dispatch job after the endpoint was recorded. Null for a worker endpoint.

dispatched_atstring (date-time) | null (date-time)

When the hub spawned the modal call (W13); null for worker.

errorstring | null
created_atrequiredstring (date-time)
started_atstring (date-time) | null (date-time)
stopped_atstring (date-time) | null (date-time)
updated_atrequiredstring (date-time)

ApprovalCheck

object

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.

ApprovalCheck fields
FieldTypeDescription
coderequiredstring
statusrequiredApprovalCheckStatus
messagerequiredstring

One sentence, safe to show verbatim.

evidenceobject | null

The facts behind the verdict (fingerprints, ids, counts) for a UI or an agent to show.

DeviceAdapterProtocol

string

one of policy · chunks · staged_start · safe_stop

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

string

one of person · key · system

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

object

The API key that wrote the row — enough to recognise it, never enough to use it.

ActivityActorKey fields
FieldTypeDescription
idrequiredId
namerequiredstring
prefixrequiredstring

The key's display prefix (lucen_sk_ + 8 characters), as the key list shows it.

scoperequiredApiKeyScope
revokedrequiredboolean

True once the key has been revoked; the row still says it was used.

ActivityAgent

string

one of planner · agent

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

string

one of active · done · handed_back · canceled · budget_exhausted · failed

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

string

one of queued · running · stopped · failed

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

string

one of onnx · checkpoint_dir

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

string

one of pass · fail · warn · absent · not_applicable

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.