openvibers/openvibe.contracts Commits
Require an artifact for code jobs and give bot.job.dispatch its result contract (0.97.0) (#40)
Changes
@@@ -4,6 +4,22 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) thfrom `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.97.0 — 2026-10-04++++**The job contract matches the Node again, and Bot's dispatch answer has a contract** (plan T14 step 6 follow-up).++- `platform.job@1`: `artifact` is required for class `code` as well as `function` again — the allOf `if` names both++ classes and the `artifact` description says so — and the invalid fixture `code-without-artifact` is back.++ OpenVibe.Node already refuses a code job without an exact-version artifact (its `internal/protocol/protocol.go`++ `JobNoArtifact`), and `run.job-create-request@1` already required it; b88849c had dropped the `code` case from++ this contract alone. A correction to match the implementations, not a new rule.++- `bot.job-dispatch-result@1` (new, owner bot, ADR-034): what OpenVibe.Bot answers OpenVibe.Run's `bot.job.dispatch`++ calls (Bot `server/api/v1.js`, `server/jobs/index.js`). `POST /api/v1/jobs` (201) and `POST /api/v1/jobs/:id/cancel`++ answer `{ job, sent }`; `GET /api/v1/jobs/:id` answers `{ job, stdout }`. `$defs.job` is the `run_jobs` row++ (`state` queued … expired, device, payer project, timing, exit, `usage_read` and the original `platform.job@1`++ body); `$defs.stdout` is the held stdout (the last 1 MiB, or null). `bot.job.dispatch` names it as its++ `outputSchema`, so that capability is complete and its row leaves `compatibility/capability-schema-gaps.json`;++ `generated/openapi/bot.json` now carries the responses. Additive.++## 0.96.0 — 2026-10-04**OpenVibe.Bot's job dispatch for OpenVibe.Run** (plan T14 step 6, R1c): Run hands a job to a paired Node through Bot.@@@ -21,12 +21,6 @@"inputSchema","outputSchema"]-- },-- {-- "id": "bot.job.dispatch",-- "missing": [-- "outputSchema"-- ]}]}nnew file mode 100644@@@ -0,0 +1,251 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/bot/job-dispatch-result.v1.json",++ "title": "BotJobDispatchResult",++ "description": "bot.job-dispatch-result@1: what OpenVibe.Bot answers OpenVibe.Run's bot.job.dispatch calls (server/api/v1.js and server/jobs/index.js). POST /api/v1/jobs is 201 { job, sent }: `job` is Bot's run_jobs record and `sent` says whether the `job` frame went out on the device link now (false while the paired Node is away; it is resent on every reconnect until the Node acks it). POST /api/v1/jobs/:id/cancel is 200 { job, sent } with the same shape: `sent` says whether a `job_cancel` frame went out. GET /api/v1/jobs/:id is 200 { job, stdout }: the record plus the last 1 MiB of the job's captured stdout, or null when the Node sent none. The record's `state` follows the run_jobs lifecycle queued -> placed (the Node acked) -> running (job_started) -> one end state, and its `job` field is the platform.job@1 body Run dispatched, stored and resent verbatim. A refused dispatch answers a problem instead (422 bot.invalid_job, 404 bot.device_not_found, 409 bot.class_unadvertised, 409 bot.job_id_reused), never this contract.",++ "oneOf": [++ { "$ref": "#/$defs/dispatched" },++ { "$ref": "#/$defs/state" }++ ],++ "$defs": {++ "job": {++ "type": "object",++ "additionalProperties": false,++ "description": "One run_jobs row as presentJob renders it: what Run dispatched, to which device, and how far the job and its metering have come.",++ "required": [++ "id",++ "node_id",++ "class",++ "state",++ "project_id",++ "subject",++ "provider",++ "cancel_requested",++ "fault_code",++ "sent_at",++ "started_ms",++ "finished_at",++ "exit_reason",++ "exit_code",++ "wall_ms",++ "usage_read",++ "result",++ "job",++ "created_at",++ "updated_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The platform.job@1 id Run minted and the stem of every usage reading key (run:<id>:<n>)."++ },++ "node_id": {++ "type": "string",++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The paired device the job was sent to; a Node never reads another device's job."++ },++ "class": {++ "$ref": "../platform/runtime-class.v1.json"++ },++ "state": {++ "enum": [++ "queued",++ "placed",++ "running",++ "succeeded",++ "failed",++ "cancelled",++ "expired"++ ],++ "description": "queued (stored; sent and resent until acked), placed (the Node acked it), running (job_started), then one end state from job_exit: succeeded (exited 0), failed (another exit code, or reason limit, stopped or failed, or a nack), cancelled (a cancel before start, or reason cancelled), expired (reason ttl)."++ },++ "project_id": {++ "type": "string",++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The payer project Run named; required on every dispatch (422 without it)."++ },++ "subject": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 128,++ "description": "The subject the readings are for (user:usr_…), as Run sent it; null when Run sent none."++ },++ "provider": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 128,++ "description": "The offer's provider, as Run sent it; null for a first-party node."++ },++ "cancel_requested": {++ "type": "boolean",++ "description": "A cancel was asked for this job; a job not yet started is cancelled at once, a running one ends with its job_exit."++ },++ "fault_code": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 64,++ "description": "The Node's nack fault code, or why Bot stopped sending the job (bot.class_unadvertised); null while it is still sendable."++ },++ "sent_at": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0,++ "description": "First time the `job` frame went out (epoch ms); null while queued and never sent."++ },++ "started_ms": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0,++ "description": "The Node's job_started started_ms: the anchor of every usage second; null until it started."++ },++ "finished_at": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0,++ "description": "When the first job_exit settled the job (epoch ms); null until then, and for a job cancelled before it started that no job_exit ever settled."++ },++ "exit_reason": {++ "type": [++ "string",++ "null"++ ],++ "enum": [++ "exited",++ "cancelled",++ "ttl",++ "limit",++ "stopped",++ "failed",++ null++ ],++ "description": "The first job_exit's reason (platform.job-frame@1); null until it arrived."++ },++ "exit_code": {++ "type": [++ "integer",++ "null"++ ],++ "description": "The process's exit status; null when it was killed or never ran."++ },++ "wall_ms": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0,++ "description": "job_exit.usage.wall_ms as billed, capped at the job's limits.wall_ms; null until job_exit."++ },++ "usage_read": {++ "type": "integer",++ "minimum": 0,++ "description": "Wall-clock seconds of the job already queued for Billing; only moves forward."++ },++ "result": {++ "description": "The job_exit result (any JSON); null unless a job_exit carried one."++ },++ "job": {++ "anyOf": [++ { "$ref": "../platform/job.v1.json" },++ { "type": "null" }++ ],++ "description": "The platform.job@1 body Run dispatched, stored and resent verbatim; present on every stored job."++ },++ "created_at": {++ "type": "integer",++ "minimum": 0,++ "description": "Epoch ms Bot first stored the job."++ },++ "updated_at": {++ "type": "integer",++ "minimum": 0,++ "description": "Epoch ms of the last change to the row."++ }++ }++ },++ "stdout": {++ "type": "object",++ "additionalProperties": false,++ "description": "The last 1 MiB of a job's captured stdout, held in memory by Bot (job_stdout chunks; a chunk_seq already held is dropped, and older chunks fall off once the ring is full).",++ "required": [++ "text",++ "first_seq",++ "last_seq",++ "truncated"++ ],++ "properties": {++ "text": {++ "type": "string",++ "description": "The held chunks joined in order."++ },++ "first_seq": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "The chunk_seq of the first held chunk; null when none is held."++ },++ "last_seq": {++ "type": "integer",++ "minimum": 0,++ "description": "The highest chunk_seq seen for the job; 0 when none."++ },++ "truncated": {++ "type": "boolean",++ "description": "True when an earlier chunk fell off the 1 MiB ring."++ }++ }++ },++ "dispatched": {++ "type": "object",++ "additionalProperties": false,++ "description": "The answer of POST /api/v1/jobs (201) and POST /api/v1/jobs/:id/cancel (200): the row and whether the frame went out on the link now.",++ "required": [++ "job",++ "sent"++ ],++ "properties": {++ "job": {++ "$ref": "#/$defs/job"++ },++ "sent": {++ "type": "boolean",++ "description": "True when the `job` (dispatch) or `job_cancel` (cancel) frame was written to the device's link; false when the device is away (a job is resent on reconnect until acked)."++ }++ }++ },++ "state": {++ "type": "object",++ "additionalProperties": false,++ "description": "The answer of GET /api/v1/jobs/:id (200): the row and the job's held stdout (null when the Node sent none).",++ "required": [++ "job",++ "stdout"++ ],++ "properties": {++ "job": {++ "$ref": "#/$defs/job"++ },++ "stdout": {++ "anyOf": [++ { "$ref": "#/$defs/stdout" },++ { "type": "null" }++ ]++ }++ }++ }++ }++}@@@ -5843,6 +5843,16 @@"status": "active","adr": "ADR-043"},++ {++ "id": "bot.job-dispatch-result",++ "version": "1.0.0",++ "owner": "bot",++ "schema": "bot/job-dispatch-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active",++ "adr": "ADR-034"++ },{"id": "bot.robot-read-request","version": "1.0.0",@@@ -25,7 +25,7 @@"artifact": {"type": "object","additionalProperties": false,-- "description": "The pre-registered artifact to run; required for class `function`.",++ "description": "The pre-registered artifact to run; required for classes `function` and `code`.","required": ["name","version"@@@ -130,7 +130,10 @@],"properties": {"class": {-- "const": "function"++ "enum": [++ "function",++ "code"++ ]}}},nnew file mode 100644@@@ -0,0 +1,38 @@++{++ "job": {++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "node_id": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "class": "function",++ "state": "queued",++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",++ "subject": null,++ "provider": null,++ "cancel_requested": false,++ "fault_code": null,++ "sent_at": 1759500000000,++ "started_ms": null,++ "finished_at": null,++ "exit_reason": null,++ "exit_code": null,++ "wall_ms": null,++ "usage_read": 0,++ "result": null,++ "job": {++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "class": "function",++ "artifact": {++ "name": "thumbnail",++ "version": "1.2.0"++ },++ "args": {},++ "ttl_ms": 60000,++ "limits": {++ "wall_ms": 30000,++ "cpu_ms": 30000,++ "mem_bytes": 268435456++ }++ },++ "created_at": 1759500000000,++ "updated_at": 1759500000000++ }++}nnew file mode 100644@@@ -0,0 +1,39 @@++{++ "job": {++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "node_id": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "class": "function",++ "state": "done",++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",++ "subject": null,++ "provider": null,++ "cancel_requested": false,++ "fault_code": null,++ "sent_at": 1759500000000,++ "started_ms": 1759500001200,++ "finished_at": 1759500004700,++ "exit_reason": "exited",++ "exit_code": 0,++ "wall_ms": 3500,++ "usage_read": 4,++ "result": null,++ "job": {++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "class": "function",++ "artifact": {++ "name": "thumbnail",++ "version": "1.2.0"++ },++ "args": {},++ "ttl_ms": 60000,++ "limits": {++ "wall_ms": 30000,++ "cpu_ms": 30000,++ "mem_bytes": 268435456++ }++ },++ "created_at": 1759500000000,++ "updated_at": 1759500004700++ },++ "sent": true++}nnew file mode 100644@@@ -0,0 +1,42 @@++{++ "job": {++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "node_id": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "class": "function",++ "state": "queued",++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",++ "subject": "user:usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "provider": null,++ "cancel_requested": false,++ "fault_code": null,++ "sent_at": 1759500000000,++ "started_ms": null,++ "finished_at": null,++ "exit_reason": null,++ "exit_code": null,++ "wall_ms": null,++ "usage_read": 0,++ "result": null,++ "job": {++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "class": "function",++ "artifact": {++ "name": "thumbnail",++ "version": "1.2.0"++ },++ "args": {++ "width": 320++ },++ "ttl_ms": 60000,++ "limits": {++ "wall_ms": 30000,++ "cpu_ms": 30000,++ "mem_bytes": 268435456++ },++ "net": "deny"++ },++ "created_at": 1759500000000,++ "updated_at": 1759500000000++ },++ "sent": true++}nnew file mode 100644@@@ -0,0 +1,49 @@++{++ "job": {++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "node_id": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "class": "function",++ "state": "succeeded",++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",++ "subject": "user:usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "provider": null,++ "cancel_requested": false,++ "fault_code": null,++ "sent_at": 1759500000000,++ "started_ms": 1759500001200,++ "finished_at": 1759500004700,++ "exit_reason": "exited",++ "exit_code": 0,++ "wall_ms": 3500,++ "usage_read": 4,++ "result": {++ "width": 320++ },++ "job": {++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "class": "function",++ "artifact": {++ "name": "thumbnail",++ "version": "1.2.0"++ },++ "args": {++ "width": 320++ },++ "ttl_ms": 60000,++ "limits": {++ "wall_ms": 30000,++ "cpu_ms": 30000,++ "mem_bytes": 268435456++ },++ "net": "deny"++ },++ "created_at": 1759500000000,++ "updated_at": 1759500004700++ },++ "stdout": {++ "text": "wrote thumbnail.png\n",++ "first_seq": 1,++ "last_seq": 2,++ "truncated": false++ }++}nnew file mode 100644@@@ -0,0 +1,42 @@++{++ "job": {++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPS",++ "node_id": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",++ "class": "code",++ "state": "cancelled",++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",++ "subject": null,++ "provider": null,++ "cancel_requested": true,++ "fault_code": null,++ "sent_at": 1759500000000,++ "started_ms": null,++ "finished_at": null,++ "exit_reason": null,++ "exit_code": null,++ "wall_ms": null,++ "usage_read": 0,++ "result": null,++ "job": {++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPS",++ "class": "code",++ "artifact": {++ "name": "transcode",++ "version": "0.4.1"++ },++ "args": {++ "format": "webm"++ },++ "ttl_ms": 120000,++ "limits": {++ "wall_ms": 90000,++ "cpu_ms": 90000,++ "mem_bytes": 536870912++ },++ "net": "public"++ },++ "created_at": 1759500000000,++ "updated_at": 1759500000900++ },++ "stdout": null++}nnew file mode 100644@@@ -0,0 +1,22 @@++{++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPR",++ "class": "code",++ "args": {++ "format": "webm"++ },++ "ttl_ms": 120000,++ "limits": {++ "wall_ms": 90000,++ "cpu_ms": 90000,++ "mem_bytes": 536870912++ },++ "net": "public",++ "inputs": [++ {++ "name": "clip.mp4",++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "sha256": "9f86d081884c7d659a2feb15b0b4f8f1c0e6ad1d7e6fd2e1b0b6fd1c4f1d2a3b",++ "size_bytes": 1048576++ }++ ]++}@@@ -63954,7 +63954,7 @@"artifact": {"type": "object","additionalProperties": false,-- "description": "The pre-registered artifact to run; required for class `function`.",++ "description": "The pre-registered artifact to run; required for classes `function` and `code`.","required": ["name","version"@@@ -64059,7 +64059,10 @@],"properties": {"class": {-- "const": "function"++ "enum": [++ "function",++ "code"++ ]}}},@@@ -67832,6 +67835,274 @@}}},++ "bot.job-dispatch-result": {++ "version": "1.0.0",++ "owner": "bot",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/bot/job-dispatch-result.v1.json",++ "title": "BotJobDispatchResult",++ "description": "bot.job-dispatch-result@1: what OpenVibe.Bot answers OpenVibe.Run's bot.job.dispatch calls (server/api/v1.js and server/jobs/index.js). POST /api/v1/jobs is 201 { job, sent }: `job` is Bot's run_jobs record and `sent` says whether the `job` frame went out on the device link now (false while the paired Node is away; it is resent on every reconnect until the Node acks it). POST /api/v1/jobs/:id/cancel is 200 { job, sent } with the same shape: `sent` says whether a `job_cancel` frame went out. GET /api/v1/jobs/:id is 200 { job, stdout }: the record plus the last 1 MiB of the job's captured stdout, or null when the Node sent none. The record's `state` follows the run_jobs lifecycle queued -> placed (the Node acked) -> running (job_started) -> one end state, and its `job` field is the platform.job@1 body Run dispatched, stored and resent verbatim. A refused dispatch answers a problem instead (422 bot.invalid_job, 404 bot.device_not_found, 409 bot.class_unadvertised, 409 bot.job_id_reused), never this contract.",++ "oneOf": [++ {++ "$ref": "#/$defs/dispatched"++ },++ {++ "$ref": "#/$defs/state"++ }++ ],++ "$defs": {++ "job": {++ "type": "object",++ "additionalProperties": false,++ "description": "One run_jobs row as presentJob renders it: what Run dispatched, to which device, and how far the job and its metering have come.",++ "required": [++ "id",++ "node_id",++ "class",++ "state",++ "project_id",++ "subject",++ "provider",++ "cancel_requested",++ "fault_code",++ "sent_at",++ "started_ms",++ "finished_at",++ "exit_reason",++ "exit_code",++ "wall_ms",++ "usage_read",++ "result",++ "job",++ "created_at",++ "updated_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The platform.job@1 id Run minted and the stem of every usage reading key (run:<id>:<n>)."++ },++ "node_id": {++ "type": "string",++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The paired device the job was sent to; a Node never reads another device's job."++ },++ "class": {++ "$ref": "../platform/runtime-class.v1.json"++ },++ "state": {++ "enum": [++ "queued",++ "placed",++ "running",++ "succeeded",++ "failed",++ "cancelled",++ "expired"++ ],++ "description": "queued (stored; sent and resent until acked), placed (the Node acked it), running (job_started), then one end state from job_exit: succeeded (exited 0), failed (another exit code, or reason limit, stopped or failed, or a nack), cancelled (a cancel before start, or reason cancelled), expired (reason ttl)."++ },++ "project_id": {++ "type": "string",++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The payer project Run named; required on every dispatch (422 without it)."++ },++ "subject": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 128,++ "description": "The subject the readings are for (user:usr_…), as Run sent it; null when Run sent none."++ },++ "provider": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 128,++ "description": "The offer's provider, as Run sent it; null for a first-party node."++ },++ "cancel_requested": {++ "type": "boolean",++ "description": "A cancel was asked for this job; a job not yet started is cancelled at once, a running one ends with its job_exit."++ },++ "fault_code": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 64,++ "description": "The Node's nack fault code, or why Bot stopped sending the job (bot.class_unadvertised); null while it is still sendable."++ },++ "sent_at": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0,++ "description": "First time the `job` frame went out (epoch ms); null while queued and never sent."++ },++ "started_ms": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0,++ "description": "The Node's job_started started_ms: the anchor of every usage second; null until it started."++ },++ "finished_at": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0,++ "description": "When the first job_exit settled the job (epoch ms); null until then, and for a job cancelled before it started that no job_exit ever settled."++ },++ "exit_reason": {++ "type": [++ "string",++ "null"++ ],++ "enum": [++ "exited",++ "cancelled",++ "ttl",++ "limit",++ "stopped",++ "failed",++ null++ ],++ "description": "The first job_exit's reason (platform.job-frame@1); null until it arrived."++ },++ "exit_code": {++ "type": [++ "integer",++ "null"++ ],++ "description": "The process's exit status; null when it was killed or never ran."++ },++ "wall_ms": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0,++ "description": "job_exit.usage.wall_ms as billed, capped at the job's limits.wall_ms; null until job_exit."++ },++ "usage_read": {++ "type": "integer",++ "minimum": 0,++ "description": "Wall-clock seconds of the job already queued for Billing; only moves forward."++ },++ "result": {++ "description": "The job_exit result (any JSON); null unless a job_exit carried one."++ },++ "job": {++ "anyOf": [++ {++ "$ref": "../platform/job.v1.json"++ },++ {++ "type": "null"++ }++ ],++ "description": "The platform.job@1 body Run dispatched, stored and resent verbatim; present on every stored job."++ },++ "created_at": {++ "type": "integer",++ "minimum": 0,++ "description": "Epoch ms Bot first stored the job."++ },++ "updated_at": {++ "type": "integer",++ "minimum": 0,++ "description": "Epoch ms of the last change to the row."++ }++ }++ },++ "stdout": {++ "type": "object",++ "additionalProperties": false,++ "description": "The last 1 MiB of a job's captured stdout, held in memory by Bot (job_stdout chunks; a chunk_seq already held is dropped, and older chunks fall off once the ring is full).",++ "required": [++ "text",++ "first_seq",++ "last_seq",++ "truncated"++ ],++ "properties": {++ "text": {++ "type": "string",++ "description": "The held chunks joined in order."++ },++ "first_seq": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "The chunk_seq of the first held chunk; null when none is held."++ },++ "last_seq": {++ "type": "integer",++ "minimum": 0,++ "description": "The highest chunk_seq seen for the job; 0 when none."++ },++ "truncated": {++ "type": "boolean",++ "description": "True when an earlier chunk fell off the 1 MiB ring."++ }++ }++ },++ "dispatched": {++ "type": "object",++ "additionalProperties": false,++ "description": "The answer of POST /api/v1/jobs (201) and POST /api/v1/jobs/:id/cancel (200): the row and whether the frame went out on the link now.",++ "required": [++ "job",++ "sent"++ ],++ "properties": {++ "job": {++ "$ref": "#/$defs/job"++ },++ "sent": {++ "type": "boolean",++ "description": "True when the `job` (dispatch) or `job_cancel` (cancel) frame was written to the device's link; false when the device is away (a job is resent on reconnect until acked)."++ }++ }++ },++ "state": {++ "type": "object",++ "additionalProperties": false,++ "description": "The answer of GET /api/v1/jobs/:id (200): the row and the job's held stdout (null when the Node sent none).",++ "required": [++ "job",++ "stdout"++ ],++ "properties": {++ "job": {++ "$ref": "#/$defs/job"++ },++ "stdout": {++ "anyOf": [++ {++ "$ref": "#/$defs/stdout"++ },++ {++ "type": "null"++ }++ ]++ }++ }++ }++ }++ }++ },"bot.robot-read-request": {"version": "1.0.0","owner": "bot",@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.AI API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Billing API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Blog API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Bot API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -182,7 +182,14 @@},"responses": {"2XX": {-- "description": "Success"++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/bot.job-dispatch-result.v1"++ }++ }++ }},"default": {"description": "An error (RFC 9457 problem details)",@@@ -246,7 +253,7 @@"schema": {"type": "object","additionalProperties": false,-- "description": "The pre-registered artifact to run; required for class `function`.",++ "description": "The pre-registered artifact to run; required for classes `function` and `code`.","required": ["name","version"@@@ -263,7 +270,7 @@}}},-- "description": "The pre-registered artifact to run; required for class `function`."++ "description": "The pre-registered artifact to run; required for classes `function` and `code`."},{"name": "args",@@@ -377,7 +384,14 @@],"responses": {"2XX": {-- "description": "Success"++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/bot.job-dispatch-result.v1"++ }++ }++ }},"default": {"description": "An error (RFC 9457 problem details)",@@@ -436,7 +450,14 @@],"responses": {"2XX": {-- "description": "Success"++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/bot.job-dispatch-result.v1"++ }++ }++ }},"default": {"description": "An error (RFC 9457 problem details)",@@@ -1486,6 +1507,267 @@}}},++ "bot.job-dispatch-result.v1": {++ "title": "BotJobDispatchResult",++ "description": "bot.job-dispatch-result@1: what OpenVibe.Bot answers OpenVibe.Run's bot.job.dispatch calls (server/api/v1.js and server/jobs/index.js). POST /api/v1/jobs is 201 { job, sent }: `job` is Bot's run_jobs record and `sent` says whether the `job` frame went out on the device link now (false while the paired Node is away; it is resent on every reconnect until the Node acks it). POST /api/v1/jobs/:id/cancel is 200 { job, sent } with the same shape: `sent` says whether a `job_cancel` frame went out. GET /api/v1/jobs/:id is 200 { job, stdout }: the record plus the last 1 MiB of the job's captured stdout, or null when the Node sent none. The record's `state` follows the run_jobs lifecycle queued -> placed (the Node acked) -> running (job_started) -> one end state, and its `job` field is the platform.job@1 body Run dispatched, stored and resent verbatim. A refused dispatch answers a problem instead (422 bot.invalid_job, 404 bot.device_not_found, 409 bot.class_unadvertised, 409 bot.job_id_reused), never this contract.",++ "oneOf": [++ {++ "$ref": "#/components/schemas/bot.job-dispatch-result.v1/$defs/dispatched"++ },++ {++ "$ref": "#/components/schemas/bot.job-dispatch-result.v1/$defs/state"++ }++ ],++ "$defs": {++ "job": {++ "type": "object",++ "additionalProperties": false,++ "description": "One run_jobs row as presentJob renders it: what Run dispatched, to which device, and how far the job and its metering have come.",++ "required": [++ "id",++ "node_id",++ "class",++ "state",++ "project_id",++ "subject",++ "provider",++ "cancel_requested",++ "fault_code",++ "sent_at",++ "started_ms",++ "finished_at",++ "exit_reason",++ "exit_code",++ "wall_ms",++ "usage_read",++ "result",++ "job",++ "created_at",++ "updated_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The platform.job@1 id Run minted and the stem of every usage reading key (run:<id>:<n>)."++ },++ "node_id": {++ "type": "string",++ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The paired device the job was sent to; a Node never reads another device's job."++ },++ "class": {++ "$ref": "#/components/schemas/platform.runtime-class.v1"++ },++ "state": {++ "enum": [++ "queued",++ "placed",++ "running",++ "succeeded",++ "failed",++ "cancelled",++ "expired"++ ],++ "description": "queued (stored; sent and resent until acked), placed (the Node acked it), running (job_started), then one end state from job_exit: succeeded (exited 0), failed (another exit code, or reason limit, stopped or failed, or a nack), cancelled (a cancel before start, or reason cancelled), expired (reason ttl)."++ },++ "project_id": {++ "type": "string",++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The payer project Run named; required on every dispatch (422 without it)."++ },++ "subject": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 128,++ "description": "The subject the readings are for (user:usr_…), as Run sent it; null when Run sent none."++ },++ "provider": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 128,++ "description": "The offer's provider, as Run sent it; null for a first-party node."++ },++ "cancel_requested": {++ "type": "boolean",++ "description": "A cancel was asked for this job; a job not yet started is cancelled at once, a running one ends with its job_exit."++ },++ "fault_code": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 64,++ "description": "The Node's nack fault code, or why Bot stopped sending the job (bot.class_unadvertised); null while it is still sendable."++ },++ "sent_at": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0,++ "description": "First time the `job` frame went out (epoch ms); null while queued and never sent."++ },++ "started_ms": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0,++ "description": "The Node's job_started started_ms: the anchor of every usage second; null until it started."++ },++ "finished_at": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0,++ "description": "When the first job_exit settled the job (epoch ms); null until then, and for a job cancelled before it started that no job_exit ever settled."++ },++ "exit_reason": {++ "type": [++ "string",++ "null"++ ],++ "enum": [++ "exited",++ "cancelled",++ "ttl",++ "limit",++ "stopped",++ "failed",++ null++ ],++ "description": "The first job_exit's reason (platform.job-frame@1); null until it arrived."++ },++ "exit_code": {++ "type": [++ "integer",++ "null"++ ],++ "description": "The process's exit status; null when it was killed or never ran."++ },++ "wall_ms": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0,++ "description": "job_exit.usage.wall_ms as billed, capped at the job's limits.wall_ms; null until job_exit."++ },++ "usage_read": {++ "type": "integer",++ "minimum": 0,++ "description": "Wall-clock seconds of the job already queued for Billing; only moves forward."++ },++ "result": {++ "description": "The job_exit result (any JSON); null unless a job_exit carried one."++ },++ "job": {++ "anyOf": [++ {++ "$ref": "#/components/schemas/platform.job.v1"++ },++ {++ "type": "null"++ }++ ],++ "description": "The platform.job@1 body Run dispatched, stored and resent verbatim; present on every stored job."++ },++ "created_at": {++ "type": "integer",++ "minimum": 0,++ "description": "Epoch ms Bot first stored the job."++ },++ "updated_at": {++ "type": "integer",++ "minimum": 0,++ "description": "Epoch ms of the last change to the row."++ }++ }++ },++ "stdout": {++ "type": "object",++ "additionalProperties": false,++ "description": "The last 1 MiB of a job's captured stdout, held in memory by Bot (job_stdout chunks; a chunk_seq already held is dropped, and older chunks fall off once the ring is full).",++ "required": [++ "text",++ "first_seq",++ "last_seq",++ "truncated"++ ],++ "properties": {++ "text": {++ "type": "string",++ "description": "The held chunks joined in order."++ },++ "first_seq": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "The chunk_seq of the first held chunk; null when none is held."++ },++ "last_seq": {++ "type": "integer",++ "minimum": 0,++ "description": "The highest chunk_seq seen for the job; 0 when none."++ },++ "truncated": {++ "type": "boolean",++ "description": "True when an earlier chunk fell off the 1 MiB ring."++ }++ }++ },++ "dispatched": {++ "type": "object",++ "additionalProperties": false,++ "description": "The answer of POST /api/v1/jobs (201) and POST /api/v1/jobs/:id/cancel (200): the row and whether the frame went out on the link now.",++ "required": [++ "job",++ "sent"++ ],++ "properties": {++ "job": {++ "$ref": "#/components/schemas/bot.job-dispatch-result.v1/$defs/job"++ },++ "sent": {++ "type": "boolean",++ "description": "True when the `job` (dispatch) or `job_cancel` (cancel) frame was written to the device's link; false when the device is away (a job is resent on reconnect until acked)."++ }++ }++ },++ "state": {++ "type": "object",++ "additionalProperties": false,++ "description": "The answer of GET /api/v1/jobs/:id (200): the row and the job's held stdout (null when the Node sent none).",++ "required": [++ "job",++ "stdout"++ ],++ "properties": {++ "job": {++ "$ref": "#/components/schemas/bot.job-dispatch-result.v1/$defs/job"++ },++ "stdout": {++ "anyOf": [++ {++ "$ref": "#/components/schemas/bot.job-dispatch-result.v1/$defs/stdout"++ },++ {++ "type": "null"++ }++ ]++ }++ }++ }++ }++ },"bot.robot-control-result.v1": {"title": "BotRobotControlResult","description": "bot.robot-control-result@1: answers of bot.robot.control on OpenVibe.Bot. POST /api/v1/robots/:id/estop → { robot } with its e-stop as it now stands.",@@@ -2072,7 +2354,7 @@"artifact": {"type": "object","additionalProperties": false,-- "description": "The pre-registered artifact to run; required for class `function`.",++ "description": "The pre-registered artifact to run; required for classes `function` and `code`.","required": ["name","version"@@@ -2177,7 +2459,10 @@],"properties": {"class": {-- "const": "function"++ "enum": [++ "function",++ "code"++ ]}}},@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Chat API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Codes API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Community API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Coupons API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Deals API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Events API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Games API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Host API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Live API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Media API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Network API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.News API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenRe.Stream API",-- "version": "0.96.0",-- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Reviews API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Search API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Sources API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Space API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Tips API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Tools API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Trade API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.VIP API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Wiki API",-- "version": "0.96.0",-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.97.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"@@@ -52385,7 +52385,7 @@ export type Job = {id: string;class: RuntimeClass;/**-- * The pre-registered artifact to run; required for class `function`.++ * The pre-registered artifact to run; required for classes `function` and `code`.*/artifact?: {name: string;@@@ -53489,6 +53489,206 @@ export interface BotCommandResult {cached?: boolean;}++/** [email protected] (owner: bot) */++/**++ * bot.job-dispatch-result@1: what OpenVibe.Bot answers OpenVibe.Run's bot.job.dispatch calls (server/api/v1.js and server/jobs/index.js). POST /api/v1/jobs is 201 { job, sent }: `job` is Bot's run_jobs record and `sent` says whether the `job` frame went out on the device link now (false while the paired Node is away; it is resent on every reconnect until the Node acks it). POST /api/v1/jobs/:id/cancel is 200 { job, sent } with the same shape: `sent` says whether a `job_cancel` frame went out. GET /api/v1/jobs/:id is 200 { job, stdout }: the record plus the last 1 MiB of the job's captured stdout, or null when the Node sent none. The record's `state` follows the run_jobs lifecycle queued -> placed (the Node acked) -> running (job_started) -> one end state, and its `job` field is the platform.job@1 body Run dispatched, stored and resent verbatim. A refused dispatch answers a problem instead (422 bot.invalid_job, 404 bot.device_not_found, 409 bot.class_unadvertised, 409 bot.job_id_reused), never this contract.++ */++export type BotJobDispatchResult =++ | {++ /**++ * One run_jobs row as presentJob renders it: what Run dispatched, to which device, and how far the job and its metering have come.++ */++ job: {++ /**++ * The platform.job@1 id Run minted and the stem of every usage reading key (run:<id>:<n>).++ */++ id: string;++ /**++ * The paired device the job was sent to; a Node never reads another device's job.++ */++ node_id: string;++ class: RuntimeClass;++ /**++ * queued (stored; sent and resent until acked), placed (the Node acked it), running (job_started), then one end state from job_exit: succeeded (exited 0), failed (another exit code, or reason limit, stopped or failed, or a nack), cancelled (a cancel before start, or reason cancelled), expired (reason ttl).++ */++ state: "queued" | "placed" | "running" | "succeeded" | "failed" | "cancelled" | "expired";++ /**++ * The payer project Run named; required on every dispatch (422 without it).++ */++ project_id: string;++ /**++ * The subject the readings are for (user:usr_…), as Run sent it; null when Run sent none.++ */++ subject: string | null;++ /**++ * The offer's provider, as Run sent it; null for a first-party node.++ */++ provider: string | null;++ /**++ * A cancel was asked for this job; a job not yet started is cancelled at once, a running one ends with its job_exit.++ */++ cancel_requested: boolean;++ /**++ * The Node's nack fault code, or why Bot stopped sending the job (bot.class_unadvertised); null while it is still sendable.++ */++ fault_code: string | null;++ /**++ * First time the `job` frame went out (epoch ms); null while queued and never sent.++ */++ sent_at: number | null;++ /**++ * The Node's job_started started_ms: the anchor of every usage second; null until it started.++ */++ started_ms: number | null;++ /**++ * When the first job_exit settled the job (epoch ms); null until then, and for a job cancelled before it started that no job_exit ever settled.++ */++ finished_at: number | null;++ /**++ * The first job_exit's reason (platform.job-frame@1); null until it arrived.++ */++ exit_reason: "exited" | "cancelled" | "ttl" | "limit" | "stopped" | "failed" | null;++ /**++ * The process's exit status; null when it was killed or never ran.++ */++ exit_code: number | null;++ /**++ * job_exit.usage.wall_ms as billed, capped at the job's limits.wall_ms; null until job_exit.++ */++ wall_ms: number | null;++ /**++ * Wall-clock seconds of the job already queued for Billing; only moves forward.++ */++ usage_read: number;++ /**++ * The job_exit result (any JSON); null unless a job_exit carried one.++ */++ result: {++ [k: string]: unknown | undefined;++ };++ /**++ * The platform.job@1 body Run dispatched, stored and resent verbatim; present on every stored job.++ */++ job: Job | null;++ /**++ * Epoch ms Bot first stored the job.++ */++ created_at: number;++ /**++ * Epoch ms of the last change to the row.++ */++ updated_at: number;++ };++ /**++ * True when the `job` (dispatch) or `job_cancel` (cancel) frame was written to the device's link; false when the device is away (a job is resent on reconnect until acked).++ */++ sent: boolean;++ }++ | {++ /**++ * One run_jobs row as presentJob renders it: what Run dispatched, to which device, and how far the job and its metering have come.++ */++ job: {++ /**++ * The platform.job@1 id Run minted and the stem of every usage reading key (run:<id>:<n>).++ */++ id: string;++ /**++ * The paired device the job was sent to; a Node never reads another device's job.++ */++ node_id: string;++ class: RuntimeClass;++ /**++ * queued (stored; sent and resent until acked), placed (the Node acked it), running (job_started), then one end state from job_exit: succeeded (exited 0), failed (another exit code, or reason limit, stopped or failed, or a nack), cancelled (a cancel before start, or reason cancelled), expired (reason ttl).++ */++ state: "queued" | "placed" | "running" | "succeeded" | "failed" | "cancelled" | "expired";++ /**++ * The payer project Run named; required on every dispatch (422 without it).++ */++ project_id: string;++ /**++ * The subject the readings are for (user:usr_…), as Run sent it; null when Run sent none.++ */++ subject: string | null;++ /**++ * The offer's provider, as Run sent it; null for a first-party node.++ */++ provider: string | null;++ /**++ * A cancel was asked for this job; a job not yet started is cancelled at once, a running one ends with its job_exit.++ */++ cancel_requested: boolean;++ /**++ * The Node's nack fault code, or why Bot stopped sending the job (bot.class_unadvertised); null while it is still sendable.++ */++ fault_code: string | null;++ /**++ * First time the `job` frame went out (epoch ms); null while queued and never sent.++ */++ sent_at: number | null;++ /**++ * The Node's job_started started_ms: the anchor of every usage second; null until it started.++ */++ started_ms: number | null;++ /**++ * When the first job_exit settled the job (epoch ms); null until then, and for a job cancelled before it started that no job_exit ever settled.++ */++ finished_at: number | null;++ /**++ * The first job_exit's reason (platform.job-frame@1); null until it arrived.++ */++ exit_reason: "exited" | "cancelled" | "ttl" | "limit" | "stopped" | "failed" | null;++ /**++ * The process's exit status; null when it was killed or never ran.++ */++ exit_code: number | null;++ /**++ * job_exit.usage.wall_ms as billed, capped at the job's limits.wall_ms; null until job_exit.++ */++ wall_ms: number | null;++ /**++ * Wall-clock seconds of the job already queued for Billing; only moves forward.++ */++ usage_read: number;++ /**++ * The job_exit result (any JSON); null unless a job_exit carried one.++ */++ result: {++ [k: string]: unknown | undefined;++ };++ /**++ * The platform.job@1 body Run dispatched, stored and resent verbatim; present on every stored job.++ */++ job: Job | null;++ /**++ * Epoch ms Bot first stored the job.++ */++ created_at: number;++ /**++ * Epoch ms of the last change to the row.++ */++ updated_at: number;++ };++ stdout: {++ /**++ * The held chunks joined in order.++ */++ text: string;++ /**++ * The chunk_seq of the first held chunk; null when none is held.++ */++ first_seq: number | null;++ /**++ * The highest chunk_seq seen for the job; 0 when none.++ */++ last_seq: number;++ /**++ * True when an earlier chunk fell off the 1 MiB ring.++ */++ truncated: boolean;++ } | null;++ };++/** [email protected] (owner: bot) *//*** bot.robot-read-request@1: the query of bot.robot.read on OpenVibe.Bot (server/api/v1.js); the routes take no body. owner names whose robots GET /api/v1/robots lists (a service must give it; a person may give only their own subject). limit (1 to 200, default 50) and before (the last audit id seen) page GET …/robots/:id/audit.@@@ -21,5 +21,6 @@"POST /api/v1/jobs/:id/cancel","GET /api/v1/jobs/:id"],-- "inputSchema": "platform.job@1"++ "inputSchema": "platform.job@1",++ "outputSchema": "bot.job-dispatch-result@1"}@@@ -1,12 +1,12 @@{"name": "openvibe-contracts",-- "version": "0.96.0",++ "version": "0.97.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-contracts",-- "version": "0.96.0",++ "version": "0.97.0","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -1,6 +1,6 @@{"name": "openvibe-contracts",-- "version": "0.96.0",++ "version": "0.97.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",
@@ -4,6 +4,22 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
@@ -4,6 +4,22 @@ 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,
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`).
the notes were in the tag and commit messages (`git tag -n1`).
+## 0.97.0 — 2026-10-04
+
+**The job contract matches the Node again, and Bot's dispatch answer has a contract** (plan T14 step 6 follow-up).
+- `platform.job@1`: `artifact` is required for class `code` as well as `function` again — the allOf `if` names both
+ classes and the `artifact` description says so — and the invalid fixture `code-without-artifact` is back.
+ OpenVibe.Node already refuses a code job without an exact-version artifact (its `internal/protocol/protocol.go`
+ `JobNoArtifact`), and `run.job-create-request@1` already required it; b88849c had dropped the `code` case from
+ this contract alone. A correction to match the implementations, not a new rule.
+- `bot.job-dispatch-result@1` (new, owner bot, ADR-034): what OpenVibe.Bot answers OpenVibe.Run's `bot.job.dispatch`
+ calls (Bot `server/api/v1.js`, `server/jobs/index.js`). `POST /api/v1/jobs` (201) and `POST /api/v1/jobs/:id/cancel`
+ answer `{ job, sent }`; `GET /api/v1/jobs/:id` answers `{ job, stdout }`. `$defs.job` is the `run_jobs` row
+ (`state` queued … expired, device, payer project, timing, exit, `usage_read` and the original `platform.job@1`
+ body); `$defs.stdout` is the held stdout (the last 1 MiB, or null). `bot.job.dispatch` names it as its
+ `outputSchema`, so that capability is complete and its row leaves `compatibility/capability-schema-gaps.json`;
+ `generated/openapi/bot.json` now carries the responses. Additive.
+
## 0.96.0 — 2026-10-04
## 0.96.0 — 2026-10-04
**OpenVibe.Bot's job dispatch for OpenVibe.Run** (plan T14 step 6, R1c): Run hands a job to a paired Node through Bot.
**OpenVibe.Bot's job dispatch for OpenVibe.Run** (plan T14 step 6, R1c): Run hands a job to a paired Node through Bot.
@@ -21,12 +21,6 @@
@@ -21,12 +21,6 @@
"inputSchema",
"inputSchema",
"outputSchema"
"outputSchema"
]
]
- },
- {
- "id": "bot.job.dispatch",
- "missing": [
- "outputSchema"
- ]
}
}
]
]
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,251 @@
@@ -0,0 +1,251 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/bot/job-dispatch-result.v1.json",
+ "title": "BotJobDispatchResult",
+ "description": "bot.job-dispatch-result@1: what OpenVibe.Bot answers OpenVibe.Run's bot.job.dispatch calls (server/api/v1.js and server/jobs/index.js). POST /api/v1/jobs is 201 { job, sent }: `job` is Bot's run_jobs record and `sent` says whether the `job` frame went out on the device link now (false while the paired Node is away; it is resent on every reconnect until the Node acks it). POST /api/v1/jobs/:id/cancel is 200 { job, sent } with the same shape: `sent` says whether a `job_cancel` frame went out. GET /api/v1/jobs/:id is 200 { job, stdout }: the record plus the last 1 MiB of the job's captured stdout, or null when the Node sent none. The record's `state` follows the run_jobs lifecycle queued -> placed (the Node acked) -> running (job_started) -> one end state, and its `job` field is the platform.job@1 body Run dispatched, stored and resent verbatim. A refused dispatch answers a problem instead (422 bot.invalid_job, 404 bot.device_not_found, 409 bot.class_unadvertised, 409 bot.job_id_reused), never this contract.",
+ "oneOf": [
+ { "$ref": "#/$defs/dispatched" },
+ { "$ref": "#/$defs/state" }
+ ],
+ "$defs": {
+ "job": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "One run_jobs row as presentJob renders it: what Run dispatched, to which device, and how far the job and its metering have come.",
+ "required": [
+ "id",
+ "node_id",
+ "class",
+ "state",
+ "project_id",
+ "subject",
+ "provider",
+ "cancel_requested",
+ "fault_code",
+ "sent_at",
+ "started_ms",
+ "finished_at",
+ "exit_reason",
+ "exit_code",
+ "wall_ms",
+ "usage_read",
+ "result",
+ "job",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The platform.job@1 id Run minted and the stem of every usage reading key (run:<id>:<n>)."
+ },
+ "node_id": {
+ "type": "string",
+ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The paired device the job was sent to; a Node never reads another device's job."
+ },
+ "class": {
+ "$ref": "../platform/runtime-class.v1.json"
+ },
+ "state": {
+ "enum": [
+ "queued",
+ "placed",
+ "running",
+ "succeeded",
+ "failed",
+ "cancelled",
+ "expired"
+ ],
+ "description": "queued (stored; sent and resent until acked), placed (the Node acked it), running (job_started), then one end state from job_exit: succeeded (exited 0), failed (another exit code, or reason limit, stopped or failed, or a nack), cancelled (a cancel before start, or reason cancelled), expired (reason ttl)."
+ },
+ "project_id": {
+ "type": "string",
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The payer project Run named; required on every dispatch (422 without it)."
+ },
+ "subject": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 128,
+ "description": "The subject the readings are for (user:usr_…), as Run sent it; null when Run sent none."
+ },
+ "provider": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 128,
+ "description": "The offer's provider, as Run sent it; null for a first-party node."
+ },
+ "cancel_requested": {
+ "type": "boolean",
+ "description": "A cancel was asked for this job; a job not yet started is cancelled at once, a running one ends with its job_exit."
+ },
+ "fault_code": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 64,
+ "description": "The Node's nack fault code, or why Bot stopped sending the job (bot.class_unadvertised); null while it is still sendable."
+ },
+ "sent_at": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0,
+ "description": "First time the `job` frame went out (epoch ms); null while queued and never sent."
+ },
+ "started_ms": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0,
+ "description": "The Node's job_started started_ms: the anchor of every usage second; null until it started."
+ },
+ "finished_at": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0,
+ "description": "When the first job_exit settled the job (epoch ms); null until then, and for a job cancelled before it started that no job_exit ever settled."
+ },
+ "exit_reason": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "exited",
+ "cancelled",
+ "ttl",
+ "limit",
+ "stopped",
+ "failed",
+ null
+ ],
+ "description": "The first job_exit's reason (platform.job-frame@1); null until it arrived."
+ },
+ "exit_code": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "The process's exit status; null when it was killed or never ran."
+ },
+ "wall_ms": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0,
+ "description": "job_exit.usage.wall_ms as billed, capped at the job's limits.wall_ms; null until job_exit."
+ },
+ "usage_read": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Wall-clock seconds of the job already queued for Billing; only moves forward."
+ },
+ "result": {
+ "description": "The job_exit result (any JSON); null unless a job_exit carried one."
+ },
+ "job": {
+ "anyOf": [
+ { "$ref": "../platform/job.v1.json" },
+ { "type": "null" }
+ ],
+ "description": "The platform.job@1 body Run dispatched, stored and resent verbatim; present on every stored job."
+ },
+ "created_at": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Epoch ms Bot first stored the job."
+ },
+ "updated_at": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Epoch ms of the last change to the row."
+ }
+ }
+ },
+ "stdout": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "The last 1 MiB of a job's captured stdout, held in memory by Bot (job_stdout chunks; a chunk_seq already held is dropped, and older chunks fall off once the ring is full).",
+ "required": [
+ "text",
+ "first_seq",
+ "last_seq",
+ "truncated"
+ ],
+ "properties": {
+ "text": {
+ "type": "string",
+ "description": "The held chunks joined in order."
+ },
+ "first_seq": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "The chunk_seq of the first held chunk; null when none is held."
+ },
+ "last_seq": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "The highest chunk_seq seen for the job; 0 when none."
+ },
+ "truncated": {
+ "type": "boolean",
+ "description": "True when an earlier chunk fell off the 1 MiB ring."
+ }
+ }
+ },
+ "dispatched": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "The answer of POST /api/v1/jobs (201) and POST /api/v1/jobs/:id/cancel (200): the row and whether the frame went out on the link now.",
+ "required": [
+ "job",
+ "sent"
+ ],
+ "properties": {
+ "job": {
+ "$ref": "#/$defs/job"
+ },
+ "sent": {
+ "type": "boolean",
+ "description": "True when the `job` (dispatch) or `job_cancel` (cancel) frame was written to the device's link; false when the device is away (a job is resent on reconnect until acked)."
+ }
+ }
+ },
+ "state": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "The answer of GET /api/v1/jobs/:id (200): the row and the job's held stdout (null when the Node sent none).",
+ "required": [
+ "job",
+ "stdout"
+ ],
+ "properties": {
+ "job": {
+ "$ref": "#/$defs/job"
+ },
+ "stdout": {
+ "anyOf": [
+ { "$ref": "#/$defs/stdout" },
+ { "type": "null" }
+ ]
+ }
+ }
+ }
+ }
+}
@@ -5843,6 +5843,16 @@
@@ -5843,6 +5843,16 @@
"status": "active",
"status": "active",
"adr": "ADR-043"
"adr": "ADR-043"
},
},
+ {
+ "id": "bot.job-dispatch-result",
+ "version": "1.0.0",
+ "owner": "bot",
+ "schema": "bot/job-dispatch-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active",
+ "adr": "ADR-034"
+ },
{
{
"id": "bot.robot-read-request",
"id": "bot.robot-read-request",
"version": "1.0.0",
"version": "1.0.0",
@@ -25,7 +25,7 @@
@@ -25,7 +25,7 @@
"artifact": {
"artifact": {
"type": "object",
"type": "object",
"additionalProperties": false,
"additionalProperties": false,
- "description": "The pre-registered artifact to run; required for class `function`.",
+ "description": "The pre-registered artifact to run; required for classes `function` and `code`.",
"required": [
"required": [
"name",
"name",
"version"
"version"
@@ -130,7 +130,10 @@
@@ -130,7 +130,10 @@
],
],
"properties": {
"properties": {
"class": {
"class": {
- "const": "function"
+ "enum": [
+ "function",
+ "code"
+ ]
}
}
}
}
},
},
new file mode 100644
new file mode 100644
@@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
+{
+ "job": {
+ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "node_id": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "class": "function",
+ "state": "queued",
+ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
+ "subject": null,
+ "provider": null,
+ "cancel_requested": false,
+ "fault_code": null,
+ "sent_at": 1759500000000,
+ "started_ms": null,
+ "finished_at": null,
+ "exit_reason": null,
+ "exit_code": null,
+ "wall_ms": null,
+ "usage_read": 0,
+ "result": null,
+ "job": {
+ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "class": "function",
+ "artifact": {
+ "name": "thumbnail",
+ "version": "1.2.0"
+ },
+ "args": {},
+ "ttl_ms": 60000,
+ "limits": {
+ "wall_ms": 30000,
+ "cpu_ms": 30000,
+ "mem_bytes": 268435456
+ }
+ },
+ "created_at": 1759500000000,
+ "updated_at": 1759500000000
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
+{
+ "job": {
+ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "node_id": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "class": "function",
+ "state": "done",
+ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
+ "subject": null,
+ "provider": null,
+ "cancel_requested": false,
+ "fault_code": null,
+ "sent_at": 1759500000000,
+ "started_ms": 1759500001200,
+ "finished_at": 1759500004700,
+ "exit_reason": "exited",
+ "exit_code": 0,
+ "wall_ms": 3500,
+ "usage_read": 4,
+ "result": null,
+ "job": {
+ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "class": "function",
+ "artifact": {
+ "name": "thumbnail",
+ "version": "1.2.0"
+ },
+ "args": {},
+ "ttl_ms": 60000,
+ "limits": {
+ "wall_ms": 30000,
+ "cpu_ms": 30000,
+ "mem_bytes": 268435456
+ }
+ },
+ "created_at": 1759500000000,
+ "updated_at": 1759500004700
+ },
+ "sent": true
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
+{
+ "job": {
+ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "node_id": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "class": "function",
+ "state": "queued",
+ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
+ "subject": "user:usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "provider": null,
+ "cancel_requested": false,
+ "fault_code": null,
+ "sent_at": 1759500000000,
+ "started_ms": null,
+ "finished_at": null,
+ "exit_reason": null,
+ "exit_code": null,
+ "wall_ms": null,
+ "usage_read": 0,
+ "result": null,
+ "job": {
+ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "class": "function",
+ "artifact": {
+ "name": "thumbnail",
+ "version": "1.2.0"
+ },
+ "args": {
+ "width": 320
+ },
+ "ttl_ms": 60000,
+ "limits": {
+ "wall_ms": 30000,
+ "cpu_ms": 30000,
+ "mem_bytes": 268435456
+ },
+ "net": "deny"
+ },
+ "created_at": 1759500000000,
+ "updated_at": 1759500000000
+ },
+ "sent": true
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
+{
+ "job": {
+ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "node_id": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "class": "function",
+ "state": "succeeded",
+ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
+ "subject": "user:usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "provider": null,
+ "cancel_requested": false,
+ "fault_code": null,
+ "sent_at": 1759500000000,
+ "started_ms": 1759500001200,
+ "finished_at": 1759500004700,
+ "exit_reason": "exited",
+ "exit_code": 0,
+ "wall_ms": 3500,
+ "usage_read": 4,
+ "result": {
+ "width": 320
+ },
+ "job": {
+ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "class": "function",
+ "artifact": {
+ "name": "thumbnail",
+ "version": "1.2.0"
+ },
+ "args": {
+ "width": 320
+ },
+ "ttl_ms": 60000,
+ "limits": {
+ "wall_ms": 30000,
+ "cpu_ms": 30000,
+ "mem_bytes": 268435456
+ },
+ "net": "deny"
+ },
+ "created_at": 1759500000000,
+ "updated_at": 1759500004700
+ },
+ "stdout": {
+ "text": "wrote thumbnail.png\n",
+ "first_seq": 1,
+ "last_seq": 2,
+ "truncated": false
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
+{
+ "job": {
+ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPS",
+ "node_id": "dev_01J8Z4M2Q0R7T9YV3K6N8P1W2X",
+ "class": "code",
+ "state": "cancelled",
+ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
+ "subject": null,
+ "provider": null,
+ "cancel_requested": true,
+ "fault_code": null,
+ "sent_at": 1759500000000,
+ "started_ms": null,
+ "finished_at": null,
+ "exit_reason": null,
+ "exit_code": null,
+ "wall_ms": null,
+ "usage_read": 0,
+ "result": null,
+ "job": {
+ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPS",
+ "class": "code",
+ "artifact": {
+ "name": "transcode",
+ "version": "0.4.1"
+ },
+ "args": {
+ "format": "webm"
+ },
+ "ttl_ms": 120000,
+ "limits": {
+ "wall_ms": 90000,
+ "cpu_ms": 90000,
+ "mem_bytes": 536870912
+ },
+ "net": "public"
+ },
+ "created_at": 1759500000000,
+ "updated_at": 1759500000900
+ },
+ "stdout": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+{
+ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPR",
+ "class": "code",
+ "args": {
+ "format": "webm"
+ },
+ "ttl_ms": 120000,
+ "limits": {
+ "wall_ms": 90000,
+ "cpu_ms": 90000,
+ "mem_bytes": 536870912
+ },
+ "net": "public",
+ "inputs": [
+ {
+ "name": "clip.mp4",
+ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "sha256": "9f86d081884c7d659a2feb15b0b4f8f1c0e6ad1d7e6fd2e1b0b6fd1c4f1d2a3b",
+ "size_bytes": 1048576
+ }
+ ]
+}
@@ -63954,7 +63954,7 @@
@@ -63954,7 +63954,7 @@
"artifact": {
"artifact": {
"type": "object",
"type": "object",
"additionalProperties": false,
"additionalProperties": false,
- "description": "The pre-registered artifact to run; required for class `function`.",
+ "description": "The pre-registered artifact to run; required for classes `function` and `code`.",
"required": [
"required": [
"name",
"name",
"version"
"version"
@@ -64059,7 +64059,10 @@
@@ -64059,7 +64059,10 @@
],
],
"properties": {
"properties": {
"class": {
"class": {
- "const": "function"
+ "enum": [
+ "function",
+ "code"
+ ]
}
}
}
}
},
},
@@ -67832,6 +67835,274 @@
@@ -67832,6 +67835,274 @@
}
}
}
}
},
},
+ "bot.job-dispatch-result": {
+ "version": "1.0.0",
+ "owner": "bot",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/bot/job-dispatch-result.v1.json",
+ "title": "BotJobDispatchResult",
+ "description": "bot.job-dispatch-result@1: what OpenVibe.Bot answers OpenVibe.Run's bot.job.dispatch calls (server/api/v1.js and server/jobs/index.js). POST /api/v1/jobs is 201 { job, sent }: `job` is Bot's run_jobs record and `sent` says whether the `job` frame went out on the device link now (false while the paired Node is away; it is resent on every reconnect until the Node acks it). POST /api/v1/jobs/:id/cancel is 200 { job, sent } with the same shape: `sent` says whether a `job_cancel` frame went out. GET /api/v1/jobs/:id is 200 { job, stdout }: the record plus the last 1 MiB of the job's captured stdout, or null when the Node sent none. The record's `state` follows the run_jobs lifecycle queued -> placed (the Node acked) -> running (job_started) -> one end state, and its `job` field is the platform.job@1 body Run dispatched, stored and resent verbatim. A refused dispatch answers a problem instead (422 bot.invalid_job, 404 bot.device_not_found, 409 bot.class_unadvertised, 409 bot.job_id_reused), never this contract.",
+ "oneOf": [
+ {
+ "$ref": "#/$defs/dispatched"
+ },
+ {
+ "$ref": "#/$defs/state"
+ }
+ ],
+ "$defs": {
+ "job": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "One run_jobs row as presentJob renders it: what Run dispatched, to which device, and how far the job and its metering have come.",
+ "required": [
+ "id",
+ "node_id",
+ "class",
+ "state",
+ "project_id",
+ "subject",
+ "provider",
+ "cancel_requested",
+ "fault_code",
+ "sent_at",
+ "started_ms",
+ "finished_at",
+ "exit_reason",
+ "exit_code",
+ "wall_ms",
+ "usage_read",
+ "result",
+ "job",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The platform.job@1 id Run minted and the stem of every usage reading key (run:<id>:<n>)."
+ },
+ "node_id": {
+ "type": "string",
+ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The paired device the job was sent to; a Node never reads another device's job."
+ },
+ "class": {
+ "$ref": "../platform/runtime-class.v1.json"
+ },
+ "state": {
+ "enum": [
+ "queued",
+ "placed",
+ "running",
+ "succeeded",
+ "failed",
+ "cancelled",
+ "expired"
+ ],
+ "description": "queued (stored; sent and resent until acked), placed (the Node acked it), running (job_started), then one end state from job_exit: succeeded (exited 0), failed (another exit code, or reason limit, stopped or failed, or a nack), cancelled (a cancel before start, or reason cancelled), expired (reason ttl)."
+ },
+ "project_id": {
+ "type": "string",
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The payer project Run named; required on every dispatch (422 without it)."
+ },
+ "subject": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 128,
+ "description": "The subject the readings are for (user:usr_…), as Run sent it; null when Run sent none."
+ },
+ "provider": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 128,
+ "description": "The offer's provider, as Run sent it; null for a first-party node."
+ },
+ "cancel_requested": {
+ "type": "boolean",
+ "description": "A cancel was asked for this job; a job not yet started is cancelled at once, a running one ends with its job_exit."
+ },
+ "fault_code": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 64,
+ "description": "The Node's nack fault code, or why Bot stopped sending the job (bot.class_unadvertised); null while it is still sendable."
+ },
+ "sent_at": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0,
+ "description": "First time the `job` frame went out (epoch ms); null while queued and never sent."
+ },
+ "started_ms": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0,
+ "description": "The Node's job_started started_ms: the anchor of every usage second; null until it started."
+ },
+ "finished_at": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0,
+ "description": "When the first job_exit settled the job (epoch ms); null until then, and for a job cancelled before it started that no job_exit ever settled."
+ },
+ "exit_reason": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "exited",
+ "cancelled",
+ "ttl",
+ "limit",
+ "stopped",
+ "failed",
+ null
+ ],
+ "description": "The first job_exit's reason (platform.job-frame@1); null until it arrived."
+ },
+ "exit_code": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "The process's exit status; null when it was killed or never ran."
+ },
+ "wall_ms": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0,
+ "description": "job_exit.usage.wall_ms as billed, capped at the job's limits.wall_ms; null until job_exit."
+ },
+ "usage_read": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Wall-clock seconds of the job already queued for Billing; only moves forward."
+ },
+ "result": {
+ "description": "The job_exit result (any JSON); null unless a job_exit carried one."
+ },
+ "job": {
+ "anyOf": [
+ {
+ "$ref": "../platform/job.v1.json"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The platform.job@1 body Run dispatched, stored and resent verbatim; present on every stored job."
+ },
+ "created_at": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Epoch ms Bot first stored the job."
+ },
+ "updated_at": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Epoch ms of the last change to the row."
+ }
+ }
+ },
+ "stdout": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "The last 1 MiB of a job's captured stdout, held in memory by Bot (job_stdout chunks; a chunk_seq already held is dropped, and older chunks fall off once the ring is full).",
+ "required": [
+ "text",
+ "first_seq",
+ "last_seq",
+ "truncated"
+ ],
+ "properties": {
+ "text": {
+ "type": "string",
+ "description": "The held chunks joined in order."
+ },
+ "first_seq": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "The chunk_seq of the first held chunk; null when none is held."
+ },
+ "last_seq": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "The highest chunk_seq seen for the job; 0 when none."
+ },
+ "truncated": {
+ "type": "boolean",
+ "description": "True when an earlier chunk fell off the 1 MiB ring."
+ }
+ }
+ },
+ "dispatched": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "The answer of POST /api/v1/jobs (201) and POST /api/v1/jobs/:id/cancel (200): the row and whether the frame went out on the link now.",
+ "required": [
+ "job",
+ "sent"
+ ],
+ "properties": {
+ "job": {
+ "$ref": "#/$defs/job"
+ },
+ "sent": {
+ "type": "boolean",
+ "description": "True when the `job` (dispatch) or `job_cancel` (cancel) frame was written to the device's link; false when the device is away (a job is resent on reconnect until acked)."
+ }
+ }
+ },
+ "state": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "The answer of GET /api/v1/jobs/:id (200): the row and the job's held stdout (null when the Node sent none).",
+ "required": [
+ "job",
+ "stdout"
+ ],
+ "properties": {
+ "job": {
+ "$ref": "#/$defs/job"
+ },
+ "stdout": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/stdout"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
"bot.robot-read-request": {
"bot.robot-read-request": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "bot",
"owner": "bot",
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.AI API",
"title": "OpenVibe.AI API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Billing API",
"title": "OpenVibe.Billing API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Blog API",
"title": "OpenVibe.Blog API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Bot API",
"title": "OpenVibe.Bot API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -182,7 +182,14 @@
@@ -182,7 +182,14 @@
},
},
"responses": {
"responses": {
"2XX": {
"2XX": {
- "description": "Success"
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/bot.job-dispatch-result.v1"
+ }
+ }
+ }
},
},
"default": {
"default": {
"description": "An error (RFC 9457 problem details)",
"description": "An error (RFC 9457 problem details)",
@@ -246,7 +253,7 @@
@@ -246,7 +253,7 @@
"schema": {
"schema": {
"type": "object",
"type": "object",
"additionalProperties": false,
"additionalProperties": false,
- "description": "The pre-registered artifact to run; required for class `function`.",
+ "description": "The pre-registered artifact to run; required for classes `function` and `code`.",
"required": [
"required": [
"name",
"name",
"version"
"version"
@@ -263,7 +270,7 @@
@@ -263,7 +270,7 @@
}
}
}
}
},
},
- "description": "The pre-registered artifact to run; required for class `function`."
+ "description": "The pre-registered artifact to run; required for classes `function` and `code`."
},
},
{
{
"name": "args",
"name": "args",
@@ -377,7 +384,14 @@
@@ -377,7 +384,14 @@
],
],
"responses": {
"responses": {
"2XX": {
"2XX": {
- "description": "Success"
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/bot.job-dispatch-result.v1"
+ }
+ }
+ }
},
},
"default": {
"default": {
"description": "An error (RFC 9457 problem details)",
"description": "An error (RFC 9457 problem details)",
@@ -436,7 +450,14 @@
@@ -436,7 +450,14 @@
],
],
"responses": {
"responses": {
"2XX": {
"2XX": {
- "description": "Success"
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/bot.job-dispatch-result.v1"
+ }
+ }
+ }
},
},
"default": {
"default": {
"description": "An error (RFC 9457 problem details)",
"description": "An error (RFC 9457 problem details)",
@@ -1486,6 +1507,267 @@
@@ -1486,6 +1507,267 @@
}
}
}
}
},
},
+ "bot.job-dispatch-result.v1": {
+ "title": "BotJobDispatchResult",
+ "description": "bot.job-dispatch-result@1: what OpenVibe.Bot answers OpenVibe.Run's bot.job.dispatch calls (server/api/v1.js and server/jobs/index.js). POST /api/v1/jobs is 201 { job, sent }: `job` is Bot's run_jobs record and `sent` says whether the `job` frame went out on the device link now (false while the paired Node is away; it is resent on every reconnect until the Node acks it). POST /api/v1/jobs/:id/cancel is 200 { job, sent } with the same shape: `sent` says whether a `job_cancel` frame went out. GET /api/v1/jobs/:id is 200 { job, stdout }: the record plus the last 1 MiB of the job's captured stdout, or null when the Node sent none. The record's `state` follows the run_jobs lifecycle queued -> placed (the Node acked) -> running (job_started) -> one end state, and its `job` field is the platform.job@1 body Run dispatched, stored and resent verbatim. A refused dispatch answers a problem instead (422 bot.invalid_job, 404 bot.device_not_found, 409 bot.class_unadvertised, 409 bot.job_id_reused), never this contract.",
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/bot.job-dispatch-result.v1/$defs/dispatched"
+ },
+ {
+ "$ref": "#/components/schemas/bot.job-dispatch-result.v1/$defs/state"
+ }
+ ],
+ "$defs": {
+ "job": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "One run_jobs row as presentJob renders it: what Run dispatched, to which device, and how far the job and its metering have come.",
+ "required": [
+ "id",
+ "node_id",
+ "class",
+ "state",
+ "project_id",
+ "subject",
+ "provider",
+ "cancel_requested",
+ "fault_code",
+ "sent_at",
+ "started_ms",
+ "finished_at",
+ "exit_reason",
+ "exit_code",
+ "wall_ms",
+ "usage_read",
+ "result",
+ "job",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The platform.job@1 id Run minted and the stem of every usage reading key (run:<id>:<n>)."
+ },
+ "node_id": {
+ "type": "string",
+ "pattern": "^dev_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The paired device the job was sent to; a Node never reads another device's job."
+ },
+ "class": {
+ "$ref": "#/components/schemas/platform.runtime-class.v1"
+ },
+ "state": {
+ "enum": [
+ "queued",
+ "placed",
+ "running",
+ "succeeded",
+ "failed",
+ "cancelled",
+ "expired"
+ ],
+ "description": "queued (stored; sent and resent until acked), placed (the Node acked it), running (job_started), then one end state from job_exit: succeeded (exited 0), failed (another exit code, or reason limit, stopped or failed, or a nack), cancelled (a cancel before start, or reason cancelled), expired (reason ttl)."
+ },
+ "project_id": {
+ "type": "string",
+ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The payer project Run named; required on every dispatch (422 without it)."
+ },
+ "subject": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 128,
+ "description": "The subject the readings are for (user:usr_…), as Run sent it; null when Run sent none."
+ },
+ "provider": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 128,
+ "description": "The offer's provider, as Run sent it; null for a first-party node."
+ },
+ "cancel_requested": {
+ "type": "boolean",
+ "description": "A cancel was asked for this job; a job not yet started is cancelled at once, a running one ends with its job_exit."
+ },
+ "fault_code": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 64,
+ "description": "The Node's nack fault code, or why Bot stopped sending the job (bot.class_unadvertised); null while it is still sendable."
+ },
+ "sent_at": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0,
+ "description": "First time the `job` frame went out (epoch ms); null while queued and never sent."
+ },
+ "started_ms": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0,
+ "description": "The Node's job_started started_ms: the anchor of every usage second; null until it started."
+ },
+ "finished_at": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0,
+ "description": "When the first job_exit settled the job (epoch ms); null until then, and for a job cancelled before it started that no job_exit ever settled."
+ },
+ "exit_reason": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "exited",
+ "cancelled",
+ "ttl",
+ "limit",
+ "stopped",
+ "failed",
+ null
+ ],
+ "description": "The first job_exit's reason (platform.job-frame@1); null until it arrived."
+ },
+ "exit_code": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "The process's exit status; null when it was killed or never ran."
+ },
+ "wall_ms": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0,
+ "description": "job_exit.usage.wall_ms as billed, capped at the job's limits.wall_ms; null until job_exit."
+ },
+ "usage_read": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Wall-clock seconds of the job already queued for Billing; only moves forward."
+ },
+ "result": {
+ "description": "The job_exit result (any JSON); null unless a job_exit carried one."
+ },
+ "job": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/platform.job.v1"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The platform.job@1 body Run dispatched, stored and resent verbatim; present on every stored job."
+ },
+ "created_at": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Epoch ms Bot first stored the job."
+ },
+ "updated_at": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Epoch ms of the last change to the row."
+ }
+ }
+ },
+ "stdout": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "The last 1 MiB of a job's captured stdout, held in memory by Bot (job_stdout chunks; a chunk_seq already held is dropped, and older chunks fall off once the ring is full).",
+ "required": [
+ "text",
+ "first_seq",
+ "last_seq",
+ "truncated"
+ ],
+ "properties": {
+ "text": {
+ "type": "string",
+ "description": "The held chunks joined in order."
+ },
+ "first_seq": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "The chunk_seq of the first held chunk; null when none is held."
+ },
+ "last_seq": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "The highest chunk_seq seen for the job; 0 when none."
+ },
+ "truncated": {
+ "type": "boolean",
+ "description": "True when an earlier chunk fell off the 1 MiB ring."
+ }
+ }
+ },
+ "dispatched": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "The answer of POST /api/v1/jobs (201) and POST /api/v1/jobs/:id/cancel (200): the row and whether the frame went out on the link now.",
+ "required": [
+ "job",
+ "sent"
+ ],
+ "properties": {
+ "job": {
+ "$ref": "#/components/schemas/bot.job-dispatch-result.v1/$defs/job"
+ },
+ "sent": {
+ "type": "boolean",
+ "description": "True when the `job` (dispatch) or `job_cancel` (cancel) frame was written to the device's link; false when the device is away (a job is resent on reconnect until acked)."
+ }
+ }
+ },
+ "state": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "The answer of GET /api/v1/jobs/:id (200): the row and the job's held stdout (null when the Node sent none).",
+ "required": [
+ "job",
+ "stdout"
+ ],
+ "properties": {
+ "job": {
+ "$ref": "#/components/schemas/bot.job-dispatch-result.v1/$defs/job"
+ },
+ "stdout": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/bot.job-dispatch-result.v1/$defs/stdout"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
"bot.robot-control-result.v1": {
"bot.robot-control-result.v1": {
"title": "BotRobotControlResult",
"title": "BotRobotControlResult",
"description": "bot.robot-control-result@1: answers of bot.robot.control on OpenVibe.Bot. POST /api/v1/robots/:id/estop → { robot } with its e-stop as it now stands.",
"description": "bot.robot-control-result@1: answers of bot.robot.control on OpenVibe.Bot. POST /api/v1/robots/:id/estop → { robot } with its e-stop as it now stands.",
@@ -2072,7 +2354,7 @@
@@ -2072,7 +2354,7 @@
"artifact": {
"artifact": {
"type": "object",
"type": "object",
"additionalProperties": false,
"additionalProperties": false,
- "description": "The pre-registered artifact to run; required for class `function`.",
+ "description": "The pre-registered artifact to run; required for classes `function` and `code`.",
"required": [
"required": [
"name",
"name",
"version"
"version"
@@ -2177,7 +2459,10 @@
@@ -2177,7 +2459,10 @@
],
],
"properties": {
"properties": {
"class": {
"class": {
- "const": "function"
+ "enum": [
+ "function",
+ "code"
+ ]
}
}
}
}
},
},
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Chat API",
"title": "OpenVibe.Chat API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Codes API",
"title": "OpenVibe.Codes API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Community API",
"title": "OpenVibe.Community API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Coupons API",
"title": "OpenVibe.Coupons API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Deals API",
"title": "OpenVibe.Deals API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Events API",
"title": "OpenVibe.Events API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Games API",
"title": "OpenVibe.Games API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Host API",
"title": "OpenVibe.Host API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Live API",
"title": "OpenVibe.Live API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Media API",
"title": "OpenVibe.Media API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Network API",
"title": "OpenVibe.Network API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.News API",
"title": "OpenVibe.News API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenRe.Stream API",
"title": "OpenRe.Stream API",
- "version": "0.96.0",
- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Reviews API",
"title": "OpenVibe.Reviews API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Search API",
"title": "OpenVibe.Search API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Sources API",
"title": "OpenVibe.Sources API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Space API",
"title": "OpenVibe.Space API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Tips API",
"title": "OpenVibe.Tips API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Tools API",
"title": "OpenVibe.Tools API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Trade API",
"title": "OpenVibe.Trade API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.VIP API",
"title": "OpenVibe.VIP API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Wiki API",
"title": "OpenVibe.Wiki API",
- "version": "0.96.0",
- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.96.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.97.0",
+ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.97.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": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -52385,7 +52385,7 @@ export type Job = {
@@ -52385,7 +52385,7 @@ export type Job = {
id: string;
id: string;
class: RuntimeClass;
class: RuntimeClass;
/**
/**
- * The pre-registered artifact to run; required for class `function`.
+ * The pre-registered artifact to run; required for classes `function` and `code`.
*/
*/
artifact?: {
artifact?: {
name: string;
name: string;
@@ -53489,6 +53489,206 @@ export interface BotCommandResult {
@@ -53489,6 +53489,206 @@ export interface BotCommandResult {
cached?: boolean;
cached?: boolean;
}
}
+/** [email protected] (owner: bot) */
+/**
+ * bot.job-dispatch-result@1: what OpenVibe.Bot answers OpenVibe.Run's bot.job.dispatch calls (server/api/v1.js and server/jobs/index.js). POST /api/v1/jobs is 201 { job, sent }: `job` is Bot's run_jobs record and `sent` says whether the `job` frame went out on the device link now (false while the paired Node is away; it is resent on every reconnect until the Node acks it). POST /api/v1/jobs/:id/cancel is 200 { job, sent } with the same shape: `sent` says whether a `job_cancel` frame went out. GET /api/v1/jobs/:id is 200 { job, stdout }: the record plus the last 1 MiB of the job's captured stdout, or null when the Node sent none. The record's `state` follows the run_jobs lifecycle queued -> placed (the Node acked) -> running (job_started) -> one end state, and its `job` field is the platform.job@1 body Run dispatched, stored and resent verbatim. A refused dispatch answers a problem instead (422 bot.invalid_job, 404 bot.device_not_found, 409 bot.class_unadvertised, 409 bot.job_id_reused), never this contract.
+ */
+export type BotJobDispatchResult =
+ | {
+ /**
+ * One run_jobs row as presentJob renders it: what Run dispatched, to which device, and how far the job and its metering have come.
+ */
+ job: {
+ /**
+ * The platform.job@1 id Run minted and the stem of every usage reading key (run:<id>:<n>).
+ */
+ id: string;
+ /**
+ * The paired device the job was sent to; a Node never reads another device's job.
+ */
+ node_id: string;
+ class: RuntimeClass;
+ /**
+ * queued (stored; sent and resent until acked), placed (the Node acked it), running (job_started), then one end state from job_exit: succeeded (exited 0), failed (another exit code, or reason limit, stopped or failed, or a nack), cancelled (a cancel before start, or reason cancelled), expired (reason ttl).
+ */
+ state: "queued" | "placed" | "running" | "succeeded" | "failed" | "cancelled" | "expired";
+ /**
+ * The payer project Run named; required on every dispatch (422 without it).
+ */
+ project_id: string;
+ /**
+ * The subject the readings are for (user:usr_…), as Run sent it; null when Run sent none.
+ */
+ subject: string | null;
+ /**
+ * The offer's provider, as Run sent it; null for a first-party node.
+ */
+ provider: string | null;
+ /**
+ * A cancel was asked for this job; a job not yet started is cancelled at once, a running one ends with its job_exit.
+ */
+ cancel_requested: boolean;
+ /**
+ * The Node's nack fault code, or why Bot stopped sending the job (bot.class_unadvertised); null while it is still sendable.
+ */
+ fault_code: string | null;
+ /**
+ * First time the `job` frame went out (epoch ms); null while queued and never sent.
+ */
+ sent_at: number | null;
+ /**
+ * The Node's job_started started_ms: the anchor of every usage second; null until it started.
+ */
+ started_ms: number | null;
+ /**
+ * When the first job_exit settled the job (epoch ms); null until then, and for a job cancelled before it started that no job_exit ever settled.
+ */
+ finished_at: number | null;
+ /**
+ * The first job_exit's reason (platform.job-frame@1); null until it arrived.
+ */
+ exit_reason: "exited" | "cancelled" | "ttl" | "limit" | "stopped" | "failed" | null;
+ /**
+ * The process's exit status; null when it was killed or never ran.
+ */
+ exit_code: number | null;
+ /**
+ * job_exit.usage.wall_ms as billed, capped at the job's limits.wall_ms; null until job_exit.
+ */
+ wall_ms: number | null;
+ /**
+ * Wall-clock seconds of the job already queued for Billing; only moves forward.
+ */
+ usage_read: number;
+ /**
+ * The job_exit result (any JSON); null unless a job_exit carried one.
+ */
+ result: {
+ [k: string]: unknown | undefined;
+ };
+ /**
+ * The platform.job@1 body Run dispatched, stored and resent verbatim; present on every stored job.
+ */
+ job: Job | null;
+ /**
+ * Epoch ms Bot first stored the job.
+ */
+ created_at: number;
+ /**
+ * Epoch ms of the last change to the row.
+ */
+ updated_at: number;
+ };
+ /**
+ * True when the `job` (dispatch) or `job_cancel` (cancel) frame was written to the device's link; false when the device is away (a job is resent on reconnect until acked).
+ */
+ sent: boolean;
+ }
+ | {
+ /**
+ * One run_jobs row as presentJob renders it: what Run dispatched, to which device, and how far the job and its metering have come.
+ */
+ job: {
+ /**
+ * The platform.job@1 id Run minted and the stem of every usage reading key (run:<id>:<n>).
+ */
+ id: string;
+ /**
+ * The paired device the job was sent to; a Node never reads another device's job.
+ */
+ node_id: string;
+ class: RuntimeClass;
+ /**
+ * queued (stored; sent and resent until acked), placed (the Node acked it), running (job_started), then one end state from job_exit: succeeded (exited 0), failed (another exit code, or reason limit, stopped or failed, or a nack), cancelled (a cancel before start, or reason cancelled), expired (reason ttl).
+ */
+ state: "queued" | "placed" | "running" | "succeeded" | "failed" | "cancelled" | "expired";
+ /**
+ * The payer project Run named; required on every dispatch (422 without it).
+ */
+ project_id: string;
+ /**
+ * The subject the readings are for (user:usr_…), as Run sent it; null when Run sent none.
+ */
+ subject: string | null;
+ /**
+ * The offer's provider, as Run sent it; null for a first-party node.
+ */
+ provider: string | null;
+ /**
+ * A cancel was asked for this job; a job not yet started is cancelled at once, a running one ends with its job_exit.
+ */
+ cancel_requested: boolean;
+ /**
+ * The Node's nack fault code, or why Bot stopped sending the job (bot.class_unadvertised); null while it is still sendable.
+ */
+ fault_code: string | null;
+ /**
+ * First time the `job` frame went out (epoch ms); null while queued and never sent.
+ */
+ sent_at: number | null;
+ /**
+ * The Node's job_started started_ms: the anchor of every usage second; null until it started.
+ */
+ started_ms: number | null;
+ /**
+ * When the first job_exit settled the job (epoch ms); null until then, and for a job cancelled before it started that no job_exit ever settled.
+ */
+ finished_at: number | null;
+ /**
+ * The first job_exit's reason (platform.job-frame@1); null until it arrived.
+ */
+ exit_reason: "exited" | "cancelled" | "ttl" | "limit" | "stopped" | "failed" | null;
+ /**
+ * The process's exit status; null when it was killed or never ran.
+ */
+ exit_code: number | null;
+ /**
+ * job_exit.usage.wall_ms as billed, capped at the job's limits.wall_ms; null until job_exit.
+ */
+ wall_ms: number | null;
+ /**
+ * Wall-clock seconds of the job already queued for Billing; only moves forward.
+ */
+ usage_read: number;
+ /**
+ * The job_exit result (any JSON); null unless a job_exit carried one.
+ */
+ result: {
+ [k: string]: unknown | undefined;
+ };
+ /**
+ * The platform.job@1 body Run dispatched, stored and resent verbatim; present on every stored job.
+ */
+ job: Job | null;
+ /**
+ * Epoch ms Bot first stored the job.
+ */
+ created_at: number;
+ /**
+ * Epoch ms of the last change to the row.
+ */
+ updated_at: number;
+ };
+ stdout: {
+ /**
+ * The held chunks joined in order.
+ */
+ text: string;
+ /**
+ * The chunk_seq of the first held chunk; null when none is held.
+ */
+ first_seq: number | null;
+ /**
+ * The highest chunk_seq seen for the job; 0 when none.
+ */
+ last_seq: number;
+ /**
+ * True when an earlier chunk fell off the 1 MiB ring.
+ */
+ truncated: boolean;
+ } | null;
+ };
+
/** [email protected] (owner: bot) */
/** [email protected] (owner: bot) */
/**
/**
* bot.robot-read-request@1: the query of bot.robot.read on OpenVibe.Bot (server/api/v1.js); the routes take no body. owner names whose robots GET /api/v1/robots lists (a service must give it; a person may give only their own subject). limit (1 to 200, default 50) and before (the last audit id seen) page GET …/robots/:id/audit.
* bot.robot-read-request@1: the query of bot.robot.read on OpenVibe.Bot (server/api/v1.js); the routes take no body. owner names whose robots GET /api/v1/robots lists (a service must give it; a person may give only their own subject). limit (1 to 200, default 50) and before (the last audit id seen) page GET …/robots/:id/audit.
@@ -21,5 +21,6 @@
@@ -21,5 +21,6 @@
"POST /api/v1/jobs/:id/cancel",
"POST /api/v1/jobs/:id/cancel",
"GET /api/v1/jobs/:id"
"GET /api/v1/jobs/:id"
],
],
- "inputSchema": "platform.job@1"
+ "inputSchema": "platform.job@1",
+ "outputSchema": "bot.job-dispatch-result@1"
}
}
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.96.0",
+ "version": "0.97.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.96.0",
+ "version": "0.97.0",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.96.0",
+ "version": "0.97.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
"types": "generated/typescript/index.d.ts",