openvibers/openvibe.contracts Commits

0.223.0: Run sessions (Desks plan D6): run.session@1 and its create, control and list contracts, run.session.started and .ended, capabilities run.session.create/read/control; Bot desk leases (bot.desk-lease@1, bot.desk.session) (#231)

OP OpenVibers committed 16 days ago · 08cbd0b4d3 · 95 files, +4706 −98

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex f75e7395..5e9d1c08 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,25 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
the notes were in the tag and commit messages (`git tag -n1`).
++## 0.223.0 — 2026-10-28
++
++- **Run sessions** (the Desks plan's D6; `OpenVibe_Agent_Desks_Plan_2026-10-27.md`). A session is a browser desk that
++ OpenVibe.Run places for one person and OpenVibe.Bot leases to it:
++ - `run.session@1` (states placing, starting, running, ended; where it runs and with what trust; desk-seconds),
++ `run.session-create-request@1` (`POST /api/v1/sessions`: the person's own named machine, or placement over desk
++ hosts, `ttl_s`, `idle_s`, requirements, an idempotency key), `run.session-control-request@1` and
++ `run.session-control-result@1` (`POST /api/v1/sessions/:id/commands`: screen, input and browser on the session's
++ desk only), `run.session-list-result@1`, and the events `run.session.started` and `run.session.ended`.
++ - Capabilities `run.session.create`, `run.session.read` and `run.session.control` (public, the caller's own sessions).
++ - Desk time is metered as one `platform.usage-sample@1` a minute (service `run`, operation `browser.desk`, unit `s`,
++ idempotency key `run:<session>:<minute>`).
++- **Desk leases on Bot**: `bot.desk-lease@1` and `bot.desk-lease-create-request@1`, with the capability
++ **`bot.desk.session`** (first-party, sensitive): Run leases one named desk for one session on the person's own machine
++ (where they let agents use it) or on a desk host whose owner offers it to other people and whose OpenVibe.Node runs
++ every desk in its own sandbox (Node v0.32.0, `desk_isolation: sandbox`); only that session's commands reach the desk,
++ never the machine itself.
++- Fixtures for each, and `test/platform-run.test.js` holds the two sides together (ids, desk names, sizes, kinds).
++
## 0.222.0 — 2026-10-27
- **Site names say their domain** (owner, 2026-10-27: "it says memberships … couldn't it say VIP memberships since the
ddiff --git a/contracts/bot/desk-lease-create-request.v1.json b/contracts/bot/desk-lease-create-request.v1.json
nnew file mode 100644
iindex 00000000..7b898c8a
---- /dev/null
++++ b/contracts/bot/desk-lease-create-request.v1.json
@@@ -0,0 +1,32 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/desk-lease-create-request.v1.json",
++ "title": "BotDeskLeaseCreateRequest",
++ "description": "bot.desk-lease-create-request@1 (the Desks plan's D6): the body of POST /api/v1/machines/{id}/desk-leases on OpenVibe.Bot (capability bot.desk.session), answered 201 with bot.desk-lease@1. DESK is the name Run chose for the session (unique per session); a desk already leased to another session, or running for the machine's owner, is refused 409 bot.desk_busy, and a repeat for the same session answers its lease (200). A machine with no room (its max_desks) is refused 409 bot.machine_busy, one that is offline 409 bot.device_offline, and one the person may not use 404 bot.device_not_found.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "session",
++ "desk",
++ "kind"
++ ],
++ "properties": {
++ "session": {
++ "$ref": "../run/session.v1.json#/$defs/session_id"
++ },
++ "desk": {
++ "$ref": "desk-lease.v1.json#/properties/desk"
++ },
++ "kind": {
++ "const": "browser"
++ },
++ "width": {
++ "$ref": "desk-lease.v1.json#/properties/width",
++ "description": "Default 1280."
++ },
++ "height": {
++ "$ref": "desk-lease.v1.json#/properties/height",
++ "description": "Default 800."
++ }
++ }
++}
ddiff --git a/contracts/bot/desk-lease.v1.json b/contracts/bot/desk-lease.v1.json
nnew file mode 100644
iindex 00000000..e6fb8fd7
---- /dev/null
++++ b/contracts/bot/desk-lease.v1.json
@@@ -0,0 +1,58 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/desk-lease.v1.json",
++ "title": "BotDeskLease",
++ "description": "bot.desk-lease@1 (the Desks plan's D6): one desk OpenVibe.Bot holds for one OpenVibe.Run session, as POST /api/v1/machines/{id}/desk-leases answers it (201, capability bot.desk.session, a Run service token acting for the session's person in X-OV-Subject) and GET /api/v1/machines/{id}/desk-leases/{desk} reads it. The request is bot.desk-lease-create-request@1. Bot leases a desk only on the person's own machine where they turned on 'Let my agent use this computer' (shared false), or on a desk host whose owner offers its desks to other people and whose OpenVibe.Node runs each desk in its own sandbox (describe desk.isolation sandbox; shared true). The lease starts the desk (bot.command@1 kind desk, start, kind browser, the lease's size) and from then on only the lease's person, through Run, may act on that desk, with screen, input and browser at POST /api/v1/machines/{id}/desk-leases/{desk}/commands (bot.command@1 without desk, answered by bot.command-result@1): never another desk, never the machine's own screen, files, programs or network, and on a shared host not even its owner's own commands reach a leased desk. DELETE /api/v1/machines/{id}/desk-leases/{desk} stops the desk and ends the lease; so does the machine going away (Run sees the lease gone, 404, and ends the session host_lost).",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "machine_id",
++ "desk",
++ "session",
++ "owner",
++ "kind",
++ "width",
++ "height",
++ "shared",
++ "created_at"
++ ],
++ "properties": {
++ "machine_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "desk": {
++ "type": "string",
++ "pattern": "^desk-[a-z0-9][a-z0-9-]{0,23}$"
++ },
++ "session": {
++ "$ref": "../run/session.v1.json#/$defs/session_id"
++ },
++ "owner": {
++ "type": "string",
++ "pattern": "^user:usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The session's person, the only one whose commands reach the desk."
++ },
++ "kind": {
++ "const": "browser"
++ },
++ "width": {
++ "type": "integer",
++ "minimum": 640,
++ "maximum": 3840
++ },
++ "height": {
++ "type": "integer",
++ "minimum": 480,
++ "maximum": 2160
++ },
++ "shared": {
++ "type": "boolean",
++ "description": "true on a desk host that serves other people (the desk runs in its own sandbox there); false on the person's own machine."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++}
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex f2a0da6c..95d7a641 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -7485,6 +7485,89 @@
"visibility": "public",
"compatibility": "backward",
"status": "active"
++ },
++ {
++ "id": "run.session",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "run/session.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "run.session-create-request",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "run/session-create-request.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "run.session-control-request",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "run/session-control-request.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "run.session-control-result",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "run/session-control-result.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "run.session-list-result",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "run/session-list-result.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "run.session.started",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "events/payloads/run.session.started.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "run.session.ended",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "events/payloads/run.session.ended.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "bot.desk-lease",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/desk-lease.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-043"
++ },
++ {
++ "id": "bot.desk-lease-create-request",
++ "version": "1.0.0",
++ "owner": "bot",
++ "schema": "bot/desk-lease-create-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-043"
}
]
}
ddiff --git a/contracts/events/payloads/run.session.ended.v1.json b/contracts/events/payloads/run.session.ended.v1.json
nnew file mode 100644
iindex 00000000..595fc1ab
---- /dev/null
++++ b/contracts/events/payloads/run.session.ended.v1.json
@@@ -0,0 +1,71 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/run.session.ended.v1.json",
++ "title": "RunSessionEndedPayload",
++ "description": "run.session.ended v1 (OpenVibe.Run; the Desks plan's D6). A session ended (end.reason ended, ttl, idle, host_lost, unplaceable or failed), its desk stopped and its lease given back; usage.desk_seconds is final. The session as run.session@1 has it. Envelope: subject { type: session, id: <session id> }, visibility internal, priority low, actor service:run.",
++ "type": "object",
++ "required": [
++ "id",
++ "owner",
++ "via",
++ "kind",
++ "state",
++ "size",
++ "ttl_s",
++ "idle_s",
++ "placement",
++ "timings",
++ "end",
++ "usage",
++ "idempotency_key"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "$ref": "../../run/session.v1.json#/$defs/session_id"
++ },
++ "owner": {
++ "$ref": "../../identity/subject-ref.v1.json"
++ },
++ "via": {
++ "$ref": "../../run/session.v1.json#/properties/via"
++ },
++ "kind": {
++ "$ref": "../../run/session.v1.json#/properties/kind"
++ },
++ "state": {
++ "const": "ended"
++ },
++ "size": {
++ "$ref": "../../run/session.v1.json#/$defs/size"
++ },
++ "ttl_s": {
++ "$ref": "../../run/session.v1.json#/properties/ttl_s"
++ },
++ "idle_s": {
++ "$ref": "../../run/session.v1.json#/properties/idle_s"
++ },
++ "placement": {
++ "anyOf": [
++ {
++ "$ref": "../../run/session.v1.json#/$defs/placement"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "timings": {
++ "$ref": "../../run/session.v1.json#/$defs/timings"
++ },
++ "end": {
++ "$ref": "../../run/session.v1.json#/$defs/end"
++ },
++ "usage": {
++ "$ref": "../../run/session.v1.json#/$defs/usage"
++ },
++ "idempotency_key": {
++ "$ref": "../../run/session.v1.json#/properties/idempotency_key"
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/run.session.started.v1.json b/contracts/events/payloads/run.session.started.v1.json
nnew file mode 100644
iindex 00000000..87f3d00f
---- /dev/null
++++ b/contracts/events/payloads/run.session.started.v1.json
@@@ -0,0 +1,64 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/run.session.started.v1.json",
++ "title": "RunSessionStartedPayload",
++ "description": "run.session.started v1 (OpenVibe.Run; the Desks plan's D6). A session's desk answered for the first time: it runs on its machine, and desk_seconds count from timings.started_at. The session as run.session@1 has it. Envelope: subject { type: session, id: <session id> }, visibility internal, priority low, actor service:run.",
++ "type": "object",
++ "required": [
++ "id",
++ "owner",
++ "via",
++ "kind",
++ "state",
++ "size",
++ "ttl_s",
++ "idle_s",
++ "placement",
++ "timings",
++ "end",
++ "usage",
++ "idempotency_key"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "$ref": "../../run/session.v1.json#/$defs/session_id"
++ },
++ "owner": {
++ "$ref": "../../identity/subject-ref.v1.json"
++ },
++ "via": {
++ "$ref": "../../run/session.v1.json#/properties/via"
++ },
++ "kind": {
++ "$ref": "../../run/session.v1.json#/properties/kind"
++ },
++ "state": {
++ "const": "running"
++ },
++ "size": {
++ "$ref": "../../run/session.v1.json#/$defs/size"
++ },
++ "ttl_s": {
++ "$ref": "../../run/session.v1.json#/properties/ttl_s"
++ },
++ "idle_s": {
++ "$ref": "../../run/session.v1.json#/properties/idle_s"
++ },
++ "placement": {
++ "$ref": "../../run/session.v1.json#/$defs/placement"
++ },
++ "timings": {
++ "$ref": "../../run/session.v1.json#/$defs/timings"
++ },
++ "end": {
++ "type": "null"
++ },
++ "usage": {
++ "$ref": "../../run/session.v1.json#/$defs/usage"
++ },
++ "idempotency_key": {
++ "$ref": "../../run/session.v1.json#/properties/idempotency_key"
++ }
++ }
++}
ddiff --git a/contracts/run/session-control-request.v1.json b/contracts/run/session-control-request.v1.json
nnew file mode 100644
iindex 00000000..1d3ee096
---- /dev/null
++++ b/contracts/run/session-control-request.v1.json
@@@ -0,0 +1,52 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/session-control-request.v1.json",
++ "title": "RunSessionControlRequest",
++ "description": "run.session-control-request@1 (the Desks plan's D6): one command on a session's desk, the body of POST /api/v1/sessions/{id}/commands on OpenVibe.Run (capability run.session.control), answered with run.session-control-result@1. KIND and VALUE are bot.command@1's for that kind (screen: a picture of the page; input: the mouse, wheel and keys on the page; browser: open, click, fill, press, back, read, screenshot) without desk, which is always the session's own: Run carries the command to OpenVibe.Bot on the session's lease (bot.desk-lease@1) and nothing on the machine other than that desk is reachable. A command on a session that is not running is refused 409 run.session.not_running. ID makes a retry safe: the same id within the session answers the first result.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "kind",
++ "value"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 64,
++ "pattern": "^[A-Za-z0-9._:-]+$"
++ },
++ "kind": {
++ "enum": [
++ "screen",
++ "input",
++ "browser"
++ ]
++ },
++ "value": {
++ "type": "object",
++ "description": "bot.command@1's value for that kind, without desk (refused 422 if it names another desk).",
++ "not": {
++ "required": [
++ "desk"
++ ]
++ }
++ }
++ },
++ "examples": [
++ {
++ "kind": "browser",
++ "value": {
++ "action": "open",
++ "url": "https://example.org/"
++ }
++ },
++ {
++ "id": "c-7",
++ "kind": "screen",
++ "value": {
++ "max_width": 1024
++ }
++ }
++ ]
++}
ddiff --git a/contracts/run/session-control-result.v1.json b/contracts/run/session-control-result.v1.json
nnew file mode 100644
iindex 00000000..0fd7d592
---- /dev/null
++++ b/contracts/run/session-control-result.v1.json
@@@ -0,0 +1,69 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/session-control-result.v1.json",
++ "title": "RunSessionControlResult",
++ "description": "run.session-control-result@1 (the Desks plan's D6): the answer to run.session-control-request@1. RESULT is bot.command-result@1's: ack (the desk did it; DATA is its answer, a page as text and refs, a picture, a receipt), nack (the desk refused; CODE is its fault code and REASON says why), refused (OpenVibe.Bot's gate refused it; CODE is Bot's refusal code) or expired (no answer in time). Every answer also says how the session stands now, so a caller sees it end (idle, ttl, host_lost) without asking again.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "session_id",
++ "id",
++ "result",
++ "state"
++ ],
++ "properties": {
++ "session_id": {
++ "$ref": "session.v1.json#/$defs/session_id"
++ },
++ "id": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 64
++ },
++ "result": {
++ "enum": [
++ "ack",
++ "nack",
++ "refused",
++ "expired"
++ ]
++ },
++ "code": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 1000
++ },
++ "data": {
++ "type": "object",
++ "description": "The desk's answer on an ack (bot.command-result@1 data)."
++ },
++ "state": {
++ "$ref": "session.v1.json#/properties/state"
++ }
++ },
++ "examples": [
++ {
++ "session_id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "id": "c-7",
++ "result": "ack",
++ "code": null,
++ "reason": null,
++ "data": {
++ "format": "jpeg",
++ "width": 1024,
++ "height": 640,
++ "content_b64": "/9j/"
++ },
++ "state": "running"
++ }
++ ]
++}
ddiff --git a/contracts/run/session-create-request.v1.json b/contracts/run/session-create-request.v1.json
nnew file mode 100644
iindex 00000000..bfce47e7
---- /dev/null
++++ b/contracts/run/session-create-request.v1.json
@@@ -0,0 +1,56 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/session-create-request.v1.json",
++ "title": "RunSessionCreateRequest",
++ "description": "run.session-create-request@1 (the Desks plan's D6): the body of POST /api/v1/sessions on OpenVibe.Run (capability run.session.create), answered 201 with run.session@1 (state placing). The session is the caller's own: a person's token, or a first-party service acting for a person (X-OV-Subject; the service is recorded as via). MACHINE names one of the person's own machines (an OpenVibe.Bot device they own and let agents use): the session goes there or ends unplaceable, never elsewhere. Without it Run places the desk: on the person's own machines when REQUIREMENTS name user-owned (and only theirs), else on desk hosts that serve other people (first-party, then community, unless REQUIREMENTS narrow it), each of whose desks runs in its own sandbox. Absent REQUIREMENTS mean { kind: run.browser, mobility: session, latency_class: interactive, objective: balanced }. TTL_S (default 3600) and IDLE_S (default 900) bound its life. IDEMPOTENCY_KEY: a repeat from the same person within 24 h with an identical body answers the first session (200); with a different body 409 run.idempotency.conflict.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "kind"
++ ],
++ "properties": {
++ "kind": {
++ "const": "browser"
++ },
++ "width": {
++ "$ref": "session.v1.json#/$defs/size/properties/width",
++ "description": "Default 1280."
++ },
++ "height": {
++ "$ref": "session.v1.json#/$defs/size/properties/height",
++ "description": "Default 800."
++ },
++ "ttl_s": {
++ "$ref": "session.v1.json#/properties/ttl_s",
++ "description": "Default 3600."
++ },
++ "idle_s": {
++ "$ref": "session.v1.json#/properties/idle_s",
++ "description": "Default 900."
++ },
++ "machine": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "One of the person's own machines, by its OpenVibe.Bot id; the session goes there or nowhere."
++ },
++ "requirements": {
++ "$ref": "../platform/workload-requirements.v1.json"
++ },
++ "idempotency_key": {
++ "$ref": "session.v1.json#/$defs/idempotency_key"
++ }
++ },
++ "examples": [
++ {
++ "kind": "browser"
++ },
++ {
++ "kind": "browser",
++ "width": 1440,
++ "height": 900,
++ "ttl_s": 1800,
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "idempotency_key": "actor:tsk_01JD2C3D4E5F6G7H8J9K0MNPQR"
++ }
++ ]
++}
ddiff --git a/contracts/run/session-list-result.v1.json b/contracts/run/session-list-result.v1.json
nnew file mode 100644
iindex 00000000..5bedbf2b
---- /dev/null
++++ b/contracts/run/session-list-result.v1.json
@@@ -0,0 +1,28 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/session-list-result.v1.json",
++ "title": "RunSessionListResult",
++ "description": "run.session-list-result@1 (the Desks plan's D6): GET /api/v1/sessions on OpenVibe.Run (capability run.session.read): the caller's own sessions, newest first, running ones by default (?state=ended or ?state=all for others), at most limit (default 20, at most 100) per page; next_cursor continues the list and is null on the last page.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "sessions",
++ "next_cursor"
++ ],
++ "properties": {
++ "sessions": {
++ "type": "array",
++ "maxItems": 100,
++ "items": {
++ "$ref": "session.v1.json"
++ }
++ },
++ "next_cursor": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 200
++ }
++ }
++}
ddiff --git a/contracts/run/session.v1.json b/contracts/run/session.v1.json
nnew file mode 100644
iindex 00000000..6d29bff6
---- /dev/null
++++ b/contracts/run/session.v1.json
@@@ -0,0 +1,378 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/session.v1.json",
++ "title": "RunSession",
++ "description": "run.session@1 (the Desks plan's D6): one session on OpenVibe.Run, a browser desk held for one person, as GET /api/v1/sessions/{id} answers it (capability run.session.read) and run.session-list-result@1, POST /api/v1/sessions (run.session.create) and POST /api/v1/sessions/{id}/end (run.session.control) carry it. A session is a desk (bot.command@1 kind desk, kind browser) that Run places on a machine and OpenVibe.Bot leases to it (bot.desk-lease@1): on one of the person's own machines where they let agents use it (trust user-owned, never another person's), or on a desk host that serves other people (trust first-party or community: the host's OpenVibe.Node runs every desk in its own sandbox, desk_isolation sandbox, and offers nothing else of the machine). STATES: placing (stored, looking for a machine) -> starting (the desk is leased and starting) -> running (the desk answered) -> ended. An ended session never changes; end.reason says why: ended (POST /api/v1/sessions/{id}/end), ttl (ttl_s ran out), idle (no command for idle_s), host_lost (its machine went away or stopped the desk), unplaceable (no machine had room before ttl_s, or none at all) or failed (the desk would not start). USAGE: desk_seconds counts from started_at to ended_at (or now while running), metered to OpenVibe.Billing as one platform.usage-sample@1 per minute (service run, operation browser.desk, unit s, idempotency_key run:<session id>:<minute>). Run mints the id (ses_<ULID>).",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "owner",
++ "via",
++ "kind",
++ "state",
++ "size",
++ "ttl_s",
++ "idle_s",
++ "placement",
++ "timings",
++ "end",
++ "usage",
++ "idempotency_key"
++ ],
++ "properties": {
++ "id": {
++ "$ref": "#/$defs/session_id"
++ },
++ "owner": {
++ "$ref": "../identity/subject-ref.v1.json",
++ "description": "The person the session is for: only they (or a first-party service acting for them) read, act on or end it."
++ },
++ "via": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^service:[a-z][a-z0-9-]{0,40}$",
++ "description": "The first-party service that asked for the person (service:actor), or null when they asked themselves."
++ },
++ "kind": {
++ "const": "browser",
++ "description": "A platform.runtime-class@1 name; browser (a headless browser driven over its pipe) is the only kind of session in v1."
++ },
++ "state": {
++ "enum": [
++ "placing",
++ "starting",
++ "running",
++ "ended"
++ ]
++ },
++ "size": {
++ "$ref": "#/$defs/size"
++ },
++ "ttl_s": {
++ "type": "integer",
++ "minimum": 60,
++ "maximum": 14400,
++ "description": "The most the session lives, from created_at."
++ },
++ "idle_s": {
++ "type": "integer",
++ "minimum": 60,
++ "maximum": 3600,
++ "description": "A session no command used this long ends (end.reason idle)."
++ },
++ "placement": {
++ "anyOf": [
++ {
++ "$ref": "#/$defs/placement"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "timings": {
++ "$ref": "#/$defs/timings"
++ },
++ "end": {
++ "anyOf": [
++ {
++ "$ref": "#/$defs/end"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "usage": {
++ "$ref": "#/$defs/usage"
++ },
++ "idempotency_key": {
++ "anyOf": [
++ {
++ "$ref": "#/$defs/idempotency_key"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "required": [
++ "state"
++ ],
++ "properties": {
++ "state": {
++ "const": "ended"
++ }
++ }
++ },
++ "then": {
++ "properties": {
++ "end": {
++ "type": "object"
++ },
++ "timings": {
++ "type": "object",
++ "properties": {
++ "ended_at": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ },
++ "else": {
++ "properties": {
++ "end": {
++ "type": "null"
++ },
++ "timings": {
++ "type": "object",
++ "properties": {
++ "ended_at": {
++ "type": "null"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "required": [
++ "state"
++ ],
++ "properties": {
++ "state": {
++ "enum": [
++ "starting",
++ "running"
++ ]
++ }
++ }
++ },
++ "then": {
++ "properties": {
++ "placement": {
++ "type": "object"
++ }
++ }
++ }
++ }
++ ],
++ "$defs": {
++ "session_id": {
++ "type": "string",
++ "pattern": "^ses_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "size": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "width",
++ "height"
++ ],
++ "properties": {
++ "width": {
++ "type": "integer",
++ "minimum": 640,
++ "maximum": 3840
++ },
++ "height": {
++ "type": "integer",
++ "minimum": 480,
++ "maximum": 2160
++ }
++ },
++ "description": "The desk's page size (bot.command@1 kind desk width and height)."
++ },
++ "placement": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "machine",
++ "desk",
++ "trust",
++ "offer_id",
++ "region",
++ "reasons"
++ ],
++ "properties": {
++ "machine": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine (OpenVibe.Bot device) the desk runs on."
++ },
++ "desk": {
++ "type": "string",
++ "pattern": "^desk-[a-z0-9][a-z0-9-]{0,23}$",
++ "description": "The desk's name there (bot.command@1 desk), leased to this session alone."
++ },
++ "trust": {
++ "enum": [
++ "user-owned",
++ "first-party",
++ "partner",
++ "community"
++ ],
++ "description": "The offer's trust class (platform.resource-offer@1): user-owned is always the owner's own machine."
++ },
++ "offer_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 128,
++ "description": "The platform.resource-offer@1 it was placed on; null when the request named the person's own machine."
++ },
++ "region": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 64
++ },
++ "reasons": {
++ "type": "array",
++ "maxItems": 8,
++ "items": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "description": "Why this machine, in a few words each (the placer's reasons)."
++ }
++ }
++ },
++ "idempotency_key": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 128,
++ "pattern": "^[A-Za-z0-9._:-]+$"
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created_at",
++ "started_at",
++ "last_used_at",
++ "ended_at"
++ ],
++ "properties": {
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "started_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "When the desk first answered (state running)."
++ },
++ "last_used_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "The last command, for idle_s."
++ },
++ "ended_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ },
++ "usage": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "desk_seconds"
++ ],
++ "properties": {
++ "desk_seconds": {
++ "type": "number",
++ "minimum": 0,
++ "description": "Seconds the desk ran (started_at to ended_at, or to now while running); 0 when it never started."
++ }
++ }
++ },
++ "end": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "reason",
++ "detail"
++ ],
++ "properties": {
++ "reason": {
++ "enum": [
++ "ended",
++ "ttl",
++ "idle",
++ "host_lost",
++ "unplaceable",
++ "failed"
++ ]
++ },
++ "detail": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 500
++ }
++ }
++ }
++ },
++ "examples": [
++ {
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": "service:actor",
++ "kind": "browser",
++ "state": "running",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": {
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "desk-s0mnpqr",
++ "trust": "community",
++ "offer_id": "desk:dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "region": "eu",
++ "reasons": [
++ "a shared desk host with room"
++ ]
++ },
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": "2026-10-28T10:00:04Z",
++ "last_used_at": "2026-10-28T10:05:00Z",
++ "ended_at": null
++ },
++ "end": null,
++ "usage": {
++ "desk_seconds": 296
++ },
++ "idempotency_key": null
++ }
++ ]
++}
ddiff --git a/fixtures/bot.desk-lease-create-request/invalid/desktop.json b/fixtures/bot.desk-lease-create-request/invalid/desktop.json
nnew file mode 100644
iindex 00000000..34d54815
---- /dev/null
++++ b/fixtures/bot.desk-lease-create-request/invalid/desktop.json
@@@ -0,0 +1,5 @@
++{
++ "session": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "desk": "desk-s0mnpqr",
++ "kind": "desktop"
++}
ddiff --git a/fixtures/bot.desk-lease-create-request/invalid/main.json b/fixtures/bot.desk-lease-create-request/invalid/main.json
nnew file mode 100644
iindex 00000000..971376b6
---- /dev/null
++++ b/fixtures/bot.desk-lease-create-request/invalid/main.json
@@@ -0,0 +1,5 @@
++{
++ "session": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "desk": "main",
++ "kind": "browser"
++}
ddiff --git a/fixtures/bot.desk-lease-create-request/valid/browser.json b/fixtures/bot.desk-lease-create-request/valid/browser.json
nnew file mode 100644
iindex 00000000..1bbd521e
---- /dev/null
++++ b/fixtures/bot.desk-lease-create-request/valid/browser.json
@@@ -0,0 +1,7 @@
++{
++ "session": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "desk": "desk-s0mnpqr",
++ "kind": "browser",
++ "width": 1280,
++ "height": 800
++}
ddiff --git a/fixtures/bot.desk-lease/invalid/private-desk.json b/fixtures/bot.desk-lease/invalid/private-desk.json
nnew file mode 100644
iindex 00000000..b2c19937
---- /dev/null
++++ b/fixtures/bot.desk-lease/invalid/private-desk.json
@@@ -0,0 +1,11 @@
++{
++ "machine_id": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "private",
++ "session": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": "user:usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "kind": "browser",
++ "width": 1280,
++ "height": 800,
++ "shared": true,
++ "created_at": "2026-10-28T10:00:01Z"
++}
ddiff --git a/fixtures/bot.desk-lease/invalid/service-owner.json b/fixtures/bot.desk-lease/invalid/service-owner.json
nnew file mode 100644
iindex 00000000..489f89b4
---- /dev/null
++++ b/fixtures/bot.desk-lease/invalid/service-owner.json
@@@ -0,0 +1,11 @@
++{
++ "machine_id": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "desk-s0mnpqr",
++ "session": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": "service:run",
++ "kind": "browser",
++ "width": 1280,
++ "height": 800,
++ "shared": true,
++ "created_at": "2026-10-28T10:00:01Z"
++}
ddiff --git a/fixtures/bot.desk-lease/valid/shared-host.json b/fixtures/bot.desk-lease/valid/shared-host.json
nnew file mode 100644
iindex 00000000..e062f40c
---- /dev/null
++++ b/fixtures/bot.desk-lease/valid/shared-host.json
@@@ -0,0 +1,11 @@
++{
++ "machine_id": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "desk-s0mnpqr",
++ "session": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": "user:usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "kind": "browser",
++ "width": 1280,
++ "height": 800,
++ "shared": true,
++ "created_at": "2026-10-28T10:00:01Z"
++}
ddiff --git a/fixtures/run.session-control-request/invalid/exec.json b/fixtures/run.session-control-request/invalid/exec.json
nnew file mode 100644
iindex 00000000..1eca0d3d
---- /dev/null
++++ b/fixtures/run.session-control-request/invalid/exec.json
@@@ -0,0 +1,8 @@
++{
++ "kind": "exec",
++ "value": {
++ "argv": [
++ "id"
++ ]
++ }
++}
ddiff --git a/fixtures/run.session-control-request/invalid/names-a-desk.json b/fixtures/run.session-control-request/invalid/names-a-desk.json
nnew file mode 100644
iindex 00000000..ea166b2b
---- /dev/null
++++ b/fixtures/run.session-control-request/invalid/names-a-desk.json
@@@ -0,0 +1,7 @@
++{
++ "kind": "browser",
++ "value": {
++ "action": "read",
++ "desk": "desk-other"
++ }
++}
ddiff --git a/fixtures/run.session-control-request/valid/open.json b/fixtures/run.session-control-request/valid/open.json
nnew file mode 100644
iindex 00000000..d87115eb
---- /dev/null
++++ b/fixtures/run.session-control-request/valid/open.json
@@@ -0,0 +1,7 @@
++{
++ "kind": "browser",
++ "value": {
++ "action": "open",
++ "url": "https://example.org/"
++ }
++}
ddiff --git a/fixtures/run.session-control-request/valid/screen.json b/fixtures/run.session-control-request/valid/screen.json
nnew file mode 100644
iindex 00000000..8b6485b0
---- /dev/null
++++ b/fixtures/run.session-control-request/valid/screen.json
@@@ -0,0 +1,7 @@
++{
++ "id": "c-7",
++ "kind": "screen",
++ "value": {
++ "max_width": 1024
++ }
++}
ddiff --git a/fixtures/run.session-control-result/invalid/no-state.json b/fixtures/run.session-control-result/invalid/no-state.json
nnew file mode 100644
iindex 00000000..b8d6d97d
---- /dev/null
++++ b/fixtures/run.session-control-result/invalid/no-state.json
@@@ -0,0 +1,5 @@
++{
++ "session_id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "id": "c-7",
++ "result": "ack"
++}
ddiff --git a/fixtures/run.session-control-result/valid/picture.json b/fixtures/run.session-control-result/valid/picture.json
nnew file mode 100644
iindex 00000000..4834e793
---- /dev/null
++++ b/fixtures/run.session-control-result/valid/picture.json
@@@ -0,0 +1,14 @@
++{
++ "session_id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "id": "c-7",
++ "result": "ack",
++ "code": null,
++ "reason": null,
++ "data": {
++ "format": "jpeg",
++ "width": 1024,
++ "height": 640,
++ "content_b64": "/9j/"
++ },
++ "state": "running"
++}
ddiff --git a/fixtures/run.session-control-result/valid/refused.json b/fixtures/run.session-control-result/valid/refused.json
nnew file mode 100644
iindex 00000000..f1b618ad
---- /dev/null
++++ b/fixtures/run.session-control-result/valid/refused.json
@@@ -0,0 +1,8 @@
++{
++ "session_id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "id": "c-8",
++ "result": "refused",
++ "code": "bot.device_offline",
++ "reason": "the machine is offline",
++ "state": "running"
++}
ddiff --git a/fixtures/run.session-create-request/invalid/no-kind.json b/fixtures/run.session-create-request/invalid/no-kind.json
nnew file mode 100644
iindex 00000000..aa73dbc9
---- /dev/null
++++ b/fixtures/run.session-create-request/invalid/no-kind.json
@@@ -0,0 +1,3 @@
++{
++ "width": 1280
++}
ddiff --git a/fixtures/run.session-create-request/invalid/robot-id.json b/fixtures/run.session-create-request/invalid/robot-id.json
nnew file mode 100644
iindex 00000000..0ce2cb90
---- /dev/null
++++ b/fixtures/run.session-create-request/invalid/robot-id.json
@@@ -0,0 +1,4 @@
++{
++ "kind": "browser",
++ "machine": "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X"
++}
ddiff --git a/fixtures/run.session-create-request/invalid/too-long.json b/fixtures/run.session-create-request/invalid/too-long.json
nnew file mode 100644
iindex 00000000..ccf25e70
---- /dev/null
++++ b/fixtures/run.session-create-request/invalid/too-long.json
@@@ -0,0 +1,4 @@
++{
++ "kind": "browser",
++ "ttl_s": 86400
++}
ddiff --git a/fixtures/run.session-create-request/valid/community-only.json b/fixtures/run.session-create-request/valid/community-only.json
nnew file mode 100644
iindex 00000000..cd5d6fcb
---- /dev/null
++++ b/fixtures/run.session-create-request/valid/community-only.json
@@@ -0,0 +1,12 @@
++{
++ "kind": "browser",
++ "requirements": {
++ "kind": "run.browser",
++ "mobility": "session",
++ "latency_class": "interactive",
++ "objective": "cheapest",
++ "trust": [
++ "community"
++ ]
++ }
++}
ddiff --git a/fixtures/run.session-create-request/valid/default.json b/fixtures/run.session-create-request/valid/default.json
nnew file mode 100644
iindex 00000000..1a960fa9
---- /dev/null
++++ b/fixtures/run.session-create-request/valid/default.json
@@@ -0,0 +1,3 @@
++{
++ "kind": "browser"
++}
ddiff --git a/fixtures/run.session-create-request/valid/own-machine.json b/fixtures/run.session-create-request/valid/own-machine.json
nnew file mode 100644
iindex 00000000..56f69035
---- /dev/null
++++ b/fixtures/run.session-create-request/valid/own-machine.json
@@@ -0,0 +1,9 @@
++{
++ "kind": "browser",
++ "width": 1440,
++ "height": 900,
++ "ttl_s": 1800,
++ "idle_s": 600,
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "idempotency_key": "actor:tsk_01JD2C3D4E5F6G7H8J9K0MNPQR"
++}
ddiff --git a/fixtures/run.session-list-result/invalid/no-cursor.json b/fixtures/run.session-list-result/invalid/no-cursor.json
nnew file mode 100644
iindex 00000000..f56218ab
---- /dev/null
++++ b/fixtures/run.session-list-result/invalid/no-cursor.json
@@@ -0,0 +1,3 @@
++{
++ "sessions": []
++}
ddiff --git a/fixtures/run.session-list-result/valid/one.json b/fixtures/run.session-list-result/valid/one.json
nnew file mode 100644
iindex 00000000..d09d5f37
---- /dev/null
++++ b/fixtures/run.session-list-result/valid/one.json
@@@ -0,0 +1,42 @@
++{
++ "sessions": [
++ {
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": "service:actor",
++ "kind": "browser",
++ "state": "running",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": {
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "desk-s0mnpqr",
++ "trust": "community",
++ "offer_id": "desk:dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "region": "eu",
++ "reasons": [
++ "a shared desk host with room"
++ ]
++ },
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": "2026-10-28T10:00:04Z",
++ "last_used_at": "2026-10-28T10:05:00Z",
++ "ended_at": null
++ },
++ "end": null,
++ "usage": {
++ "desk_seconds": 296
++ },
++ "idempotency_key": null
++ }
++ ],
++ "next_cursor": null
++}
ddiff --git a/fixtures/run.session.ended/invalid/no-end.json b/fixtures/run.session.ended/invalid/no-end.json
nnew file mode 100644
iindex 00000000..9be52d04
---- /dev/null
++++ b/fixtures/run.session.ended/invalid/no-end.json
@@@ -0,0 +1,37 @@
++{
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": "service:actor",
++ "kind": "browser",
++ "state": "ended",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": {
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "desk-s0mnpqr",
++ "trust": "community",
++ "offer_id": "desk:dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "region": "eu",
++ "reasons": [
++ "a shared desk host with room"
++ ]
++ },
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": "2026-10-28T10:00:04Z",
++ "last_used_at": "2026-10-28T10:05:00Z",
++ "ended_at": "2026-10-28T10:20:00Z"
++ },
++ "end": null,
++ "usage": {
++ "desk_seconds": 1196
++ },
++ "idempotency_key": null
++}
ddiff --git a/fixtures/run.session.ended/valid/idle.json b/fixtures/run.session.ended/valid/idle.json
nnew file mode 100644
iindex 00000000..0840abe8
---- /dev/null
++++ b/fixtures/run.session.ended/valid/idle.json
@@@ -0,0 +1,40 @@
++{
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": "service:actor",
++ "kind": "browser",
++ "state": "ended",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": {
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "desk-s0mnpqr",
++ "trust": "community",
++ "offer_id": "desk:dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "region": "eu",
++ "reasons": [
++ "a shared desk host with room"
++ ]
++ },
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": "2026-10-28T10:00:04Z",
++ "last_used_at": "2026-10-28T10:05:00Z",
++ "ended_at": "2026-10-28T10:20:00Z"
++ },
++ "end": {
++ "reason": "idle",
++ "detail": "no command for 900 s"
++ },
++ "usage": {
++ "desk_seconds": 1196
++ },
++ "idempotency_key": null
++}
ddiff --git a/fixtures/run.session.started/invalid/still-placing.json b/fixtures/run.session.started/invalid/still-placing.json
nnew file mode 100644
iindex 00000000..226d6fd8
---- /dev/null
++++ b/fixtures/run.session.started/invalid/still-placing.json
@@@ -0,0 +1,37 @@
++{
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": "service:actor",
++ "kind": "browser",
++ "state": "placing",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": {
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "desk-s0mnpqr",
++ "trust": "community",
++ "offer_id": "desk:dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "region": "eu",
++ "reasons": [
++ "a shared desk host with room"
++ ]
++ },
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": "2026-10-28T10:00:04Z",
++ "last_used_at": "2026-10-28T10:05:00Z",
++ "ended_at": null
++ },
++ "end": null,
++ "usage": {
++ "desk_seconds": 296
++ },
++ "idempotency_key": null
++}
ddiff --git a/fixtures/run.session.started/valid/shared-host.json b/fixtures/run.session.started/valid/shared-host.json
nnew file mode 100644
iindex 00000000..97dc8cb6
---- /dev/null
++++ b/fixtures/run.session.started/valid/shared-host.json
@@@ -0,0 +1,37 @@
++{
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": "service:actor",
++ "kind": "browser",
++ "state": "running",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": {
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "desk-s0mnpqr",
++ "trust": "community",
++ "offer_id": "desk:dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "region": "eu",
++ "reasons": [
++ "a shared desk host with room"
++ ]
++ },
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": "2026-10-28T10:00:04Z",
++ "last_used_at": "2026-10-28T10:05:00Z",
++ "ended_at": null
++ },
++ "end": null,
++ "usage": {
++ "desk_seconds": 296
++ },
++ "idempotency_key": null
++}
ddiff --git a/fixtures/run.session/invalid/desktop-kind.json b/fixtures/run.session/invalid/desktop-kind.json
nnew file mode 100644
iindex 00000000..8a45f2fe
---- /dev/null
++++ b/fixtures/run.session/invalid/desktop-kind.json
@@@ -0,0 +1,37 @@
++{
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": "service:actor",
++ "kind": "desktop",
++ "state": "running",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": {
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "desk-s0mnpqr",
++ "trust": "community",
++ "offer_id": "desk:dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "region": "eu",
++ "reasons": [
++ "a shared desk host with room"
++ ]
++ },
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": "2026-10-28T10:00:04Z",
++ "last_used_at": "2026-10-28T10:05:00Z",
++ "ended_at": null
++ },
++ "end": null,
++ "usage": {
++ "desk_seconds": 296
++ },
++ "idempotency_key": null
++}
ddiff --git a/fixtures/run.session/invalid/ended-without-reason.json b/fixtures/run.session/invalid/ended-without-reason.json
nnew file mode 100644
iindex 00000000..9be52d04
---- /dev/null
++++ b/fixtures/run.session/invalid/ended-without-reason.json
@@@ -0,0 +1,37 @@
++{
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": "service:actor",
++ "kind": "browser",
++ "state": "ended",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": {
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "desk-s0mnpqr",
++ "trust": "community",
++ "offer_id": "desk:dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "region": "eu",
++ "reasons": [
++ "a shared desk host with room"
++ ]
++ },
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": "2026-10-28T10:00:04Z",
++ "last_used_at": "2026-10-28T10:05:00Z",
++ "ended_at": "2026-10-28T10:20:00Z"
++ },
++ "end": null,
++ "usage": {
++ "desk_seconds": 1196
++ },
++ "idempotency_key": null
++}
ddiff --git a/fixtures/run.session/invalid/main-screen.json b/fixtures/run.session/invalid/main-screen.json
nnew file mode 100644
iindex 00000000..2fa9bda9
---- /dev/null
++++ b/fixtures/run.session/invalid/main-screen.json
@@@ -0,0 +1,37 @@
++{
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": "service:actor",
++ "kind": "browser",
++ "state": "running",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": {
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "main",
++ "trust": "community",
++ "offer_id": "desk:dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "region": "eu",
++ "reasons": [
++ "a shared desk host with room"
++ ]
++ },
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": "2026-10-28T10:00:04Z",
++ "last_used_at": "2026-10-28T10:05:00Z",
++ "ended_at": null
++ },
++ "end": null,
++ "usage": {
++ "desk_seconds": 296
++ },
++ "idempotency_key": null
++}
ddiff --git a/fixtures/run.session/invalid/running-unplaced.json b/fixtures/run.session/invalid/running-unplaced.json
nnew file mode 100644
iindex 00000000..0971f9ea
---- /dev/null
++++ b/fixtures/run.session/invalid/running-unplaced.json
@@@ -0,0 +1,28 @@
++{
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": "service:actor",
++ "kind": "browser",
++ "state": "running",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": null,
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": "2026-10-28T10:00:04Z",
++ "last_used_at": "2026-10-28T10:05:00Z",
++ "ended_at": null
++ },
++ "end": null,
++ "usage": {
++ "desk_seconds": 296
++ },
++ "idempotency_key": null
++}
ddiff --git a/fixtures/run.session/invalid/running-with-end.json b/fixtures/run.session/invalid/running-with-end.json
nnew file mode 100644
iindex 00000000..c0415935
---- /dev/null
++++ b/fixtures/run.session/invalid/running-with-end.json
@@@ -0,0 +1,40 @@
++{
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": "service:actor",
++ "kind": "browser",
++ "state": "running",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": {
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "desk-s0mnpqr",
++ "trust": "community",
++ "offer_id": "desk:dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "region": "eu",
++ "reasons": [
++ "a shared desk host with room"
++ ]
++ },
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": "2026-10-28T10:00:04Z",
++ "last_used_at": "2026-10-28T10:05:00Z",
++ "ended_at": null
++ },
++ "end": {
++ "reason": "idle",
++ "detail": null
++ },
++ "usage": {
++ "desk_seconds": 296
++ },
++ "idempotency_key": null
++}
ddiff --git a/fixtures/run.session/valid/ended-idle.json b/fixtures/run.session/valid/ended-idle.json
nnew file mode 100644
iindex 00000000..0840abe8
---- /dev/null
++++ b/fixtures/run.session/valid/ended-idle.json
@@@ -0,0 +1,40 @@
++{
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": "service:actor",
++ "kind": "browser",
++ "state": "ended",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": {
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "desk-s0mnpqr",
++ "trust": "community",
++ "offer_id": "desk:dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "region": "eu",
++ "reasons": [
++ "a shared desk host with room"
++ ]
++ },
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": "2026-10-28T10:00:04Z",
++ "last_used_at": "2026-10-28T10:05:00Z",
++ "ended_at": "2026-10-28T10:20:00Z"
++ },
++ "end": {
++ "reason": "idle",
++ "detail": "no command for 900 s"
++ },
++ "usage": {
++ "desk_seconds": 1196
++ },
++ "idempotency_key": null
++}
ddiff --git a/fixtures/run.session/valid/ended-unplaceable.json b/fixtures/run.session/valid/ended-unplaceable.json
nnew file mode 100644
iindex 00000000..5bc35f2c
---- /dev/null
++++ b/fixtures/run.session/valid/ended-unplaceable.json
@@@ -0,0 +1,31 @@
++{
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": null,
++ "kind": "browser",
++ "state": "ended",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": null,
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": null,
++ "last_used_at": null,
++ "ended_at": "2026-10-28T10:01:00Z"
++ },
++ "end": {
++ "reason": "unplaceable",
++ "detail": "no desk host had room"
++ },
++ "usage": {
++ "desk_seconds": 0
++ },
++ "idempotency_key": "actor:tsk_01JD2C3D4E5F6G7H8J9K0MNPQR"
++}
ddiff --git a/fixtures/run.session/valid/placing.json b/fixtures/run.session/valid/placing.json
nnew file mode 100644
iindex 00000000..a319e7a7
---- /dev/null
++++ b/fixtures/run.session/valid/placing.json
@@@ -0,0 +1,28 @@
++{
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": null,
++ "kind": "browser",
++ "state": "placing",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": null,
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": null,
++ "last_used_at": null,
++ "ended_at": null
++ },
++ "end": null,
++ "usage": {
++ "desk_seconds": 0
++ },
++ "idempotency_key": "actor:tsk_01JD2C3D4E5F6G7H8J9K0MNPQR"
++}
ddiff --git a/fixtures/run.session/valid/running-own-machine.json b/fixtures/run.session/valid/running-own-machine.json
nnew file mode 100644
iindex 00000000..952a4e59
---- /dev/null
++++ b/fixtures/run.session/valid/running-own-machine.json
@@@ -0,0 +1,37 @@
++{
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": "service:actor",
++ "kind": "browser",
++ "state": "running",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": {
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "desk-s0mnpqr",
++ "trust": "user-owned",
++ "offer_id": null,
++ "region": null,
++ "reasons": [
++ "your own machine, as asked"
++ ]
++ },
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": "2026-10-28T10:00:04Z",
++ "last_used_at": "2026-10-28T10:05:00Z",
++ "ended_at": null
++ },
++ "end": null,
++ "usage": {
++ "desk_seconds": 296
++ },
++ "idempotency_key": null
++}
ddiff --git a/fixtures/run.session/valid/running-shared.json b/fixtures/run.session/valid/running-shared.json
nnew file mode 100644
iindex 00000000..97dc8cb6
---- /dev/null
++++ b/fixtures/run.session/valid/running-shared.json
@@@ -0,0 +1,37 @@
++{
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": "service:actor",
++ "kind": "browser",
++ "state": "running",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": {
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "desk-s0mnpqr",
++ "trust": "community",
++ "offer_id": "desk:dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "region": "eu",
++ "reasons": [
++ "a shared desk host with room"
++ ]
++ },
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": "2026-10-28T10:00:04Z",
++ "last_used_at": "2026-10-28T10:05:00Z",
++ "ended_at": null
++ },
++ "end": null,
++ "usage": {
++ "desk_seconds": 296
++ },
++ "idempotency_key": null
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex ad5a6012..eecb98a7 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -83773,6 +83773,859 @@
}
}
}
++ },
++ "run.session": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/session.v1.json",
++ "title": "RunSession",
++ "description": "run.session@1 (the Desks plan's D6): one session on OpenVibe.Run, a browser desk held for one person, as GET /api/v1/sessions/{id} answers it (capability run.session.read) and run.session-list-result@1, POST /api/v1/sessions (run.session.create) and POST /api/v1/sessions/{id}/end (run.session.control) carry it. A session is a desk (bot.command@1 kind desk, kind browser) that Run places on a machine and OpenVibe.Bot leases to it (bot.desk-lease@1): on one of the person's own machines where they let agents use it (trust user-owned, never another person's), or on a desk host that serves other people (trust first-party or community: the host's OpenVibe.Node runs every desk in its own sandbox, desk_isolation sandbox, and offers nothing else of the machine). STATES: placing (stored, looking for a machine) -> starting (the desk is leased and starting) -> running (the desk answered) -> ended. An ended session never changes; end.reason says why: ended (POST /api/v1/sessions/{id}/end), ttl (ttl_s ran out), idle (no command for idle_s), host_lost (its machine went away or stopped the desk), unplaceable (no machine had room before ttl_s, or none at all) or failed (the desk would not start). USAGE: desk_seconds counts from started_at to ended_at (or now while running), metered to OpenVibe.Billing as one platform.usage-sample@1 per minute (service run, operation browser.desk, unit s, idempotency_key run:<session id>:<minute>). Run mints the id (ses_<ULID>).",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "owner",
++ "via",
++ "kind",
++ "state",
++ "size",
++ "ttl_s",
++ "idle_s",
++ "placement",
++ "timings",
++ "end",
++ "usage",
++ "idempotency_key"
++ ],
++ "properties": {
++ "id": {
++ "$ref": "#/$defs/session_id"
++ },
++ "owner": {
++ "$ref": "../identity/subject-ref.v1.json",
++ "description": "The person the session is for: only they (or a first-party service acting for them) read, act on or end it."
++ },
++ "via": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^service:[a-z][a-z0-9-]{0,40}$",
++ "description": "The first-party service that asked for the person (service:actor), or null when they asked themselves."
++ },
++ "kind": {
++ "const": "browser",
++ "description": "A platform.runtime-class@1 name; browser (a headless browser driven over its pipe) is the only kind of session in v1."
++ },
++ "state": {
++ "enum": [
++ "placing",
++ "starting",
++ "running",
++ "ended"
++ ]
++ },
++ "size": {
++ "$ref": "#/$defs/size"
++ },
++ "ttl_s": {
++ "type": "integer",
++ "minimum": 60,
++ "maximum": 14400,
++ "description": "The most the session lives, from created_at."
++ },
++ "idle_s": {
++ "type": "integer",
++ "minimum": 60,
++ "maximum": 3600,
++ "description": "A session no command used this long ends (end.reason idle)."
++ },
++ "placement": {
++ "anyOf": [
++ {
++ "$ref": "#/$defs/placement"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "timings": {
++ "$ref": "#/$defs/timings"
++ },
++ "end": {
++ "anyOf": [
++ {
++ "$ref": "#/$defs/end"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "usage": {
++ "$ref": "#/$defs/usage"
++ },
++ "idempotency_key": {
++ "anyOf": [
++ {
++ "$ref": "#/$defs/idempotency_key"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "required": [
++ "state"
++ ],
++ "properties": {
++ "state": {
++ "const": "ended"
++ }
++ }
++ },
++ "then": {
++ "properties": {
++ "end": {
++ "type": "object"
++ },
++ "timings": {
++ "type": "object",
++ "properties": {
++ "ended_at": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ },
++ "else": {
++ "properties": {
++ "end": {
++ "type": "null"
++ },
++ "timings": {
++ "type": "object",
++ "properties": {
++ "ended_at": {
++ "type": "null"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "required": [
++ "state"
++ ],
++ "properties": {
++ "state": {
++ "enum": [
++ "starting",
++ "running"
++ ]
++ }
++ }
++ },
++ "then": {
++ "properties": {
++ "placement": {
++ "type": "object"
++ }
++ }
++ }
++ }
++ ],
++ "$defs": {
++ "session_id": {
++ "type": "string",
++ "pattern": "^ses_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "size": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "width",
++ "height"
++ ],
++ "properties": {
++ "width": {
++ "type": "integer",
++ "minimum": 640,
++ "maximum": 3840
++ },
++ "height": {
++ "type": "integer",
++ "minimum": 480,
++ "maximum": 2160
++ }
++ },
++ "description": "The desk's page size (bot.command@1 kind desk width and height)."
++ },
++ "placement": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "machine",
++ "desk",
++ "trust",
++ "offer_id",
++ "region",
++ "reasons"
++ ],
++ "properties": {
++ "machine": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine (OpenVibe.Bot device) the desk runs on."
++ },
++ "desk": {
++ "type": "string",
++ "pattern": "^desk-[a-z0-9][a-z0-9-]{0,23}$",
++ "description": "The desk's name there (bot.command@1 desk), leased to this session alone."
++ },
++ "trust": {
++ "enum": [
++ "user-owned",
++ "first-party",
++ "partner",
++ "community"
++ ],
++ "description": "The offer's trust class (platform.resource-offer@1): user-owned is always the owner's own machine."
++ },
++ "offer_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 128,
++ "description": "The platform.resource-offer@1 it was placed on; null when the request named the person's own machine."
++ },
++ "region": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 64
++ },
++ "reasons": {
++ "type": "array",
++ "maxItems": 8,
++ "items": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "description": "Why this machine, in a few words each (the placer's reasons)."
++ }
++ }
++ },
++ "idempotency_key": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 128,
++ "pattern": "^[A-Za-z0-9._:-]+$"
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created_at",
++ "started_at",
++ "last_used_at",
++ "ended_at"
++ ],
++ "properties": {
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "started_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "When the desk first answered (state running)."
++ },
++ "last_used_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "The last command, for idle_s."
++ },
++ "ended_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ },
++ "usage": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "desk_seconds"
++ ],
++ "properties": {
++ "desk_seconds": {
++ "type": "number",
++ "minimum": 0,
++ "description": "Seconds the desk ran (started_at to ended_at, or to now while running); 0 when it never started."
++ }
++ }
++ },
++ "end": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "reason",
++ "detail"
++ ],
++ "properties": {
++ "reason": {
++ "enum": [
++ "ended",
++ "ttl",
++ "idle",
++ "host_lost",
++ "unplaceable",
++ "failed"
++ ]
++ },
++ "detail": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 500
++ }
++ }
++ }
++ },
++ "examples": [
++ {
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": "service:actor",
++ "kind": "browser",
++ "state": "running",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": {
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "desk-s0mnpqr",
++ "trust": "community",
++ "offer_id": "desk:dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "region": "eu",
++ "reasons": [
++ "a shared desk host with room"
++ ]
++ },
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": "2026-10-28T10:00:04Z",
++ "last_used_at": "2026-10-28T10:05:00Z",
++ "ended_at": null
++ },
++ "end": null,
++ "usage": {
++ "desk_seconds": 296
++ },
++ "idempotency_key": null
++ }
++ ]
++ }
++ },
++ "run.session-create-request": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/session-create-request.v1.json",
++ "title": "RunSessionCreateRequest",
++ "description": "run.session-create-request@1 (the Desks plan's D6): the body of POST /api/v1/sessions on OpenVibe.Run (capability run.session.create), answered 201 with run.session@1 (state placing). The session is the caller's own: a person's token, or a first-party service acting for a person (X-OV-Subject; the service is recorded as via). MACHINE names one of the person's own machines (an OpenVibe.Bot device they own and let agents use): the session goes there or ends unplaceable, never elsewhere. Without it Run places the desk: on the person's own machines when REQUIREMENTS name user-owned (and only theirs), else on desk hosts that serve other people (first-party, then community, unless REQUIREMENTS narrow it), each of whose desks runs in its own sandbox. Absent REQUIREMENTS mean { kind: run.browser, mobility: session, latency_class: interactive, objective: balanced }. TTL_S (default 3600) and IDLE_S (default 900) bound its life. IDEMPOTENCY_KEY: a repeat from the same person within 24 h with an identical body answers the first session (200); with a different body 409 run.idempotency.conflict.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "kind"
++ ],
++ "properties": {
++ "kind": {
++ "const": "browser"
++ },
++ "width": {
++ "$ref": "session.v1.json#/$defs/size/properties/width",
++ "description": "Default 1280."
++ },
++ "height": {
++ "$ref": "session.v1.json#/$defs/size/properties/height",
++ "description": "Default 800."
++ },
++ "ttl_s": {
++ "$ref": "session.v1.json#/properties/ttl_s",
++ "description": "Default 3600."
++ },
++ "idle_s": {
++ "$ref": "session.v1.json#/properties/idle_s",
++ "description": "Default 900."
++ },
++ "machine": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "One of the person's own machines, by its OpenVibe.Bot id; the session goes there or nowhere."
++ },
++ "requirements": {
++ "$ref": "../platform/workload-requirements.v1.json"
++ },
++ "idempotency_key": {
++ "$ref": "session.v1.json#/$defs/idempotency_key"
++ }
++ },
++ "examples": [
++ {
++ "kind": "browser"
++ },
++ {
++ "kind": "browser",
++ "width": 1440,
++ "height": 900,
++ "ttl_s": 1800,
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "idempotency_key": "actor:tsk_01JD2C3D4E5F6G7H8J9K0MNPQR"
++ }
++ ]
++ }
++ },
++ "run.session-control-request": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/session-control-request.v1.json",
++ "title": "RunSessionControlRequest",
++ "description": "run.session-control-request@1 (the Desks plan's D6): one command on a session's desk, the body of POST /api/v1/sessions/{id}/commands on OpenVibe.Run (capability run.session.control), answered with run.session-control-result@1. KIND and VALUE are bot.command@1's for that kind (screen: a picture of the page; input: the mouse, wheel and keys on the page; browser: open, click, fill, press, back, read, screenshot) without desk, which is always the session's own: Run carries the command to OpenVibe.Bot on the session's lease (bot.desk-lease@1) and nothing on the machine other than that desk is reachable. A command on a session that is not running is refused 409 run.session.not_running. ID makes a retry safe: the same id within the session answers the first result.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "kind",
++ "value"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 64,
++ "pattern": "^[A-Za-z0-9._:-]+$"
++ },
++ "kind": {
++ "enum": [
++ "screen",
++ "input",
++ "browser"
++ ]
++ },
++ "value": {
++ "type": "object",
++ "description": "bot.command@1's value for that kind, without desk (refused 422 if it names another desk).",
++ "not": {
++ "required": [
++ "desk"
++ ]
++ }
++ }
++ },
++ "examples": [
++ {
++ "kind": "browser",
++ "value": {
++ "action": "open",
++ "url": "https://example.org/"
++ }
++ },
++ {
++ "id": "c-7",
++ "kind": "screen",
++ "value": {
++ "max_width": 1024
++ }
++ }
++ ]
++ }
++ },
++ "run.session-control-result": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/session-control-result.v1.json",
++ "title": "RunSessionControlResult",
++ "description": "run.session-control-result@1 (the Desks plan's D6): the answer to run.session-control-request@1. RESULT is bot.command-result@1's: ack (the desk did it; DATA is its answer, a page as text and refs, a picture, a receipt), nack (the desk refused; CODE is its fault code and REASON says why), refused (OpenVibe.Bot's gate refused it; CODE is Bot's refusal code) or expired (no answer in time). Every answer also says how the session stands now, so a caller sees it end (idle, ttl, host_lost) without asking again.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "session_id",
++ "id",
++ "result",
++ "state"
++ ],
++ "properties": {
++ "session_id": {
++ "$ref": "session.v1.json#/$defs/session_id"
++ },
++ "id": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 64
++ },
++ "result": {
++ "enum": [
++ "ack",
++ "nack",
++ "refused",
++ "expired"
++ ]
++ },
++ "code": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 1000
++ },
++ "data": {
++ "type": "object",
++ "description": "The desk's answer on an ack (bot.command-result@1 data)."
++ },
++ "state": {
++ "$ref": "session.v1.json#/properties/state"
++ }
++ },
++ "examples": [
++ {
++ "session_id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "id": "c-7",
++ "result": "ack",
++ "code": null,
++ "reason": null,
++ "data": {
++ "format": "jpeg",
++ "width": 1024,
++ "height": 640,
++ "content_b64": "/9j/"
++ },
++ "state": "running"
++ }
++ ]
++ }
++ },
++ "run.session-list-result": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/session-list-result.v1.json",
++ "title": "RunSessionListResult",
++ "description": "run.session-list-result@1 (the Desks plan's D6): GET /api/v1/sessions on OpenVibe.Run (capability run.session.read): the caller's own sessions, newest first, running ones by default (?state=ended or ?state=all for others), at most limit (default 20, at most 100) per page; next_cursor continues the list and is null on the last page.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "sessions",
++ "next_cursor"
++ ],
++ "properties": {
++ "sessions": {
++ "type": "array",
++ "maxItems": 100,
++ "items": {
++ "$ref": "session.v1.json"
++ }
++ },
++ "next_cursor": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 200
++ }
++ }
++ }
++ },
++ "run.session.started": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/run.session.started.v1.json",
++ "title": "RunSessionStartedPayload",
++ "description": "run.session.started v1 (OpenVibe.Run; the Desks plan's D6). A session's desk answered for the first time: it runs on its machine, and desk_seconds count from timings.started_at. The session as run.session@1 has it. Envelope: subject { type: session, id: <session id> }, visibility internal, priority low, actor service:run.",
++ "type": "object",
++ "required": [
++ "id",
++ "owner",
++ "via",
++ "kind",
++ "state",
++ "size",
++ "ttl_s",
++ "idle_s",
++ "placement",
++ "timings",
++ "end",
++ "usage",
++ "idempotency_key"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "$ref": "../../run/session.v1.json#/$defs/session_id"
++ },
++ "owner": {
++ "$ref": "../../identity/subject-ref.v1.json"
++ },
++ "via": {
++ "$ref": "../../run/session.v1.json#/properties/via"
++ },
++ "kind": {
++ "$ref": "../../run/session.v1.json#/properties/kind"
++ },
++ "state": {
++ "const": "running"
++ },
++ "size": {
++ "$ref": "../../run/session.v1.json#/$defs/size"
++ },
++ "ttl_s": {
++ "$ref": "../../run/session.v1.json#/properties/ttl_s"
++ },
++ "idle_s": {
++ "$ref": "../../run/session.v1.json#/properties/idle_s"
++ },
++ "placement": {
++ "$ref": "../../run/session.v1.json#/$defs/placement"
++ },
++ "timings": {
++ "$ref": "../../run/session.v1.json#/$defs/timings"
++ },
++ "end": {
++ "type": "null"
++ },
++ "usage": {
++ "$ref": "../../run/session.v1.json#/$defs/usage"
++ },
++ "idempotency_key": {
++ "$ref": "../../run/session.v1.json#/properties/idempotency_key"
++ }
++ }
++ }
++ },
++ "run.session.ended": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/run.session.ended.v1.json",
++ "title": "RunSessionEndedPayload",
++ "description": "run.session.ended v1 (OpenVibe.Run; the Desks plan's D6). A session ended (end.reason ended, ttl, idle, host_lost, unplaceable or failed), its desk stopped and its lease given back; usage.desk_seconds is final. The session as run.session@1 has it. Envelope: subject { type: session, id: <session id> }, visibility internal, priority low, actor service:run.",
++ "type": "object",
++ "required": [
++ "id",
++ "owner",
++ "via",
++ "kind",
++ "state",
++ "size",
++ "ttl_s",
++ "idle_s",
++ "placement",
++ "timings",
++ "end",
++ "usage",
++ "idempotency_key"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "$ref": "../../run/session.v1.json#/$defs/session_id"
++ },
++ "owner": {
++ "$ref": "../../identity/subject-ref.v1.json"
++ },
++ "via": {
++ "$ref": "../../run/session.v1.json#/properties/via"
++ },
++ "kind": {
++ "$ref": "../../run/session.v1.json#/properties/kind"
++ },
++ "state": {
++ "const": "ended"
++ },
++ "size": {
++ "$ref": "../../run/session.v1.json#/$defs/size"
++ },
++ "ttl_s": {
++ "$ref": "../../run/session.v1.json#/properties/ttl_s"
++ },
++ "idle_s": {
++ "$ref": "../../run/session.v1.json#/properties/idle_s"
++ },
++ "placement": {
++ "anyOf": [
++ {
++ "$ref": "../../run/session.v1.json#/$defs/placement"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "timings": {
++ "$ref": "../../run/session.v1.json#/$defs/timings"
++ },
++ "end": {
++ "$ref": "../../run/session.v1.json#/$defs/end"
++ },
++ "usage": {
++ "$ref": "../../run/session.v1.json#/$defs/usage"
++ },
++ "idempotency_key": {
++ "$ref": "../../run/session.v1.json#/properties/idempotency_key"
++ }
++ }
++ }
++ },
++ "bot.desk-lease": {
++ "version": "1.0.0",
++ "owner": "bot",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/desk-lease.v1.json",
++ "title": "BotDeskLease",
++ "description": "bot.desk-lease@1 (the Desks plan's D6): one desk OpenVibe.Bot holds for one OpenVibe.Run session, as POST /api/v1/machines/{id}/desk-leases answers it (201, capability bot.desk.session, a Run service token acting for the session's person in X-OV-Subject) and GET /api/v1/machines/{id}/desk-leases/{desk} reads it. The request is bot.desk-lease-create-request@1. Bot leases a desk only on the person's own machine where they turned on 'Let my agent use this computer' (shared false), or on a desk host whose owner offers its desks to other people and whose OpenVibe.Node runs each desk in its own sandbox (describe desk.isolation sandbox; shared true). The lease starts the desk (bot.command@1 kind desk, start, kind browser, the lease's size) and from then on only the lease's person, through Run, may act on that desk, with screen, input and browser at POST /api/v1/machines/{id}/desk-leases/{desk}/commands (bot.command@1 without desk, answered by bot.command-result@1): never another desk, never the machine's own screen, files, programs or network, and on a shared host not even its owner's own commands reach a leased desk. DELETE /api/v1/machines/{id}/desk-leases/{desk} stops the desk and ends the lease; so does the machine going away (Run sees the lease gone, 404, and ends the session host_lost).",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "machine_id",
++ "desk",
++ "session",
++ "owner",
++ "kind",
++ "width",
++ "height",
++ "shared",
++ "created_at"
++ ],
++ "properties": {
++ "machine_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "desk": {
++ "type": "string",
++ "pattern": "^desk-[a-z0-9][a-z0-9-]{0,23}$"
++ },
++ "session": {
++ "$ref": "../run/session.v1.json#/$defs/session_id"
++ },
++ "owner": {
++ "type": "string",
++ "pattern": "^user:usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The session's person, the only one whose commands reach the desk."
++ },
++ "kind": {
++ "const": "browser"
++ },
++ "width": {
++ "type": "integer",
++ "minimum": 640,
++ "maximum": 3840
++ },
++ "height": {
++ "type": "integer",
++ "minimum": 480,
++ "maximum": 2160
++ },
++ "shared": {
++ "type": "boolean",
++ "description": "true on a desk host that serves other people (the desk runs in its own sandbox there); false on the person's own machine."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "bot.desk-lease-create-request": {
++ "version": "1.0.0",
++ "owner": "bot",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/bot/desk-lease-create-request.v1.json",
++ "title": "BotDeskLeaseCreateRequest",
++ "description": "bot.desk-lease-create-request@1 (the Desks plan's D6): the body of POST /api/v1/machines/{id}/desk-leases on OpenVibe.Bot (capability bot.desk.session), answered 201 with bot.desk-lease@1. DESK is the name Run chose for the session (unique per session); a desk already leased to another session, or running for the machine's owner, is refused 409 bot.desk_busy, and a repeat for the same session answers its lease (200). A machine with no room (its max_desks) is refused 409 bot.machine_busy, one that is offline 409 bot.device_offline, and one the person may not use 404 bot.device_not_found.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "session",
++ "desk",
++ "kind"
++ ],
++ "properties": {
++ "session": {
++ "$ref": "../run/session.v1.json#/$defs/session_id"
++ },
++ "desk": {
++ "$ref": "desk-lease.v1.json#/properties/desk"
++ },
++ "kind": {
++ "const": "browser"
++ },
++ "width": {
++ "$ref": "desk-lease.v1.json#/properties/width",
++ "description": "Default 1280."
++ },
++ "height": {
++ "$ref": "desk-lease.v1.json#/properties/height",
++ "description": "Default 800."
++ }
++ }
++ }
}
}
}
ddiff --git a/generated/openapi/actor.json b/generated/openapi/actor.json
iindex c79fdf3c..0d0e9a3d 100644
---- a/generated/openapi/actor.json
++++ b/generated/openapi/actor.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Actor API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/ai.json b/generated/openapi/ai.json
iindex 0fc68472..87e9547e 100644
---- a/generated/openapi/ai.json
++++ b/generated/openapi/ai.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.AI API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/billing.json b/generated/openapi/billing.json
iindex 2b35f043..93e8d94b 100644
---- a/generated/openapi/billing.json
++++ b/generated/openapi/billing.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Billing API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/blog.json b/generated/openapi/blog.json
iindex c25a0b71..0e3fed77 100644
---- a/generated/openapi/blog.json
++++ b/generated/openapi/blog.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Blog API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/bot.json b/generated/openapi/bot.json
iindex f27650eb..3503b886 100644
---- a/generated/openapi/bot.json
++++ b/generated/openapi/bot.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Bot API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -472,6 +472,350 @@
}
}
},
++ "/api/v1/machines/{id}/desk-leases": {
++ "post": {
++ "operationId": "post_api_v1_machines_id_desk_leases",
++ "summary": "OpenVibe.Run's sessions on machines (the Desks plan's D6): lease one desk for one session and act on it, with a Run service token naming the session's person in X-OV-Subject.",
++ "description": "**bot.desk.session** (first-party): OpenVibe.Run's sessions on machines (the Desks plan's D6): lease one desk for one session and act on it, with a Run service token naming the session's person in X-OV-Subject. POST /api/v1/machines/:id/desk-leases (bot.desk-lease-create-request@1, answered by bot.desk-lease@1) starts a browser desk on the person's own machine where they let agents use it, or on a desk host whose owner offers its desks to other people and whose Node sandboxes every desk; POST /api/v1/machines/:id/desk-leases/:desk/commands carries screen, input and browser (bot.command@1 without desk) to that desk alone; DELETE /api/v1/machines/:id/desk-leases/:desk stops it. Bot takes it only from Run, never reaches another desk or the machine itself, and on a shared host keeps even its owner off a leased desk.",
++ "tags": [
++ "bot.desk"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.desk.session"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-control",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.desk.session"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.desk-lease-create-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.desk-lease.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/machines/{id}/desk-leases/{desk}": {
++ "delete": {
++ "operationId": "delete_api_v1_machines_id_desk_leases_desk",
++ "summary": "OpenVibe.Run's sessions on machines (the Desks plan's D6): lease one desk for one session and act on it, with a Run service token naming the session's person in X-OV-Subject.",
++ "description": "**bot.desk.session** (first-party): OpenVibe.Run's sessions on machines (the Desks plan's D6): lease one desk for one session and act on it, with a Run service token naming the session's person in X-OV-Subject. POST /api/v1/machines/:id/desk-leases (bot.desk-lease-create-request@1, answered by bot.desk-lease@1) starts a browser desk on the person's own machine where they let agents use it, or on a desk host whose owner offers its desks to other people and whose Node sandboxes every desk; POST /api/v1/machines/:id/desk-leases/:desk/commands carries screen, input and browser (bot.command@1 without desk) to that desk alone; DELETE /api/v1/machines/:id/desk-leases/:desk stops it. Bot takes it only from Run, never reaches another desk or the machine itself, and on a shared host keeps even its owner off a leased desk.",
++ "tags": [
++ "bot.desk"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.desk.session"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-control",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.desk.session"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/bot.desk-lease-create-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "desk",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "session",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "$ref": "#/components/schemas/run.session.v1/$defs/session_id"
++ }
++ },
++ {
++ "name": "kind",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "const": "browser"
++ }
++ },
++ {
++ "name": "width",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "$ref": "#/components/schemas/bot.desk-lease.v1/properties/width",
++ "description": "Default 1280."
++ },
++ "description": "Default 1280."
++ },
++ {
++ "name": "height",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "$ref": "#/components/schemas/bot.desk-lease.v1/properties/height",
++ "description": "Default 800."
++ },
++ "description": "Default 800."
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.desk-lease.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "get": {
++ "operationId": "get_api_v1_machines_id_desk_leases_desk",
++ "summary": "OpenVibe.Run's sessions on machines (the Desks plan's D6): lease one desk for one session and act on it, with a Run service token naming the session's person in X-OV-Subject.",
++ "description": "**bot.desk.session** (first-party): OpenVibe.Run's sessions on machines (the Desks plan's D6): lease one desk for one session and act on it, with a Run service token naming the session's person in X-OV-Subject. POST /api/v1/machines/:id/desk-leases (bot.desk-lease-create-request@1, answered by bot.desk-lease@1) starts a browser desk on the person's own machine where they let agents use it, or on a desk host whose owner offers its desks to other people and whose Node sandboxes every desk; POST /api/v1/machines/:id/desk-leases/:desk/commands carries screen, input and browser (bot.command@1 without desk) to that desk alone; DELETE /api/v1/machines/:id/desk-leases/:desk stops it. Bot takes it only from Run, never reaches another desk or the machine itself, and on a shared host keeps even its owner off a leased desk.",
++ "tags": [
++ "bot.desk"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.desk.session"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-control",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.desk.session"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/bot.desk-lease-create-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "desk",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "session",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "$ref": "#/components/schemas/run.session.v1/$defs/session_id"
++ }
++ },
++ {
++ "name": "kind",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "const": "browser"
++ }
++ },
++ {
++ "name": "width",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "$ref": "#/components/schemas/bot.desk-lease.v1/properties/width",
++ "description": "Default 1280."
++ },
++ "description": "Default 1280."
++ },
++ {
++ "name": "height",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "$ref": "#/components/schemas/bot.desk-lease.v1/properties/height",
++ "description": "Default 800."
++ },
++ "description": "Default 800."
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.desk-lease.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/machines/{id}/desk-leases/{desk}/commands": {
++ "post": {
++ "operationId": "post_api_v1_machines_id_desk_leases_desk_commands",
++ "summary": "OpenVibe.Run's sessions on machines (the Desks plan's D6): lease one desk for one session and act on it, with a Run service token naming the session's person in X-OV-Subject.",
++ "description": "**bot.desk.session** (first-party): OpenVibe.Run's sessions on machines (the Desks plan's D6): lease one desk for one session and act on it, with a Run service token naming the session's person in X-OV-Subject. POST /api/v1/machines/:id/desk-leases (bot.desk-lease-create-request@1, answered by bot.desk-lease@1) starts a browser desk on the person's own machine where they let agents use it, or on a desk host whose owner offers its desks to other people and whose Node sandboxes every desk; POST /api/v1/machines/:id/desk-leases/:desk/commands carries screen, input and browser (bot.command@1 without desk) to that desk alone; DELETE /api/v1/machines/:id/desk-leases/:desk stops it. Bot takes it only from Run, never reaches another desk or the machine itself, and on a shared host keeps even its owner off a leased desk.\n\nRoute note: bot.command@1, answered by bot.command-result@1",
++ "tags": [
++ "bot.desk"
++ ],
++ "x-openvibe-capabilities": [
++ "bot.desk.session"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "bot-control",
++ "security": [
++ {
++ "openvibeToken": [
++ "bot.desk.session"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.desk-lease-create-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "desk",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/bot.desk-lease.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"/api/v1/resources": {
"get": {
"operationId": "get_api_v1_resources",
@@@ -1538,22 +1882,108 @@
}
}
},
-- "default": {
-- "description": "An error (RFC 9457 problem details)",
-- "content": {
-- "application/problem+json": {
-- "schema": {
-- "$ref": "#/components/schemas/errors.problem.v1"
-- }
-- }
-- }
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "components": {
++ "schemas": {
++ "bot.desk-lease-create-request.v1": {
++ "title": "BotDeskLeaseCreateRequest",
++ "description": "bot.desk-lease-create-request@1 (the Desks plan's D6): the body of POST /api/v1/machines/{id}/desk-leases on OpenVibe.Bot (capability bot.desk.session), answered 201 with bot.desk-lease@1. DESK is the name Run chose for the session (unique per session); a desk already leased to another session, or running for the machine's owner, is refused 409 bot.desk_busy, and a repeat for the same session answers its lease (200). A machine with no room (its max_desks) is refused 409 bot.machine_busy, one that is offline 409 bot.device_offline, and one the person may not use 404 bot.device_not_found.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "session",
++ "desk",
++ "kind"
++ ],
++ "properties": {
++ "session": {
++ "$ref": "#/components/schemas/run.session.v1/$defs/session_id"
++ },
++ "desk": {
++ "$ref": "#/components/schemas/bot.desk-lease.v1/properties/desk"
++ },
++ "kind": {
++ "const": "browser"
++ },
++ "width": {
++ "$ref": "#/components/schemas/bot.desk-lease.v1/properties/width",
++ "description": "Default 1280."
++ },
++ "height": {
++ "$ref": "#/components/schemas/bot.desk-lease.v1/properties/height",
++ "description": "Default 800."
++ }
++ }
++ },
++ "bot.desk-lease.v1": {
++ "title": "BotDeskLease",
++ "description": "bot.desk-lease@1 (the Desks plan's D6): one desk OpenVibe.Bot holds for one OpenVibe.Run session, as POST /api/v1/machines/{id}/desk-leases answers it (201, capability bot.desk.session, a Run service token acting for the session's person in X-OV-Subject) and GET /api/v1/machines/{id}/desk-leases/{desk} reads it. The request is bot.desk-lease-create-request@1. Bot leases a desk only on the person's own machine where they turned on 'Let my agent use this computer' (shared false), or on a desk host whose owner offers its desks to other people and whose OpenVibe.Node runs each desk in its own sandbox (describe desk.isolation sandbox; shared true). The lease starts the desk (bot.command@1 kind desk, start, kind browser, the lease's size) and from then on only the lease's person, through Run, may act on that desk, with screen, input and browser at POST /api/v1/machines/{id}/desk-leases/{desk}/commands (bot.command@1 without desk, answered by bot.command-result@1): never another desk, never the machine's own screen, files, programs or network, and on a shared host not even its owner's own commands reach a leased desk. DELETE /api/v1/machines/{id}/desk-leases/{desk} stops the desk and ends the lease; so does the machine going away (Run sees the lease gone, 404, and ends the session host_lost).",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "machine_id",
++ "desk",
++ "session",
++ "owner",
++ "kind",
++ "width",
++ "height",
++ "shared",
++ "created_at"
++ ],
++ "properties": {
++ "machine_id": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "desk": {
++ "type": "string",
++ "pattern": "^desk-[a-z0-9][a-z0-9-]{0,23}$"
++ },
++ "session": {
++ "$ref": "#/components/schemas/run.session.v1/$defs/session_id"
++ },
++ "owner": {
++ "type": "string",
++ "pattern": "^user:usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The session's person, the only one whose commands reach the desk."
++ },
++ "kind": {
++ "const": "browser"
++ },
++ "width": {
++ "type": "integer",
++ "minimum": 640,
++ "maximum": 3840
++ },
++ "height": {
++ "type": "integer",
++ "minimum": 480,
++ "maximum": 2160
++ },
++ "shared": {
++ "type": "boolean",
++ "description": "true on a desk host that serves other people (the desk runs in its own sandbox there); false on the person's own machine."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
}
}
-- }
-- }
-- },
-- "components": {
-- "schemas": {
++ },
"bot.device-connect-result.v1": {
"title": "BotDeviceConnectResult",
"description": "bot.device-connect-result@1: answers of OpenVibe.Bot's device credential routes, which Bot gates with bot.robot.manage (bot.device.connect is reserved, not enforced). POST /api/v1/devices/:id/rotate → { device, credential, publish_key } (both secrets shown once); POST …/devices/:id/revoke → { device } (its socket closes at once).",
@@@ -3112,6 +3542,382 @@
"function",
"gpu"
]
++ },
++ "run.session.v1": {
++ "title": "RunSession",
++ "description": "run.session@1 (the Desks plan's D6): one session on OpenVibe.Run, a browser desk held for one person, as GET /api/v1/sessions/{id} answers it (capability run.session.read) and run.session-list-result@1, POST /api/v1/sessions (run.session.create) and POST /api/v1/sessions/{id}/end (run.session.control) carry it. A session is a desk (bot.command@1 kind desk, kind browser) that Run places on a machine and OpenVibe.Bot leases to it (bot.desk-lease@1): on one of the person's own machines where they let agents use it (trust user-owned, never another person's), or on a desk host that serves other people (trust first-party or community: the host's OpenVibe.Node runs every desk in its own sandbox, desk_isolation sandbox, and offers nothing else of the machine). STATES: placing (stored, looking for a machine) -> starting (the desk is leased and starting) -> running (the desk answered) -> ended. An ended session never changes; end.reason says why: ended (POST /api/v1/sessions/{id}/end), ttl (ttl_s ran out), idle (no command for idle_s), host_lost (its machine went away or stopped the desk), unplaceable (no machine had room before ttl_s, or none at all) or failed (the desk would not start). USAGE: desk_seconds counts from started_at to ended_at (or now while running), metered to OpenVibe.Billing as one platform.usage-sample@1 per minute (service run, operation browser.desk, unit s, idempotency_key run:<session id>:<minute>). Run mints the id (ses_<ULID>).",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "owner",
++ "via",
++ "kind",
++ "state",
++ "size",
++ "ttl_s",
++ "idle_s",
++ "placement",
++ "timings",
++ "end",
++ "usage",
++ "idempotency_key"
++ ],
++ "properties": {
++ "id": {
++ "$ref": "#/components/schemas/run.session.v1/$defs/session_id"
++ },
++ "owner": {
++ "$ref": "#/components/schemas/identity.subject-ref.v1",
++ "description": "The person the session is for: only they (or a first-party service acting for them) read, act on or end it."
++ },
++ "via": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^service:[a-z][a-z0-9-]{0,40}$",
++ "description": "The first-party service that asked for the person (service:actor), or null when they asked themselves."
++ },
++ "kind": {
++ "const": "browser",
++ "description": "A platform.runtime-class@1 name; browser (a headless browser driven over its pipe) is the only kind of session in v1."
++ },
++ "state": {
++ "enum": [
++ "placing",
++ "starting",
++ "running",
++ "ended"
++ ]
++ },
++ "size": {
++ "$ref": "#/components/schemas/run.session.v1/$defs/size"
++ },
++ "ttl_s": {
++ "type": "integer",
++ "minimum": 60,
++ "maximum": 14400,
++ "description": "The most the session lives, from created_at."
++ },
++ "idle_s": {
++ "type": "integer",
++ "minimum": 60,
++ "maximum": 3600,
++ "description": "A session no command used this long ends (end.reason idle)."
++ },
++ "placement": {
++ "anyOf": [
++ {
++ "$ref": "#/components/schemas/run.session.v1/$defs/placement"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "timings": {
++ "$ref": "#/components/schemas/run.session.v1/$defs/timings"
++ },
++ "end": {
++ "anyOf": [
++ {
++ "$ref": "#/components/schemas/run.session.v1/$defs/end"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "usage": {
++ "$ref": "#/components/schemas/run.session.v1/$defs/usage"
++ },
++ "idempotency_key": {
++ "anyOf": [
++ {
++ "$ref": "#/components/schemas/run.session.v1/$defs/idempotency_key"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "required": [
++ "state"
++ ],
++ "properties": {
++ "state": {
++ "const": "ended"
++ }
++ }
++ },
++ "then": {
++ "properties": {
++ "end": {
++ "type": "object"
++ },
++ "timings": {
++ "type": "object",
++ "properties": {
++ "ended_at": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ },
++ "else": {
++ "properties": {
++ "end": {
++ "type": "null"
++ },
++ "timings": {
++ "type": "object",
++ "properties": {
++ "ended_at": {
++ "type": "null"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "required": [
++ "state"
++ ],
++ "properties": {
++ "state": {
++ "enum": [
++ "starting",
++ "running"
++ ]
++ }
++ }
++ },
++ "then": {
++ "properties": {
++ "placement": {
++ "type": "object"
++ }
++ }
++ }
++ }
++ ],
++ "$defs": {
++ "session_id": {
++ "type": "string",
++ "pattern": "^ses_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "size": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "width",
++ "height"
++ ],
++ "properties": {
++ "width": {
++ "type": "integer",
++ "minimum": 640,
++ "maximum": 3840
++ },
++ "height": {
++ "type": "integer",
++ "minimum": 480,
++ "maximum": 2160
++ }
++ },
++ "description": "The desk's page size (bot.command@1 kind desk width and height)."
++ },
++ "placement": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "machine",
++ "desk",
++ "trust",
++ "offer_id",
++ "region",
++ "reasons"
++ ],
++ "properties": {
++ "machine": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine (OpenVibe.Bot device) the desk runs on."
++ },
++ "desk": {
++ "type": "string",
++ "pattern": "^desk-[a-z0-9][a-z0-9-]{0,23}$",
++ "description": "The desk's name there (bot.command@1 desk), leased to this session alone."
++ },
++ "trust": {
++ "enum": [
++ "user-owned",
++ "first-party",
++ "partner",
++ "community"
++ ],
++ "description": "The offer's trust class (platform.resource-offer@1): user-owned is always the owner's own machine."
++ },
++ "offer_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 128,
++ "description": "The platform.resource-offer@1 it was placed on; null when the request named the person's own machine."
++ },
++ "region": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 64
++ },
++ "reasons": {
++ "type": "array",
++ "maxItems": 8,
++ "items": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "description": "Why this machine, in a few words each (the placer's reasons)."
++ }
++ }
++ },
++ "idempotency_key": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 128,
++ "pattern": "^[A-Za-z0-9._:-]+$"
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created_at",
++ "started_at",
++ "last_used_at",
++ "ended_at"
++ ],
++ "properties": {
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "started_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "When the desk first answered (state running)."
++ },
++ "last_used_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "The last command, for idle_s."
++ },
++ "ended_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ },
++ "usage": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "desk_seconds"
++ ],
++ "properties": {
++ "desk_seconds": {
++ "type": "number",
++ "minimum": 0,
++ "description": "Seconds the desk ran (started_at to ended_at, or to now while running); 0 when it never started."
++ }
++ }
++ },
++ "end": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "reason",
++ "detail"
++ ],
++ "properties": {
++ "reason": {
++ "enum": [
++ "ended",
++ "ttl",
++ "idle",
++ "host_lost",
++ "unplaceable",
++ "failed"
++ ]
++ },
++ "detail": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 500
++ }
++ }
++ }
++ },
++ "examples": [
++ {
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": "service:actor",
++ "kind": "browser",
++ "state": "running",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": {
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "desk-s0mnpqr",
++ "trust": "community",
++ "offer_id": "desk:dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "region": "eu",
++ "reasons": [
++ "a shared desk host with room"
++ ]
++ },
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": "2026-10-28T10:00:04Z",
++ "last_used_at": "2026-10-28T10:05:00Z",
++ "ended_at": null
++ },
++ "end": null,
++ "usage": {
++ "desk_seconds": 296
++ },
++ "idempotency_key": null
++ }
++ ]
}
},
"securitySchemes": {
ddiff --git a/generated/openapi/chat.json b/generated/openapi/chat.json
iindex 0aa09719..b3454f5a 100644
---- a/generated/openapi/chat.json
++++ b/generated/openapi/chat.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Chat API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/codes.json b/generated/openapi/codes.json
iindex 32edc016..60fbbca7 100644
---- a/generated/openapi/codes.json
++++ b/generated/openapi/codes.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Codes API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/community.json b/generated/openapi/community.json
iindex 31437f51..57b3be3a 100644
---- a/generated/openapi/community.json
++++ b/generated/openapi/community.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Community API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/coupons.json b/generated/openapi/coupons.json
iindex b120d2d0..175fc3df 100644
---- a/generated/openapi/coupons.json
++++ b/generated/openapi/coupons.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Coupons API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/deals.json b/generated/openapi/deals.json
iindex c7bf6970..6d5596b2 100644
---- a/generated/openapi/deals.json
++++ b/generated/openapi/deals.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Deals API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/events.json b/generated/openapi/events.json
iindex 8fe402f3..e67781c2 100644
---- a/generated/openapi/events.json
++++ b/generated/openapi/events.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Events API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/food.json b/generated/openapi/food.json
iindex 6922debb..22903a79 100644
---- a/generated/openapi/food.json
++++ b/generated/openapi/food.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Food API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/games.json b/generated/openapi/games.json
iindex 33d10676..2580bd13 100644
---- a/generated/openapi/games.json
++++ b/generated/openapi/games.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Games API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/host.json b/generated/openapi/host.json
iindex aec243fa..1451e68a 100644
---- a/generated/openapi/host.json
++++ b/generated/openapi/host.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Host API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/index.json b/generated/openapi/index.json
iindex 90313157..4bcf549a 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -33,8 +33,8 @@
"service": "bot",
"name": "OpenVibe.Bot",
"origin": "https://openvibe.bot",
-- "operations": 21,
-- "capabilities": 8
++ "operations": 25,
++ "capabilities": 9
},
{
"service": "chat",
@@@ -173,8 +173,8 @@
"service": "run",
"name": "OpenVibe.Run",
"origin": "https://openvibe.run",
-- "operations": 7,
-- "capabilities": 6
++ "operations": 12,
++ "capabilities": 9
},
{
"service": "search",
ddiff --git a/generated/openapi/inventory.json b/generated/openapi/inventory.json
iindex 912b6fb8..b7c4e1e0 100644
---- a/generated/openapi/inventory.json
++++ b/generated/openapi/inventory.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Inventory API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/live.json b/generated/openapi/live.json
iindex 570b4458..14fa0eb1 100644
---- a/generated/openapi/live.json
++++ b/generated/openapi/live.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Live API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/media-hub.json b/generated/openapi/media-hub.json
iindex 4f4425db..c4300612 100644
---- a/generated/openapi/media-hub.json
++++ b/generated/openapi/media-hub.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.MediaHub API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/media.json b/generated/openapi/media.json
iindex 64ca86ed..a176efa9 100644
---- a/generated/openapi/media.json
++++ b/generated/openapi/media.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Media API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/network.json b/generated/openapi/network.json
iindex 46b84da7..bb1323bb 100644
---- a/generated/openapi/network.json
++++ b/generated/openapi/network.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Network API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/news.json b/generated/openapi/news.json
iindex 1282b41c..dfc9888c 100644
---- a/generated/openapi/news.json
++++ b/generated/openapi/news.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.News API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/openre.json b/generated/openapi/openre.json
iindex 5b3f40d4..56bcd73d 100644
---- a/generated/openapi/openre.json
++++ b/generated/openapi/openre.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenRestream API",
-- "version": "0.222.0",
-- "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/play.json b/generated/openapi/play.json
iindex 1e4d6634..f58cb038 100644
---- a/generated/openapi/play.json
++++ b/generated/openapi/play.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Play API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/quest.json b/generated/openapi/quest.json
iindex 5e7c9197..8562aa7e 100644
---- a/generated/openapi/quest.json
++++ b/generated/openapi/quest.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Quest API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/reviews.json b/generated/openapi/reviews.json
iindex 32319036..73056a67 100644
---- a/generated/openapi/reviews.json
++++ b/generated/openapi/reviews.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Reviews API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/run.json b/generated/openapi/run.json
iindex 376ede70..a5848d01 100644
---- a/generated/openapi/run.json
++++ b/generated/openapi/run.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Run API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -511,6 +511,294 @@
}
}
}
++ },
++ "/api/v1/sessions": {
++ "get": {
++ "operationId": "get_api_v1_sessions",
++ "summary": "Read your own browser sessions: GET /api/v1/sessions (run.session-list-result@1) and GET /api/v1/sessions/:id (run.session@1: state, where it runs, timings, desk-seconds).",
++ "description": "**run.session.read** (public): Read your own browser sessions: GET /api/v1/sessions (run.session-list-result@1) and GET /api/v1/sessions/:id (run.session@1: state, where it runs, timings, desk-seconds). Never another person's.",
++ "tags": [
++ "run.session"
++ ],
++ "x-openvibe-capabilities": [
++ "run.session.read"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "run-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "run.session.read"
++ ]
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/run.session.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "post": {
++ "operationId": "post_api_v1_sessions",
++ "summary": "Open a browser session for yourself (the Desks plan's D6): POST /api/v1/sessions with run.session-create-request@1.",
++ "description": "**run.session.create** (public): Open a browser session for yourself (the Desks plan's D6): POST /api/v1/sessions with run.session-create-request@1. Run places a browser desk on one of your own machines (when you name it or your requirements name user-owned) or on a desk host that serves other people, where every desk runs in its own sandbox, and answers run.session@1. A first-party service acting for a person (X-OV-Subject) opens it for them and is recorded as via. Desk time is metered in desk-seconds.",
++ "tags": [
++ "run.session"
++ ],
++ "x-openvibe-capabilities": [
++ "run.session.create"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "run-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "run.session.create"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/run.session-create-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/run.session.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/sessions/{id}": {
++ "get": {
++ "operationId": "get_api_v1_sessions_id",
++ "summary": "Read your own browser sessions: GET /api/v1/sessions (run.session-list-result@1) and GET /api/v1/sessions/:id (run.session@1: state, where it runs, timings, desk-seconds).",
++ "description": "**run.session.read** (public): Read your own browser sessions: GET /api/v1/sessions (run.session-list-result@1) and GET /api/v1/sessions/:id (run.session@1: state, where it runs, timings, desk-seconds). Never another person's.",
++ "tags": [
++ "run.session"
++ ],
++ "x-openvibe-capabilities": [
++ "run.session.read"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "run-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "run.session.read"
++ ]
++ }
++ ],
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/run.session.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/sessions/{id}/commands": {
++ "post": {
++ "operationId": "post_api_v1_sessions_id_commands",
++ "summary": "Use and end your own browser session: POST /api/v1/sessions/:id/commands with run.session-control-request@1 (screen, input and browser on the session's desk only, answered by run.session-control-resul",
++ "description": "**run.session.control** (public): Use and end your own browser session: POST /api/v1/sessions/:id/commands with run.session-control-request@1 (screen, input and browser on the session's desk only, answered by run.session-control-result@1) and POST /api/v1/sessions/:id/end (run.session@1, ended). Nothing else on the machine it runs on is reachable.",
++ "tags": [
++ "run.session"
++ ],
++ "x-openvibe-capabilities": [
++ "run.session.control"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "run-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "run.session.control"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/run.session-control-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/run.session-control-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/sessions/{id}/end": {
++ "post": {
++ "operationId": "post_api_v1_sessions_id_end",
++ "summary": "Use and end your own browser session: POST /api/v1/sessions/:id/commands with run.session-control-request@1 (screen, input and browser on the session's desk only, answered by run.session-control-resul",
++ "description": "**run.session.control** (public): Use and end your own browser session: POST /api/v1/sessions/:id/commands with run.session-control-request@1 (screen, input and browser on the session's desk only, answered by run.session-control-result@1) and POST /api/v1/sessions/:id/end (run.session@1, ended). Nothing else on the machine it runs on is reachable.",
++ "tags": [
++ "run.session"
++ ],
++ "x-openvibe-capabilities": [
++ "run.session.control"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "run-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "run.session.control"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/run.session-control-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/run.session-control-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
}
},
"components": {
@@@ -1721,6 +2009,553 @@
"pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
}
}
++ },
++ "run.session-control-request.v1": {
++ "title": "RunSessionControlRequest",
++ "description": "run.session-control-request@1 (the Desks plan's D6): one command on a session's desk, the body of POST /api/v1/sessions/{id}/commands on OpenVibe.Run (capability run.session.control), answered with run.session-control-result@1. KIND and VALUE are bot.command@1's for that kind (screen: a picture of the page; input: the mouse, wheel and keys on the page; browser: open, click, fill, press, back, read, screenshot) without desk, which is always the session's own: Run carries the command to OpenVibe.Bot on the session's lease (bot.desk-lease@1) and nothing on the machine other than that desk is reachable. A command on a session that is not running is refused 409 run.session.not_running. ID makes a retry safe: the same id within the session answers the first result.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "kind",
++ "value"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 64,
++ "pattern": "^[A-Za-z0-9._:-]+$"
++ },
++ "kind": {
++ "enum": [
++ "screen",
++ "input",
++ "browser"
++ ]
++ },
++ "value": {
++ "type": "object",
++ "description": "bot.command@1's value for that kind, without desk (refused 422 if it names another desk).",
++ "not": {
++ "required": [
++ "desk"
++ ]
++ }
++ }
++ },
++ "examples": [
++ {
++ "kind": "browser",
++ "value": {
++ "action": "open",
++ "url": "https://example.org/"
++ }
++ },
++ {
++ "id": "c-7",
++ "kind": "screen",
++ "value": {
++ "max_width": 1024
++ }
++ }
++ ]
++ },
++ "run.session-control-result.v1": {
++ "title": "RunSessionControlResult",
++ "description": "run.session-control-result@1 (the Desks plan's D6): the answer to run.session-control-request@1. RESULT is bot.command-result@1's: ack (the desk did it; DATA is its answer, a page as text and refs, a picture, a receipt), nack (the desk refused; CODE is its fault code and REASON says why), refused (OpenVibe.Bot's gate refused it; CODE is Bot's refusal code) or expired (no answer in time). Every answer also says how the session stands now, so a caller sees it end (idle, ttl, host_lost) without asking again.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "session_id",
++ "id",
++ "result",
++ "state"
++ ],
++ "properties": {
++ "session_id": {
++ "$ref": "#/components/schemas/run.session.v1/$defs/session_id"
++ },
++ "id": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 64
++ },
++ "result": {
++ "enum": [
++ "ack",
++ "nack",
++ "refused",
++ "expired"
++ ]
++ },
++ "code": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 1000
++ },
++ "data": {
++ "type": "object",
++ "description": "The desk's answer on an ack (bot.command-result@1 data)."
++ },
++ "state": {
++ "$ref": "#/components/schemas/run.session.v1/properties/state"
++ }
++ },
++ "examples": [
++ {
++ "session_id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "id": "c-7",
++ "result": "ack",
++ "code": null,
++ "reason": null,
++ "data": {
++ "format": "jpeg",
++ "width": 1024,
++ "height": 640,
++ "content_b64": "/9j/"
++ },
++ "state": "running"
++ }
++ ]
++ },
++ "run.session-create-request.v1": {
++ "title": "RunSessionCreateRequest",
++ "description": "run.session-create-request@1 (the Desks plan's D6): the body of POST /api/v1/sessions on OpenVibe.Run (capability run.session.create), answered 201 with run.session@1 (state placing). The session is the caller's own: a person's token, or a first-party service acting for a person (X-OV-Subject; the service is recorded as via). MACHINE names one of the person's own machines (an OpenVibe.Bot device they own and let agents use): the session goes there or ends unplaceable, never elsewhere. Without it Run places the desk: on the person's own machines when REQUIREMENTS name user-owned (and only theirs), else on desk hosts that serve other people (first-party, then community, unless REQUIREMENTS narrow it), each of whose desks runs in its own sandbox. Absent REQUIREMENTS mean { kind: run.browser, mobility: session, latency_class: interactive, objective: balanced }. TTL_S (default 3600) and IDLE_S (default 900) bound its life. IDEMPOTENCY_KEY: a repeat from the same person within 24 h with an identical body answers the first session (200); with a different body 409 run.idempotency.conflict.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "kind"
++ ],
++ "properties": {
++ "kind": {
++ "const": "browser"
++ },
++ "width": {
++ "$ref": "#/components/schemas/run.session.v1/$defs/size/properties/width",
++ "description": "Default 1280."
++ },
++ "height": {
++ "$ref": "#/components/schemas/run.session.v1/$defs/size/properties/height",
++ "description": "Default 800."
++ },
++ "ttl_s": {
++ "$ref": "#/components/schemas/run.session.v1/properties/ttl_s",
++ "description": "Default 3600."
++ },
++ "idle_s": {
++ "$ref": "#/components/schemas/run.session.v1/properties/idle_s",
++ "description": "Default 900."
++ },
++ "machine": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "One of the person's own machines, by its OpenVibe.Bot id; the session goes there or nowhere."
++ },
++ "requirements": {
++ "$ref": "#/components/schemas/platform.workload-requirements.v1"
++ },
++ "idempotency_key": {
++ "$ref": "#/components/schemas/run.session.v1/$defs/idempotency_key"
++ }
++ },
++ "examples": [
++ {
++ "kind": "browser"
++ },
++ {
++ "kind": "browser",
++ "width": 1440,
++ "height": 900,
++ "ttl_s": 1800,
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "idempotency_key": "actor:tsk_01JD2C3D4E5F6G7H8J9K0MNPQR"
++ }
++ ]
++ },
++ "run.session.v1": {
++ "title": "RunSession",
++ "description": "run.session@1 (the Desks plan's D6): one session on OpenVibe.Run, a browser desk held for one person, as GET /api/v1/sessions/{id} answers it (capability run.session.read) and run.session-list-result@1, POST /api/v1/sessions (run.session.create) and POST /api/v1/sessions/{id}/end (run.session.control) carry it. A session is a desk (bot.command@1 kind desk, kind browser) that Run places on a machine and OpenVibe.Bot leases to it (bot.desk-lease@1): on one of the person's own machines where they let agents use it (trust user-owned, never another person's), or on a desk host that serves other people (trust first-party or community: the host's OpenVibe.Node runs every desk in its own sandbox, desk_isolation sandbox, and offers nothing else of the machine). STATES: placing (stored, looking for a machine) -> starting (the desk is leased and starting) -> running (the desk answered) -> ended. An ended session never changes; end.reason says why: ended (POST /api/v1/sessions/{id}/end), ttl (ttl_s ran out), idle (no command for idle_s), host_lost (its machine went away or stopped the desk), unplaceable (no machine had room before ttl_s, or none at all) or failed (the desk would not start). USAGE: desk_seconds counts from started_at to ended_at (or now while running), metered to OpenVibe.Billing as one platform.usage-sample@1 per minute (service run, operation browser.desk, unit s, idempotency_key run:<session id>:<minute>). Run mints the id (ses_<ULID>).",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "owner",
++ "via",
++ "kind",
++ "state",
++ "size",
++ "ttl_s",
++ "idle_s",
++ "placement",
++ "timings",
++ "end",
++ "usage",
++ "idempotency_key"
++ ],
++ "properties": {
++ "id": {
++ "$ref": "#/components/schemas/run.session.v1/$defs/session_id"
++ },
++ "owner": {
++ "$ref": "#/components/schemas/identity.subject-ref.v1",
++ "description": "The person the session is for: only they (or a first-party service acting for them) read, act on or end it."
++ },
++ "via": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^service:[a-z][a-z0-9-]{0,40}$",
++ "description": "The first-party service that asked for the person (service:actor), or null when they asked themselves."
++ },
++ "kind": {
++ "const": "browser",
++ "description": "A platform.runtime-class@1 name; browser (a headless browser driven over its pipe) is the only kind of session in v1."
++ },
++ "state": {
++ "enum": [
++ "placing",
++ "starting",
++ "running",
++ "ended"
++ ]
++ },
++ "size": {
++ "$ref": "#/components/schemas/run.session.v1/$defs/size"
++ },
++ "ttl_s": {
++ "type": "integer",
++ "minimum": 60,
++ "maximum": 14400,
++ "description": "The most the session lives, from created_at."
++ },
++ "idle_s": {
++ "type": "integer",
++ "minimum": 60,
++ "maximum": 3600,
++ "description": "A session no command used this long ends (end.reason idle)."
++ },
++ "placement": {
++ "anyOf": [
++ {
++ "$ref": "#/components/schemas/run.session.v1/$defs/placement"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "timings": {
++ "$ref": "#/components/schemas/run.session.v1/$defs/timings"
++ },
++ "end": {
++ "anyOf": [
++ {
++ "$ref": "#/components/schemas/run.session.v1/$defs/end"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "usage": {
++ "$ref": "#/components/schemas/run.session.v1/$defs/usage"
++ },
++ "idempotency_key": {
++ "anyOf": [
++ {
++ "$ref": "#/components/schemas/run.session.v1/$defs/idempotency_key"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "required": [
++ "state"
++ ],
++ "properties": {
++ "state": {
++ "const": "ended"
++ }
++ }
++ },
++ "then": {
++ "properties": {
++ "end": {
++ "type": "object"
++ },
++ "timings": {
++ "type": "object",
++ "properties": {
++ "ended_at": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ },
++ "else": {
++ "properties": {
++ "end": {
++ "type": "null"
++ },
++ "timings": {
++ "type": "object",
++ "properties": {
++ "ended_at": {
++ "type": "null"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "required": [
++ "state"
++ ],
++ "properties": {
++ "state": {
++ "enum": [
++ "starting",
++ "running"
++ ]
++ }
++ }
++ },
++ "then": {
++ "properties": {
++ "placement": {
++ "type": "object"
++ }
++ }
++ }
++ }
++ ],
++ "$defs": {
++ "session_id": {
++ "type": "string",
++ "pattern": "^ses_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "size": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "width",
++ "height"
++ ],
++ "properties": {
++ "width": {
++ "type": "integer",
++ "minimum": 640,
++ "maximum": 3840
++ },
++ "height": {
++ "type": "integer",
++ "minimum": 480,
++ "maximum": 2160
++ }
++ },
++ "description": "The desk's page size (bot.command@1 kind desk width and height)."
++ },
++ "placement": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "machine",
++ "desk",
++ "trust",
++ "offer_id",
++ "region",
++ "reasons"
++ ],
++ "properties": {
++ "machine": {
++ "type": "string",
++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The machine (OpenVibe.Bot device) the desk runs on."
++ },
++ "desk": {
++ "type": "string",
++ "pattern": "^desk-[a-z0-9][a-z0-9-]{0,23}$",
++ "description": "The desk's name there (bot.command@1 desk), leased to this session alone."
++ },
++ "trust": {
++ "enum": [
++ "user-owned",
++ "first-party",
++ "partner",
++ "community"
++ ],
++ "description": "The offer's trust class (platform.resource-offer@1): user-owned is always the owner's own machine."
++ },
++ "offer_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 128,
++ "description": "The platform.resource-offer@1 it was placed on; null when the request named the person's own machine."
++ },
++ "region": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 64
++ },
++ "reasons": {
++ "type": "array",
++ "maxItems": 8,
++ "items": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "description": "Why this machine, in a few words each (the placer's reasons)."
++ }
++ }
++ },
++ "idempotency_key": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 128,
++ "pattern": "^[A-Za-z0-9._:-]+$"
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created_at",
++ "started_at",
++ "last_used_at",
++ "ended_at"
++ ],
++ "properties": {
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "started_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "When the desk first answered (state running)."
++ },
++ "last_used_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "The last command, for idle_s."
++ },
++ "ended_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ },
++ "usage": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "desk_seconds"
++ ],
++ "properties": {
++ "desk_seconds": {
++ "type": "number",
++ "minimum": 0,
++ "description": "Seconds the desk ran (started_at to ended_at, or to now while running); 0 when it never started."
++ }
++ }
++ },
++ "end": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "reason",
++ "detail"
++ ],
++ "properties": {
++ "reason": {
++ "enum": [
++ "ended",
++ "ttl",
++ "idle",
++ "host_lost",
++ "unplaceable",
++ "failed"
++ ]
++ },
++ "detail": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 500
++ }
++ }
++ }
++ },
++ "examples": [
++ {
++ "id": "ses_01JD2C3D4E5F6G7H8J9K0MNPQR",
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "via": "service:actor",
++ "kind": "browser",
++ "state": "running",
++ "size": {
++ "width": 1280,
++ "height": 800
++ },
++ "ttl_s": 3600,
++ "idle_s": 900,
++ "placement": {
++ "machine": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "desk": "desk-s0mnpqr",
++ "trust": "community",
++ "offer_id": "desk:dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
++ "region": "eu",
++ "reasons": [
++ "a shared desk host with room"
++ ]
++ },
++ "timings": {
++ "created_at": "2026-10-28T10:00:00Z",
++ "started_at": "2026-10-28T10:00:04Z",
++ "last_used_at": "2026-10-28T10:05:00Z",
++ "ended_at": null
++ },
++ "end": null,
++ "usage": {
++ "desk_seconds": 296
++ },
++ "idempotency_key": null
++ }
++ ]
}
},
"securitySchemes": {
ddiff --git a/generated/openapi/search.json b/generated/openapi/search.json
iindex 84c0308c..1eadd97a 100644
---- a/generated/openapi/search.json
++++ b/generated/openapi/search.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Search API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/services.json b/generated/openapi/services.json
iindex 74016208..6686e97d 100644
---- a/generated/openapi/services.json
++++ b/generated/openapi/services.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Services API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/sources.json b/generated/openapi/sources.json
iindex 2f34c470..5c845cc2 100644
---- a/generated/openapi/sources.json
++++ b/generated/openapi/sources.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Sources API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/space.json b/generated/openapi/space.json
iindex ac967c36..2f8d1663 100644
---- a/generated/openapi/space.json
++++ b/generated/openapi/space.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Space API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tips.json b/generated/openapi/tips.json
iindex 510ffa58..bf11b2b4 100644
---- a/generated/openapi/tips.json
++++ b/generated/openapi/tips.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tips API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tools.json b/generated/openapi/tools.json
iindex bf450c84..0089962d 100644
---- a/generated/openapi/tools.json
++++ b/generated/openapi/tools.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tools API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/trade.json b/generated/openapi/trade.json
iindex 7f2c1e0d..4f316f60 100644
---- a/generated/openapi/trade.json
++++ b/generated/openapi/trade.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Trade API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/vip.json b/generated/openapi/vip.json
iindex 4d43e531..dabb9d9f 100644
---- a/generated/openapi/vip.json
++++ b/generated/openapi/vip.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.VIP API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/watch.json b/generated/openapi/watch.json
iindex 7b9a9d3d..c4f2ec6f 100644
---- a/generated/openapi/watch.json
++++ b/generated/openapi/watch.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Watch API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/wiki.json b/generated/openapi/wiki.json
iindex dae84474..9620455b 100644
---- a/generated/openapi/wiki.json
++++ b/generated/openapi/wiki.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Wiki API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/work.json b/generated/openapi/work.json
iindex 3be7ca5b..d7e5f4b8 100644
---- a/generated/openapi/work.json
++++ b/generated/openapi/work.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Work API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/zone.json b/generated/openapi/zone.json
iindex 26ef195f..84eec30a 100644
---- a/generated/openapi/zone.json
++++ b/generated/openapi/zone.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Zone API",
-- "version": "0.222.0",
-- "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.222.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.223.0",
++ "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.223.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex 6040c03c..d56fff21 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -206118,3 +206118,384 @@ export interface ChatInboxConversationsResult {
message?: {};
replies?: {}[];
}
++
++/** [email protected] (owner: run) */
++/**
++ * run.session@1 (the Desks plan's D6): one session on OpenVibe.Run, a browser desk held for one person, as GET /api/v1/sessions/{id} answers it (capability run.session.read) and run.session-list-result@1, POST /api/v1/sessions (run.session.create) and POST /api/v1/sessions/{id}/end (run.session.control) carry it. A session is a desk (bot.command@1 kind desk, kind browser) that Run places on a machine and OpenVibe.Bot leases to it (bot.desk-lease@1): on one of the person's own machines where they let agents use it (trust user-owned, never another person's), or on a desk host that serves other people (trust first-party or community: the host's OpenVibe.Node runs every desk in its own sandbox, desk_isolation sandbox, and offers nothing else of the machine). STATES: placing (stored, looking for a machine) -> starting (the desk is leased and starting) -> running (the desk answered) -> ended. An ended session never changes; end.reason says why: ended (POST /api/v1/sessions/{id}/end), ttl (ttl_s ran out), idle (no command for idle_s), host_lost (its machine went away or stopped the desk), unplaceable (no machine had room before ttl_s, or none at all) or failed (the desk would not start). USAGE: desk_seconds counts from started_at to ended_at (or now while running), metered to OpenVibe.Billing as one platform.usage-sample@1 per minute (service run, operation browser.desk, unit s, idempotency_key run:<session id>:<minute>). Run mints the id (ses_<ULID>).
++ */
++export type RunSession = {
++ [k: string]: unknown | undefined;
++} & {
++ id: string;
++ owner: SubjectRef;
++ /**
++ * The first-party service that asked for the person (service:actor), or null when they asked themselves.
++ */
++ via: string | null;
++ /**
++ * A platform.runtime-class@1 name; browser (a headless browser driven over its pipe) is the only kind of session in v1.
++ */
++ kind: "browser";
++ state: "placing" | "starting" | "running" | "ended";
++ /**
++ * The desk's page size (bot.command@1 kind desk width and height).
++ */
++ size: {
++ width: number;
++ height: number;
++ };
++ /**
++ * The most the session lives, from created_at.
++ */
++ ttl_s: number;
++ /**
++ * A session no command used this long ends (end.reason idle).
++ */
++ idle_s: number;
++ placement: {
++ /**
++ * The machine (OpenVibe.Bot device) the desk runs on.
++ */
++ machine: string;
++ /**
++ * The desk's name there (bot.command@1 desk), leased to this session alone.
++ */
++ desk: string;
++ /**
++ * The offer's trust class (platform.resource-offer@1): user-owned is always the owner's own machine.
++ */
++ trust: "user-owned" | "first-party" | "partner" | "community";
++ /**
++ * The platform.resource-offer@1 it was placed on; null when the request named the person's own machine.
++ */
++ offer_id: string | null;
++ region: string | null;
++ /**
++ * Why this machine, in a few words each (the placer's reasons).
++ *
++ * @maxItems 8
++ */
++ reasons:
++ | []
++ | [string]
++ | [string, string]
++ | [string, string, string]
++ | [string, string, string, string]
++ | [string, string, string, string, string]
++ | [string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string];
++ } | null;
++ timings: {
++ created_at: string;
++ /**
++ * When the desk first answered (state running).
++ */
++ started_at: string | null;
++ /**
++ * The last command, for idle_s.
++ */
++ last_used_at: string | null;
++ ended_at: string | null;
++ };
++ end: {
++ reason: "ended" | "ttl" | "idle" | "host_lost" | "unplaceable" | "failed";
++ detail: string | null;
++ } | null;
++ usage: {
++ /**
++ * Seconds the desk ran (started_at to ended_at, or to now while running); 0 when it never started.
++ */
++ desk_seconds: number;
++ };
++ idempotency_key: string | null;
++};
++
++/** [email protected] (owner: run) */
++/**
++ * run.session-create-request@1 (the Desks plan's D6): the body of POST /api/v1/sessions on OpenVibe.Run (capability run.session.create), answered 201 with run.session@1 (state placing). The session is the caller's own: a person's token, or a first-party service acting for a person (X-OV-Subject; the service is recorded as via). MACHINE names one of the person's own machines (an OpenVibe.Bot device they own and let agents use): the session goes there or ends unplaceable, never elsewhere. Without it Run places the desk: on the person's own machines when REQUIREMENTS name user-owned (and only theirs), else on desk hosts that serve other people (first-party, then community, unless REQUIREMENTS narrow it), each of whose desks runs in its own sandbox. Absent REQUIREMENTS mean { kind: run.browser, mobility: session, latency_class: interactive, objective: balanced }. TTL_S (default 3600) and IDLE_S (default 900) bound its life. IDEMPOTENCY_KEY: a repeat from the same person within 24 h with an identical body answers the first session (200); with a different body 409 run.idempotency.conflict.
++ */
++export interface RunSessionCreateRequest {
++ kind: "browser";
++ /**
++ * Default 1280.
++ */
++ width?: number;
++ /**
++ * Default 800.
++ */
++ height?: number;
++ /**
++ * Default 3600.
++ */
++ ttl_s?: number;
++ /**
++ * Default 900.
++ */
++ idle_s?: number;
++ /**
++ * One of the person's own machines, by its OpenVibe.Bot id; the session goes there or nowhere.
++ */
++ machine?: string;
++ requirements?: WorkloadRequirements;
++ idempotency_key?: string;
++}
++
++/** [email protected] (owner: run) */
++/**
++ * run.session-control-request@1 (the Desks plan's D6): one command on a session's desk, the body of POST /api/v1/sessions/{id}/commands on OpenVibe.Run (capability run.session.control), answered with run.session-control-result@1. KIND and VALUE are bot.command@1's for that kind (screen: a picture of the page; input: the mouse, wheel and keys on the page; browser: open, click, fill, press, back, read, screenshot) without desk, which is always the session's own: Run carries the command to OpenVibe.Bot on the session's lease (bot.desk-lease@1) and nothing on the machine other than that desk is reachable. A command on a session that is not running is refused 409 run.session.not_running. ID makes a retry safe: the same id within the session answers the first result.
++ */
++export interface RunSessionControlRequest {
++ id?: string;
++ kind: "screen" | "input" | "browser";
++ /**
++ * bot.command@1's value for that kind, without desk (refused 422 if it names another desk).
++ */
++ value: {};
++}
++
++/** [email protected] (owner: run) */
++/**
++ * run.session-control-result@1 (the Desks plan's D6): the answer to run.session-control-request@1. RESULT is bot.command-result@1's: ack (the desk did it; DATA is its answer, a page as text and refs, a picture, a receipt), nack (the desk refused; CODE is its fault code and REASON says why), refused (OpenVibe.Bot's gate refused it; CODE is Bot's refusal code) or expired (no answer in time). Every answer also says how the session stands now, so a caller sees it end (idle, ttl, host_lost) without asking again.
++ */
++export interface RunSessionControlResult {
++ session_id: string;
++ id: string;
++ result: "ack" | "nack" | "refused" | "expired";
++ code?: string | null;
++ reason?: string | null;
++ /**
++ * The desk's answer on an ack (bot.command-result@1 data).
++ */
++ data?: {};
++ state: "placing" | "starting" | "running" | "ended";
++}
++
++/** [email protected] (owner: run) */
++/**
++ * run.session-list-result@1 (the Desks plan's D6): GET /api/v1/sessions on OpenVibe.Run (capability run.session.read): the caller's own sessions, newest first, running ones by default (?state=ended or ?state=all for others), at most limit (default 20, at most 100) per page; next_cursor continues the list and is null on the last page.
++ */
++export interface RunSessionListResult {
++ /**
++ * @maxItems 100
++ */
++ sessions: RunSession[];
++ next_cursor: string | null;
++}
++
++/** [email protected] (owner: run) */
++/**
++ * run.session.started v1 (OpenVibe.Run; the Desks plan's D6). A session's desk answered for the first time: it runs on its machine, and desk_seconds count from timings.started_at. The session as run.session@1 has it. Envelope: subject { type: session, id: <session id> }, visibility internal, priority low, actor service:run.
++ */
++export interface RunSessionStartedPayload {
++ id: string;
++ owner: SubjectRef;
++ /**
++ * The first-party service that asked for the person (service:actor), or null when they asked themselves.
++ */
++ via: string | null;
++ /**
++ * A platform.runtime-class@1 name; browser (a headless browser driven over its pipe) is the only kind of session in v1.
++ */
++ kind: "browser";
++ state: "running";
++ /**
++ * The desk's page size (bot.command@1 kind desk width and height).
++ */
++ size: {
++ width: number;
++ height: number;
++ };
++ /**
++ * The most the session lives, from created_at.
++ */
++ ttl_s: number;
++ /**
++ * A session no command used this long ends (end.reason idle).
++ */
++ idle_s: number;
++ placement: {
++ /**
++ * The machine (OpenVibe.Bot device) the desk runs on.
++ */
++ machine: string;
++ /**
++ * The desk's name there (bot.command@1 desk), leased to this session alone.
++ */
++ desk: string;
++ /**
++ * The offer's trust class (platform.resource-offer@1): user-owned is always the owner's own machine.
++ */
++ trust: "user-owned" | "first-party" | "partner" | "community";
++ /**
++ * The platform.resource-offer@1 it was placed on; null when the request named the person's own machine.
++ */
++ offer_id: string | null;
++ region: string | null;
++ /**
++ * Why this machine, in a few words each (the placer's reasons).
++ *
++ * @maxItems 8
++ */
++ reasons:
++ | []
++ | [string]
++ | [string, string]
++ | [string, string, string]
++ | [string, string, string, string]
++ | [string, string, string, string, string]
++ | [string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string];
++ };
++ timings: {
++ created_at: string;
++ /**
++ * When the desk first answered (state running).
++ */
++ started_at: string | null;
++ /**
++ * The last command, for idle_s.
++ */
++ last_used_at: string | null;
++ ended_at: string | null;
++ };
++ end: null;
++ usage: {
++ /**
++ * Seconds the desk ran (started_at to ended_at, or to now while running); 0 when it never started.
++ */
++ desk_seconds: number;
++ };
++ idempotency_key: string | null;
++}
++
++/** [email protected] (owner: run) */
++/**
++ * run.session.ended v1 (OpenVibe.Run; the Desks plan's D6). A session ended (end.reason ended, ttl, idle, host_lost, unplaceable or failed), its desk stopped and its lease given back; usage.desk_seconds is final. The session as run.session@1 has it. Envelope: subject { type: session, id: <session id> }, visibility internal, priority low, actor service:run.
++ */
++export interface RunSessionEndedPayload {
++ id: string;
++ owner: SubjectRef;
++ /**
++ * The first-party service that asked for the person (service:actor), or null when they asked themselves.
++ */
++ via: string | null;
++ /**
++ * A platform.runtime-class@1 name; browser (a headless browser driven over its pipe) is the only kind of session in v1.
++ */
++ kind: "browser";
++ state: "ended";
++ /**
++ * The desk's page size (bot.command@1 kind desk width and height).
++ */
++ size: {
++ width: number;
++ height: number;
++ };
++ /**
++ * The most the session lives, from created_at.
++ */
++ ttl_s: number;
++ /**
++ * A session no command used this long ends (end.reason idle).
++ */
++ idle_s: number;
++ placement: {
++ /**
++ * The machine (OpenVibe.Bot device) the desk runs on.
++ */
++ machine: string;
++ /**
++ * The desk's name there (bot.command@1 desk), leased to this session alone.
++ */
++ desk: string;
++ /**
++ * The offer's trust class (platform.resource-offer@1): user-owned is always the owner's own machine.
++ */
++ trust: "user-owned" | "first-party" | "partner" | "community";
++ /**
++ * The platform.resource-offer@1 it was placed on; null when the request named the person's own machine.
++ */
++ offer_id: string | null;
++ region: string | null;
++ /**
++ * Why this machine, in a few words each (the placer's reasons).
++ *
++ * @maxItems 8
++ */
++ reasons:
++ | []
++ | [string]
++ | [string, string]
++ | [string, string, string]
++ | [string, string, string, string]
++ | [string, string, string, string, string]
++ | [string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string];
++ } | null;
++ timings: {
++ created_at: string;
++ /**
++ * When the desk first answered (state running).
++ */
++ started_at: string | null;
++ /**
++ * The last command, for idle_s.
++ */
++ last_used_at: string | null;
++ ended_at: string | null;
++ };
++ end: {
++ reason: "ended" | "ttl" | "idle" | "host_lost" | "unplaceable" | "failed";
++ detail: string | null;
++ };
++ usage: {
++ /**
++ * Seconds the desk ran (started_at to ended_at, or to now while running); 0 when it never started.
++ */
++ desk_seconds: number;
++ };
++ idempotency_key: string | null;
++}
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.desk-lease@1 (the Desks plan's D6): one desk OpenVibe.Bot holds for one OpenVibe.Run session, as POST /api/v1/machines/{id}/desk-leases answers it (201, capability bot.desk.session, a Run service token acting for the session's person in X-OV-Subject) and GET /api/v1/machines/{id}/desk-leases/{desk} reads it. The request is bot.desk-lease-create-request@1. Bot leases a desk only on the person's own machine where they turned on 'Let my agent use this computer' (shared false), or on a desk host whose owner offers its desks to other people and whose OpenVibe.Node runs each desk in its own sandbox (describe desk.isolation sandbox; shared true). The lease starts the desk (bot.command@1 kind desk, start, kind browser, the lease's size) and from then on only the lease's person, through Run, may act on that desk, with screen, input and browser at POST /api/v1/machines/{id}/desk-leases/{desk}/commands (bot.command@1 without desk, answered by bot.command-result@1): never another desk, never the machine's own screen, files, programs or network, and on a shared host not even its owner's own commands reach a leased desk. DELETE /api/v1/machines/{id}/desk-leases/{desk} stops the desk and ends the lease; so does the machine going away (Run sees the lease gone, 404, and ends the session host_lost).
++ */
++export interface BotDeskLease {
++ machine_id: string;
++ desk: string;
++ session: string;
++ /**
++ * The session's person, the only one whose commands reach the desk.
++ */
++ owner: string;
++ kind: "browser";
++ width: number;
++ height: number;
++ /**
++ * true on a desk host that serves other people (the desk runs in its own sandbox there); false on the person's own machine.
++ */
++ shared: boolean;
++ created_at: string;
++}
++
++/** [email protected] (owner: bot) */
++/**
++ * bot.desk-lease-create-request@1 (the Desks plan's D6): the body of POST /api/v1/machines/{id}/desk-leases on OpenVibe.Bot (capability bot.desk.session), answered 201 with bot.desk-lease@1. DESK is the name Run chose for the session (unique per session); a desk already leased to another session, or running for the machine's owner, is refused 409 bot.desk_busy, and a repeat for the same session answers its lease (200). A machine with no room (its max_desks) is refused 409 bot.machine_busy, one that is offline 409 bot.device_offline, and one the person may not use 404 bot.device_not_found.
++ */
++export interface BotDeskLeaseCreateRequest {
++ session: string;
++ desk: string;
++ kind: "browser";
++ /**
++ * Default 1280.
++ */
++ width?: number;
++ /**
++ * Default 800.
++ */
++ height?: number;
++}
ddiff --git a/manifests/capabilities/bot.desk.session.json b/manifests/capabilities/bot.desk.session.json
nnew file mode 100644
iindex 00000000..764bf62f
---- /dev/null
++++ b/manifests/capabilities/bot.desk.session.json
@@@ -0,0 +1,27 @@
++{
++ "id": "bot.desk.session",
++ "version": "1.0",
++ "owner": "bot",
++ "status": "active",
++ "visibility": "first-party",
++ "sensitive": true,
++ "description": "OpenVibe.Run's sessions on machines (the Desks plan's D6): lease one desk for one session and act on it, with a Run service token naming the session's person in X-OV-Subject. POST /api/v1/machines/:id/desk-leases (bot.desk-lease-create-request@1, answered by bot.desk-lease@1) starts a browser desk on the person's own machine where they let agents use it, or on a desk host whose owner offers its desks to other people and whose Node sandboxes every desk; POST /api/v1/machines/:id/desk-leases/:desk/commands carries screen, input and browser (bot.command@1 without desk) to that desk alone; DELETE /api/v1/machines/:id/desk-leases/:desk stops it. Bot takes it only from Run, never reaches another desk or the machine itself, and on a shared host keeps even its owner off a leased desk.",
++ "inputSchema": "bot.desk-lease-create-request@1",
++ "outputSchema": "bot.desk-lease@1",
++ "permissions": [
++ "bot:desk:session"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "bot-control",
++ "events": [
++ "bot.command.refused"
++ ],
++ "implementedBy": [
++ "POST /api/v1/machines/:id/desk-leases",
++ "GET /api/v1/machines/:id/desk-leases/:desk",
++ "POST /api/v1/machines/:id/desk-leases/:desk/commands (bot.command@1, answered by bot.command-result@1)",
++ "DELETE /api/v1/machines/:id/desk-leases/:desk"
++ ]
++}
ddiff --git a/manifests/capabilities/run.session.control.json b/manifests/capabilities/run.session.control.json
nnew file mode 100644
iindex 00000000..63b21dbe
---- /dev/null
++++ b/manifests/capabilities/run.session.control.json
@@@ -0,0 +1,24 @@
++{
++ "id": "run.session.control",
++ "version": "1.0",
++ "owner": "run",
++ "status": "active",
++ "visibility": "public",
++ "description": "Use and end your own browser session: POST /api/v1/sessions/:id/commands with run.session-control-request@1 (screen, input and browser on the session's desk only, answered by run.session-control-result@1) and POST /api/v1/sessions/:id/end (run.session@1, ended). Nothing else on the machine it runs on is reachable.",
++ "inputSchema": "run.session-control-request@1",
++ "outputSchema": "run.session-control-result@1",
++ "permissions": [
++ "run:session:control"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "run-write",
++ "events": [
++ "run.session.ended"
++ ],
++ "implementedBy": [
++ "POST /api/v1/sessions/:id/commands",
++ "POST /api/v1/sessions/:id/end"
++ ]
++}
ddiff --git a/manifests/capabilities/run.session.create.json b/manifests/capabilities/run.session.create.json
nnew file mode 100644
iindex 00000000..0ee844dc
---- /dev/null
++++ b/manifests/capabilities/run.session.create.json
@@@ -0,0 +1,24 @@
++{
++ "id": "run.session.create",
++ "version": "1.0",
++ "owner": "run",
++ "status": "active",
++ "visibility": "public",
++ "description": "Open a browser session for yourself (the Desks plan's D6): POST /api/v1/sessions with run.session-create-request@1. Run places a browser desk on one of your own machines (when you name it or your requirements name user-owned) or on a desk host that serves other people, where every desk runs in its own sandbox, and answers run.session@1. A first-party service acting for a person (X-OV-Subject) opens it for them and is recorded as via. Desk time is metered in desk-seconds.",
++ "inputSchema": "run.session-create-request@1",
++ "outputSchema": "run.session@1",
++ "permissions": [
++ "run:session:create"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "run-write",
++ "events": [
++ "run.session.started",
++ "run.session.ended"
++ ],
++ "implementedBy": [
++ "POST /api/v1/sessions"
++ ]
++}
ddiff --git a/manifests/capabilities/run.session.read.json b/manifests/capabilities/run.session.read.json
nnew file mode 100644
iindex 00000000..f429a3ff
---- /dev/null
++++ b/manifests/capabilities/run.session.read.json
@@@ -0,0 +1,22 @@
++{
++ "id": "run.session.read",
++ "version": "1.0",
++ "owner": "run",
++ "status": "active",
++ "visibility": "public",
++ "description": "Read your own browser sessions: GET /api/v1/sessions (run.session-list-result@1) and GET /api/v1/sessions/:id (run.session@1: state, where it runs, timings, desk-seconds). Never another person's.",
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "run.session@1",
++ "permissions": [
++ "run:session:read"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "run-read",
++ "events": [],
++ "implementedBy": [
++ "GET /api/v1/sessions",
++ "GET /api/v1/sessions/:id"
++ ]
++}
ddiff --git a/manifests/services/bot.json b/manifests/services/bot.json
iindex 86017e24..76c3cbf3 100644
---- a/manifests/services/bot.json
++++ b/manifests/services/bot.json
@@@ -35,7 +35,8 @@
"bot.device.connect",
"bot.job.dispatch",
"bot.resource.read",
-- "bot.resource.control"
++ "bot.resource.control",
++ "bot.desk.session"
],
"eventsProduced": [
"bot.robot.online",
ddiff --git a/manifests/services/run.json b/manifests/services/run.json
iindex ca1dff7b..5c915427 100644
---- a/manifests/services/run.json
++++ b/manifests/services/run.json
@@@ -25,7 +25,10 @@
"run.job.list",
"run.job.cancel",
"run.job.stream",
-- "run.job.admin"
++ "run.job.admin",
++ "run.session.create",
++ "run.session.read",
++ "run.session.control"
],
"eventsProduced": [
"run.job.queued",
@@@ -33,7 +36,9 @@
"run.job.succeeded",
"run.job.failed",
"run.job.cancelled",
-- "run.job.expired"
++ "run.job.expired",
++ "run.session.started",
++ "run.session.ended"
],
"eventsConsumed": [],
"namespacesOwned": [],
ddiff --git a/package.json b/package.json
iindex e79d6d69..8a109586 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.222.0",
++ "version": "0.223.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
ddiff --git a/test/bot-manifest.test.js b/test/bot-manifest.test.js
iindex a69b15d0..28dd7bf2 100644
---- a/test/bot-manifest.test.js
++++ b/test/bot-manifest.test.js
@@@ -11,7 +11,8 @@ const c = require('..');
// the computer kinds).
// bot.resource.control: an owner stops or resumes their robot from OpenVibe.Services (0.176.0, ADR-048 step 8 phase 2).
// bot.machine.ai: a model on the person's own machine, for that person (0.219.0, the AI-on-your-machines plan).
--const BOT_CAPABILITIES = ['bot.robot.read', 'bot.robot.manage', 'bot.robot.control', 'bot.computer.control', 'bot.machine.ai', 'bot.device.connect', 'bot.job.dispatch', 'bot.resource.read', 'bot.resource.control'];
++// bot.desk.session: Run's sessions lease one desk each (0.223.0, the Desks plan's D6).
++const BOT_CAPABILITIES = ['bot.robot.read', 'bot.robot.manage', 'bot.robot.control', 'bot.computer.control', 'bot.machine.ai', 'bot.desk.session', 'bot.device.connect', 'bot.job.dispatch', 'bot.resource.read', 'bot.resource.control'];
const BOT_EVENTS = ['bot.robot.online', 'bot.robot.offline', 'bot.estop.set', 'bot.estop.cleared', 'bot.command.refused'];
const bot = c.services.get('bot');
ddiff --git a/test/platform-run.test.js b/test/platform-run.test.js
iindex bf6b0085..4d7609e5 100644
---- a/test/platform-run.test.js
++++ b/test/platform-run.test.js
@@@ -134,4 +134,34 @@ checks++;
const sandboxDispatch = contracts.capabilities.get('sandbox_provider.job.dispatch');
ok(sandboxDispatch && sandboxDispatch.owner === 'run' && sandboxDispatch.status === 'planned' && sandboxDispatch.visibility === 'internal' && sandboxDispatch.sensitive === true && sandboxDispatch.implementedBy.length === 0 && sandboxDispatch.inputSchema === 'sandbox-provider.job-create-request@1' && sandboxDispatch.outputSchema === 'sandbox-provider.job-create-result@1' && !run.capabilities.includes('sandbox_provider.job.dispatch'), 'sandbox_provider.job.dispatch is a planned internal sensitive capability of Run naming the two contracts');
--console.log(`platform run: ${checks} job, frame and metering checks passed`);
++// Run sessions (the Desks plan's D6): a session is a browser desk that Bot leases to it. The ids, the desk names and the
++// size bounds are the same on both sides, a session's desk is always a named desk (never main or private), and its
++// command is bot.command@1's for the kinds that act on a page, never on the machine.
++const session = contracts.schema('run.session');
++const lease = contracts.schema('bot.desk-lease');
++ok(lease.properties.session.$ref === '../run/session.v1.json#/$defs/session_id', 'a lease names its session by run.session@1 id');
++const deskPatterns = [];
++(function walk(node) {
++ if (!node || typeof node !== 'object') return;
++ if (node.desk && typeof node.desk.pattern === 'string' && node.desk.pattern.includes('desk-')) deskPatterns.push(new RegExp(node.desk.pattern));
++ Object.values(node).forEach(walk);
++})(contracts.schema('bot.command'));
++ok(deskPatterns.length > 0, 'bot.command@1 names desks');
++const named = ['desk-s0mnpqr', 'desk-a'];
++ok(named.every((d) => new RegExp(session.$defs.placement.properties.desk.pattern).test(d) && new RegExp(lease.properties.desk.pattern).test(d) && deskPatterns.every((re) => re.test(d))),
++ 'a session desk is a bot.command@1 desk');
++ok(!['main', 'private'].some((d) => new RegExp(lease.properties.desk.pattern).test(d)), 'a session never leases main or private');
++assert.deepStrictEqual([lease.properties.width.minimum, lease.properties.width.maximum, lease.properties.height.minimum, lease.properties.height.maximum],
++ [session.$defs.size.properties.width.minimum, session.$defs.size.properties.width.maximum, session.$defs.size.properties.height.minimum, session.$defs.size.properties.height.maximum]);
++checks++;
++assert.deepStrictEqual(contracts.schema('run.session-control-request').properties.kind.enum, ['screen', 'input', 'browser']);
++checks++;
++ok(['run.session.create', 'run.session.read', 'run.session.control'].every((id) => run.capabilities.includes(id) && contracts.capabilities.get(id).owner === 'run'), 'Run serves the three session capabilities');
++const deskSession = contracts.capabilities.get('bot.desk.session');
++ok(deskSession && deskSession.owner === 'bot' && deskSession.visibility === 'first-party' && deskSession.sensitive === true, 'bot.desk.session is a first-party sensitive capability of Bot');
++// The usage a session meters: one reading per minute of desk time, keyed by session and minute (Billing replays, never 409s).
++const deskReading = { id: 'run:ses_01JD2C3D4E5F6G7H8J9K0MNPQR:0', idempotency_key: 'run:ses_01JD2C3D4E5F6G7H8J9K0MNPQR:0', service: 'run', subject: 'user:usr_01JAB2C3D4E5F6G7H8J9K0MNPQ',
++ resource: 'desk-seconds', node: 'dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X', operation: 'browser.desk', quantity: 60, unit: 's', at: '2026-10-28T10:01:04Z', source: 'openvibe-run.session' };
++ok(contracts.validate('platform.usage-sample', deskReading).valid, 'a minute of desk time is a valid usage reading');
++
++console.log(`platform run: ${checks} job, frame, session and metering checks passed`);