openvibers/openvibe.contracts Commits

Define Run job contracts with non-JWT sample stream tickets

OP OpenVibers committed 4 Oct 2026 · bde0e52480 · 106 files, +5540 −57


Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex e97406cf..255973c8 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,32 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
the notes were in the tag and commit messages (`git tag -n1`).
++## 0.90.0 — 2026-10-04
++
++**OpenVibe.Run service and job API** (plan T14 R1, ADR-034). The contract OpenVibe.Run is built against; everything is
++planned (Run's repository does not exist yet) and additive.
++- New service manifest `manifests/services/run.json`: `run`, status and exposure `placeholder`, domain `openvibe.run`,
++ lifecycle all `none` (not built). `manifests/products/openvibe.run.json` gains `relationships.service: run` and keeps
++ `noRepo: true`.
++- New planned capabilities, owner `run`, each with an input and an output schema: `run.job.submit` (sensitive: it bills
++ the project), `run.job.read`, `run.job.list`, `run.job.cancel`, `run.job.stream` (public, project-scoped) and
++ `run.job.admin` (internal operator access to every project's jobs). `run.admin` is spelled `run.job.admin` because a
++ capability id has three segments.
++- New contracts under `contracts/run/`: `run.job-create-request@1` (class from `platform.runtime-class@1`, artifact
++ required for `function` and `code`, `inputs` as Media objects pinned by sha256, `limits` wall/cpu/mem as
++ `platform.job@1`, `ttl_ms`, `egress` `none | public | openvibe-only` as Node's `worker.egress`,
++ `requirements` as `platform.workload-requirements@1`, `idempotency_key`), `run.job-create-result@1`,
++ `run.job-read-result@1` (states `queued | placed | running | succeeded | failed | cancelled | expired`, placement node,
++ timings, exit, `usage.seconds`, result, error, with per-state rules), `run.job-list-query@1`, `run.job-list-result@1`
++ (`next_cursor` pages), `run.job-stream-ticket-result@1` (one-use, two-minute ticket, as Events' realtime ticket) and
++ `run.job-stream-event@1` (SSE `output | state | end`).
++- New planned events produced by Run: `run.job.queued`, `run.job.started`, `run.job.succeeded`, `run.job.failed`,
++ `run.job.cancelled` and `run.job.expired`, projections of the job without args, inputs, result or output.
++- `ids.newId('job')` mints `job_<ULID>`, the `platform.job@1` id Run passes to the node, so the metering key stays
++ `run:<job id>:<n>`.
++- Fixtures: valid and invalid for every new contract; `test/platform-run.test.js` pins the Run job id, artifact,
++ limits and egress to `platform.job@1` and Node, and `usage.seconds` to the sum of the job's readings.
++
## 0.89.0 — 2026-10-03
**Agent tokens and confirmations** (plan T2 step 9; OpenVibe.Network `docs/t2-projects-and-grants.md` §2 gaps 1–3,
ddiff --git a/README.md b/README.md
iindex c0c44cec..573ff8d4 100644
---- a/README.md
++++ b/README.md
@@@ -216,6 +216,27 @@ Each service manifest has a `lifecycle` block, filled from the service's code an
`ovhost validate <service>` (OpenVibe.Host) reads the block from the inventory or from this package's manifest. It fails when a field is missing, when `deadlineSeconds` exceeds the unit's TimeoutStopSec or the unit's KillSignal is a different signal, and when the checkout's installed openvibe-contracts is outside `contracts.range`.
++## Run job API (v0.90, planned)
++
++OpenVibe.Run (`manifests/services/run.json`, a placeholder until the repository exists; plan T14 R1) takes jobs,
++places each on a node that advertises `worker:<class>` and sends it as `platform.job@1`. Its routes, all planned:
++
++| Route | Capability | Body | Answer |
++| --- | --- | --- | --- |
++| `POST /api/v1/jobs` | `run.job.submit` | `run.job-create-request@1` | `run.job-create-result@1` (201, or 200 for an idempotent repeat) |
++| `GET /api/v1/jobs` | `run.job.list` | `run.job-list-query@1` (query string) | `run.job-list-result@1` (cursor pages) |
++| `GET /api/v1/jobs/:id` | `run.job.read` | — | `run.job-read-result@1` |
++| `POST /api/v1/jobs/:id/cancel` | `run.job.cancel` | — | `run.job-read-result@1` |
++| `POST /api/v1/jobs/:id/stream/ticket` | `run.job.stream` | — | `run.job-stream-ticket-result@1` (one-use, 120 s) |
++| `GET /api/v1/jobs/:id/stream?ticket=` | `run.job.stream` | — | SSE of `run.job-stream-event@1` (`output`, `state`, `end`; Last-Event-ID resumes) |
++| `GET /api/v1/admin/jobs` | `run.job.admin` (internal) | `run.job-list-query@1` with `project_id` | `run.job-list-result@1` |
++
++A job moves `queued → placed → running` and ends `succeeded`, `failed`, `cancelled` or `expired`; Run publishes
++`run.job.queued|started|succeeded|failed|cancelled|expired`. Run mints the job id (`job_<ULID>`, `ids.newId('job')`)
++and passes it unchanged as the `platform.job@1` id, so every metering reading is `run:<job id>:<n>` (service `run`,
++operation `function.invoke`, unit `s`) and `usage.seconds` is their sum. Inputs are Media objects pinned by sha256
++(never a URL); `egress` takes Node's `worker.egress` values (`none` by default, `public`, `openvibe-only`).
++
## Versioning and compatibility
- A contract id is permanent. Minor versions only add optional fields.
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 24dbe688..a4f3b81f 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -5694,6 +5694,136 @@
"visibility": "first-party",
"compatibility": "backward",
"status": "active"
++ },
++ {
++ "id": "run.job-create-request",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "run/job-create-request.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "planned",
++ "adr": "ADR-034"
++ },
++ {
++ "id": "run.job-create-result",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "run/job-create-result.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "planned",
++ "adr": "ADR-034"
++ },
++ {
++ "id": "run.job-read-result",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "run/job-read-result.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "planned",
++ "adr": "ADR-034"
++ },
++ {
++ "id": "run.job-list-query",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "run/job-list-query.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "planned",
++ "adr": "ADR-034"
++ },
++ {
++ "id": "run.job-list-result",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "run/job-list-result.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "planned",
++ "adr": "ADR-034"
++ },
++ {
++ "id": "run.job-stream-ticket-result",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "run/job-stream-ticket-result.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "planned",
++ "adr": "ADR-034"
++ },
++ {
++ "id": "run.job-stream-event",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "run/job-stream-event.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "planned",
++ "adr": "ADR-034"
++ },
++ {
++ "id": "run.job.queued",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "events/payloads/run.job.queued.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "planned",
++ "adr": "ADR-034"
++ },
++ {
++ "id": "run.job.started",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "events/payloads/run.job.started.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "planned",
++ "adr": "ADR-034"
++ },
++ {
++ "id": "run.job.succeeded",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "events/payloads/run.job.succeeded.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "planned",
++ "adr": "ADR-034"
++ },
++ {
++ "id": "run.job.failed",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "events/payloads/run.job.failed.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "planned",
++ "adr": "ADR-034"
++ },
++ {
++ "id": "run.job.cancelled",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "events/payloads/run.job.cancelled.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "planned",
++ "adr": "ADR-034"
++ },
++ {
++ "id": "run.job.expired",
++ "version": "1.0.0",
++ "owner": "run",
++ "schema": "events/payloads/run.job.expired.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "planned",
++ "adr": "ADR-034"
}
]
}
ddiff --git a/contracts/events/payloads/run.job.cancelled.v1.json b/contracts/events/payloads/run.job.cancelled.v1.json
nnew file mode 100644
iindex 00000000..5d36c308
---- /dev/null
++++ b/contracts/events/payloads/run.job.cancelled.v1.json
@@@ -0,0 +1,115 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/run.job.cancelled.v1.json",
++ "title": "RunJobCancelledPayload",
++ "description": "run.job.cancelled v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). The job ended cancelled (POST /api/v1/jobs/{id}/cancel by its project or run.job.admin, or job_exit reason cancelled). A job cancelled before placement has no placement and no exit. A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority normal, actor service:run.",
++ "type": "object",
++ "required": [
++ "job_id",
++ "project_id",
++ "requester",
++ "state",
++ "class",
++ "artifact",
++ "egress",
++ "placement",
++ "timings",
++ "exit",
++ "usage",
++ "error"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "job_id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "requester": {
++ "$ref": "../../identity/subject-ref.v1.json"
++ },
++ "state": {
++ "const": "cancelled",
++ "description": "The job state (run.job-read-result@1 state)."
++ },
++ "class": {
++ "$ref": "../../platform/runtime-class.v1.json"
++ },
++ "artifact": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/artifact"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "egress": {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/egress"
++ },
++ "placement": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/placement"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created_at",
++ "placed_at",
++ "started_at",
++ "finished_at"
++ ],
++ "properties": {
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "placed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "started_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "finished_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "exit": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/exit"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "usage": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/usage"
++ },
++ "error": {
++ "type": "null"
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/run.job.expired.v1.json b/contracts/events/payloads/run.job.expired.v1.json
nnew file mode 100644
iindex 00000000..3840d820
---- /dev/null
++++ b/contracts/events/payloads/run.job.expired.v1.json
@@@ -0,0 +1,115 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/run.job.expired.v1.json",
++ "title": "RunJobExpiredPayload",
++ "description": "run.job.expired v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). The job ended expired: its ttl_ms ran out before a node took it (run.job.unplaceable), before it started, or while it ran (job_exit reason ttl, run.job.ttl). A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority normal, actor service:run.",
++ "type": "object",
++ "required": [
++ "job_id",
++ "project_id",
++ "requester",
++ "state",
++ "class",
++ "artifact",
++ "egress",
++ "placement",
++ "timings",
++ "exit",
++ "usage",
++ "error"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "job_id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "requester": {
++ "$ref": "../../identity/subject-ref.v1.json"
++ },
++ "state": {
++ "const": "expired",
++ "description": "The job state (run.job-read-result@1 state)."
++ },
++ "class": {
++ "$ref": "../../platform/runtime-class.v1.json"
++ },
++ "artifact": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/artifact"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "egress": {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/egress"
++ },
++ "placement": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/placement"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created_at",
++ "placed_at",
++ "started_at",
++ "finished_at"
++ ],
++ "properties": {
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "placed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "started_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "finished_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "exit": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/exit"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "usage": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/usage"
++ },
++ "error": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/error"
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/run.job.failed.v1.json b/contracts/events/payloads/run.job.failed.v1.json
nnew file mode 100644
iindex 00000000..6e2d4442
---- /dev/null
++++ b/contracts/events/payloads/run.job.failed.v1.json
@@@ -0,0 +1,115 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/run.job.failed.v1.json",
++ "title": "RunJobFailedPayload",
++ "description": "run.job.failed v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). The job ended failed: job_exit with a non-zero code or reason limit, stopped or failed, or an input digest mismatch before the process started. A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority important, actor service:run.",
++ "type": "object",
++ "required": [
++ "job_id",
++ "project_id",
++ "requester",
++ "state",
++ "class",
++ "artifact",
++ "egress",
++ "placement",
++ "timings",
++ "exit",
++ "usage",
++ "error"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "job_id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "requester": {
++ "$ref": "../../identity/subject-ref.v1.json"
++ },
++ "state": {
++ "const": "failed",
++ "description": "The job state (run.job-read-result@1 state)."
++ },
++ "class": {
++ "$ref": "../../platform/runtime-class.v1.json"
++ },
++ "artifact": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/artifact"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "egress": {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/egress"
++ },
++ "placement": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/placement"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created_at",
++ "placed_at",
++ "started_at",
++ "finished_at"
++ ],
++ "properties": {
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "placed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "started_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "finished_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "exit": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/exit"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "usage": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/usage"
++ },
++ "error": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/error"
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/run.job.queued.v1.json b/contracts/events/payloads/run.job.queued.v1.json
nnew file mode 100644
iindex 00000000..91f1c7d9
---- /dev/null
++++ b/contracts/events/payloads/run.job.queued.v1.json
@@@ -0,0 +1,92 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/run.job.queued.v1.json",
++ "title": "RunJobQueuedPayload",
++ "description": "run.job.queued v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). A job was stored queued (POST /api/v1/jobs created it; not published for an idempotent repeat). A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority low, actor service:run.",
++ "type": "object",
++ "required": [
++ "job_id",
++ "project_id",
++ "requester",
++ "state",
++ "class",
++ "artifact",
++ "egress",
++ "placement",
++ "timings",
++ "exit",
++ "usage",
++ "error"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "job_id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "requester": {
++ "$ref": "../../identity/subject-ref.v1.json"
++ },
++ "state": {
++ "const": "queued",
++ "description": "The job state (run.job-read-result@1 state)."
++ },
++ "class": {
++ "$ref": "../../platform/runtime-class.v1.json"
++ },
++ "artifact": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/artifact"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "egress": {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/egress"
++ },
++ "placement": {
++ "type": "null"
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created_at",
++ "placed_at",
++ "started_at",
++ "finished_at"
++ ],
++ "properties": {
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "placed_at": {
++ "type": "null"
++ },
++ "started_at": {
++ "type": "null"
++ },
++ "finished_at": {
++ "type": "null"
++ }
++ }
++ },
++ "exit": {
++ "type": "null"
++ },
++ "usage": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/usage"
++ },
++ "error": {
++ "type": "null"
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/run.job.started.v1.json b/contracts/events/payloads/run.job.started.v1.json
nnew file mode 100644
iindex 00000000..6cae6188
---- /dev/null
++++ b/contracts/events/payloads/run.job.started.v1.json
@@@ -0,0 +1,94 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/run.job.started.v1.json",
++ "title": "RunJobStartedPayload",
++ "description": "run.job.started v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). The node sent job_started: the process runs. Usage seconds count from started_at. A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority low, actor service:run.",
++ "type": "object",
++ "required": [
++ "job_id",
++ "project_id",
++ "requester",
++ "state",
++ "class",
++ "artifact",
++ "egress",
++ "placement",
++ "timings",
++ "exit",
++ "usage",
++ "error"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "job_id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "requester": {
++ "$ref": "../../identity/subject-ref.v1.json"
++ },
++ "state": {
++ "const": "running",
++ "description": "The job state (run.job-read-result@1 state)."
++ },
++ "class": {
++ "$ref": "../../platform/runtime-class.v1.json"
++ },
++ "artifact": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/artifact"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "egress": {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/egress"
++ },
++ "placement": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/placement"
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created_at",
++ "placed_at",
++ "started_at",
++ "finished_at"
++ ],
++ "properties": {
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "placed_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "started_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "finished_at": {
++ "type": "null"
++ }
++ }
++ },
++ "exit": {
++ "type": "null"
++ },
++ "usage": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/usage"
++ },
++ "error": {
++ "type": "null"
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/run.job.succeeded.v1.json b/contracts/events/payloads/run.job.succeeded.v1.json
nnew file mode 100644
iindex 00000000..2374cc38
---- /dev/null
++++ b/contracts/events/payloads/run.job.succeeded.v1.json
@@@ -0,0 +1,110 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/run.job.succeeded.v1.json",
++ "title": "RunJobSucceededPayload",
++ "description": "run.job.succeeded v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). The job ended succeeded: job_exit reason exited with code 0. Carries no result (read it with GET /api/v1/jobs/{id}). A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority normal, actor service:run.",
++ "type": "object",
++ "required": [
++ "job_id",
++ "project_id",
++ "requester",
++ "state",
++ "class",
++ "artifact",
++ "egress",
++ "placement",
++ "timings",
++ "exit",
++ "usage",
++ "error"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "job_id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "requester": {
++ "$ref": "../../identity/subject-ref.v1.json"
++ },
++ "state": {
++ "const": "succeeded",
++ "description": "The job state (run.job-read-result@1 state)."
++ },
++ "class": {
++ "$ref": "../../platform/runtime-class.v1.json"
++ },
++ "artifact": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/artifact"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "egress": {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/egress"
++ },
++ "placement": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/placement"
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created_at",
++ "placed_at",
++ "started_at",
++ "finished_at"
++ ],
++ "properties": {
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "placed_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "started_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "finished_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "exit": {
++ "allOf": [
++ {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/exit"
++ },
++ {
++ "type": "object",
++ "properties": {
++ "reason": {
++ "const": "exited"
++ },
++ "code": {
++ "const": 0
++ }
++ }
++ }
++ ]
++ },
++ "usage": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/usage"
++ },
++ "error": {
++ "type": "null"
++ }
++ }
++}
ddiff --git a/contracts/run/job-create-request.v1.json b/contracts/run/job-create-request.v1.json
nnew file mode 100644
iindex 00000000..a6420423
---- /dev/null
++++ b/contracts/run/job-create-request.v1.json
@@@ -0,0 +1,119 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/job-create-request.v1.json",
++ "title": "RunJobCreateRequest",
++ "description": "run.job-create-request@1 (PLANNED; plan T14 R1, ADR-034): the body of POST /api/v1/jobs on OpenVibe.Run (capability run.job.submit). The job belongs to the project the caller's token names. CLASS is a platform.runtime-class@1 name; Run accepts any of them but places a job only on a node that advertises worker:<class> (platform.resource-offer@1), so a class no node runs today (browser, linux, desktop, gpu) ends expired with run.job.unplaceable. ARTIFACT is the pre-registered artifact to run (name and one exact version), required for function and code, as platform.job@1 carries it. ARGS is the artifact's declared input as JSON (at most 512 KiB serialized, else 413). INPUTS are files placed in the job directory before the process starts, each an OpenVibe.Media object pinned by its sha256: a size or digest mismatch fails the job (run.job.input_digest_mismatch) before the process ever runs; Run never fetches a caller-chosen URL. LIMITS are hard caps merged stricter with the node's own (platform.job@1 limits); a request over the project's tier cap is refused 422 run.limits.exceeded, never silently lowered. EGRESS is the network the job may reach, the values of OpenVibe.Node's worker.egress: none (default; any node, sent as net deny), public or openvibe-only (only a node whose worker.egress is exactly that policy, until platform.job@1 net carries it per job). REQUIREMENTS (platform.workload-requirements@1) narrow placement; absent means { kind: run.<class>, mobility: job, latency_class: background, objective: balanced }. IDEMPOTENCY_KEY: a repeat from the same project within 24 h with an identical body answers the first job (200, created false); with a different body 409 run.idempotency.conflict.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "class",
++ "limits"
++ ],
++ "properties": {
++ "class": {
++ "$ref": "../platform/runtime-class.v1.json"
++ },
++ "artifact": {
++ "$ref": "#/$defs/artifact"
++ },
++ "args": {
++ "type": "object",
++ "description": "The artifact's declared input as JSON; default {}."
++ },
++ "inputs": {
++ "type": "array",
++ "maxItems": 32,
++ "items": {
++ "$ref": "#/$defs/input"
++ },
++ "description": "Files placed in the job directory before the process starts; names are unique (else 422)."
++ },
++ "limits": {
++ "$ref": "#/$defs/limits"
++ },
++ "ttl_ms": {
++ "type": "integer",
++ "minimum": 1000,
++ "maximum": 86400000,
++ "description": "Lifetime from creation, covering the wait for a node and the run (platform.job@1 ttl_ms); when it runs out the job ends expired. Default: limits.wall_ms plus 600000."
++ },
++ "egress": {
++ "$ref": "#/$defs/egress"
++ },
++ "requirements": {
++ "$ref": "../platform/workload-requirements.v1.json"
++ },
++ "idempotency_key": {
++ "$ref": "#/$defs/idempotency_key"
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "required": ["class"],
++ "properties": { "class": { "enum": ["function", "code"] } }
++ },
++ "then": {
++ "required": ["artifact"]
++ }
++ }
++ ],
++ "$defs": {
++ "artifact": {
++ "type": "object",
++ "additionalProperties": false,
++ "description": "The pre-registered artifact to run (platform.job@1 artifact).",
++ "required": ["name", "version"],
++ "properties": {
++ "name": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{0,127}$" },
++ "version": { "type": "string", "pattern": "^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$", "description": "One exact version, never a range" }
++ }
++ },
++ "input": {
++ "type": "object",
++ "additionalProperties": false,
++ "description": "One input file: an OpenVibe.Media object the caller's project can read, pinned by digest.",
++ "required": ["name", "media_id", "sha256"],
++ "properties": {
++ "name": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$", "description": "File name in the job directory; no path separators." },
++ "media_id": { "type": "string", "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$" },
++ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$", "description": "Lowercase hex SHA-256 of the object's bytes." },
++ "size_bytes": { "type": "integer", "minimum": 0 }
++ }
++ },
++ "limits": {
++ "type": "object",
++ "additionalProperties": false,
++ "description": "Caps on the running process (platform.job@1 limits); hitting one ends the job failed with run.job.limit.",
++ "required": ["wall_ms", "cpu_ms", "mem_bytes"],
++ "properties": {
++ "wall_ms": { "type": "integer", "minimum": 1, "description": "Running time from `job_started`" },
++ "cpu_ms": { "type": "integer", "minimum": 1, "description": "CPU time, user plus system" },
++ "mem_bytes": { "type": "integer", "minimum": 1, "description": "Peak resident memory" }
++ }
++ },
++ "egress": {
++ "type": "string",
++ "enum": ["none", "public", "openvibe-only"],
++ "default": "none",
++ "description": "OpenVibe.Node worker.egress values: none (no network), public (the internet without private, link-local or the network's own ranges), openvibe-only (the network's own endpoints only)."
++ },
++ "idempotency_key": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9._:-]{1,128}$"
++ }
++ },
++ "examples": [
++ {
++ "class": "function",
++ "artifact": { "name": "thumbnail", "version": "1.2.0" },
++ "args": { "width": 320 },
++ "inputs": [
++ { "name": "photo.jpg", "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ", "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "size_bytes": 482113 }
++ ],
++ "limits": { "wall_ms": 30000, "cpu_ms": 30000, "mem_bytes": 268435456 },
++ "egress": "none",
++ "idempotency_key": "thumb:photo-7"
++ }
++ ]
++}
ddiff --git a/contracts/run/job-create-result.v1.json b/contracts/run/job-create-result.v1.json
nnew file mode 100644
iindex 00000000..38cbd1a0
---- /dev/null
++++ b/contracts/run/job-create-result.v1.json
@@@ -0,0 +1,21 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/job-create-result.v1.json",
++ "title": "RunJobCreateResult",
++ "description": "run.job-create-result@1 (PLANNED; plan T14 R1, ADR-034): the answer of POST /api/v1/jobs on OpenVibe.Run (capability run.job.submit). 201 with created true and the new job, queued; 200 with created false and the job first created under the same idempotency_key (in whatever state it has reached). Refusals are application/problem+json: 400 (body invalid), 413 (args over 512 KiB), 422 run.limits.exceeded or a duplicate input name, 409 run.idempotency.conflict, 429 (quota).",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created",
++ "job"
++ ],
++ "properties": {
++ "created": {
++ "type": "boolean",
++ "description": "false when the idempotency_key matched an earlier job."
++ },
++ "job": {
++ "$ref": "job-read-result.v1.json"
++ }
++ }
++}
ddiff --git a/contracts/run/job-list-query.v1.json b/contracts/run/job-list-query.v1.json
nnew file mode 100644
iindex 00000000..ec85c0b4
---- /dev/null
++++ b/contracts/run/job-list-query.v1.json
@@@ -0,0 +1,30 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/job-list-query.v1.json",
++ "title": "RunJobListQuery",
++ "description": "run.job-list-query@1 (PLANNED; plan T14 R1): the query string of GET /api/v1/jobs on OpenVibe.Run (capability run.job.list) and of GET /api/v1/admin/jobs (run.job.admin, which alone may pass project_id). Without project_id it lists the caller's token project.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "state": {
++ "enum": ["queued", "placed", "running", "succeeded", "failed", "cancelled", "expired"]
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "Admin only (run.job.admin): list this project's jobs; with run.job.list it is refused 403."
++ },
++ "limit": {
++ "oneOf": [
++ { "type": "integer", "minimum": 1, "maximum": 200 },
++ { "type": "string", "pattern": "^(?:[1-9][0-9]?|1[0-9]{2}|200)$" }
++ ],
++ "description": "Items per page, 1 to 200 (default 50). A query string carries it as a string, which must name the same range."
++ },
++ "cursor": {
++ "type": "string",
++ "maxLength": 512,
++ "description": "next_cursor of the previous page."
++ }
++ }
++}
ddiff --git a/contracts/run/job-list-result.v1.json b/contracts/run/job-list-result.v1.json
nnew file mode 100644
iindex 00000000..48bb9dc7
---- /dev/null
++++ b/contracts/run/job-list-result.v1.json
@@@ -0,0 +1,26 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/job-list-result.v1.json",
++ "title": "RunJobListResult",
++ "description": "run.job-list-result@1 (PLANNED; plan T14 R1): the answer of GET /api/v1/jobs on OpenVibe.Run (capability run.job.list) and GET /api/v1/admin/jobs (run.job.admin), filtered by run.job-list-query@1. Jobs come newest first (by id). More fields may be added.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "jobs",
++ "next_cursor"
++ ],
++ "properties": {
++ "jobs": {
++ "type": "array",
++ "maxItems": 200,
++ "items": {
++ "$ref": "job-read-result.v1.json"
++ }
++ },
++ "next_cursor": {
++ "type": ["string", "null"],
++ "maxLength": 512,
++ "description": "Pass as cursor for the next page; null on the last page."
++ }
++ }
++}
ddiff --git a/contracts/run/job-read-result.v1.json b/contracts/run/job-read-result.v1.json
nnew file mode 100644
iindex 00000000..d2e4ac43
---- /dev/null
++++ b/contracts/run/job-read-result.v1.json
@@@ -0,0 +1,211 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/job-read-result.v1.json",
++ "title": "RunJobReadResult",
++ "description": "run.job-read-result@1 (PLANNED; plan T14 R1, ADR-034): one job on OpenVibe.Run, as GET /api/v1/jobs/{id} answers it (capability run.job.read), POST /api/v1/jobs/{id}/cancel answers it after the cancel (run.job.cancel), and run.job-create-result@1 and run.job-list-result@1 carry it. STATES: queued (stored, waiting for a node) -> placed (sent to a node as platform.job@1, not yet started) -> running (the node sent job_started) -> one end state: succeeded (job_exit reason exited with code 0), failed (exited with another code, or reason limit, stopped or failed), cancelled (POST /api/v1/jobs/{id}/cancel, or job_exit reason cancelled), expired (ttl_ms ran out: before placement, before start, or while running, job_exit reason ttl). An end state never changes. Run mints the id (job_<ULID>) and sends it unchanged as platform.job@1 `id`, so every metering reading of the job is `run:<id>:<n>` (platform.job-frame@1 job_usage). usage.seconds is wall_ms / 1000: the sum of the job's readings (service run, operation function.invoke, unit s). Never carries the job's stdout (GET /api/v1/jobs/{id}/stream) or a credential. More fields may be added.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "project_id",
++ "requester",
++ "state",
++ "class",
++ "artifact",
++ "inputs",
++ "limits",
++ "ttl_ms",
++ "egress",
++ "requirements",
++ "idempotency_key",
++ "placement",
++ "timings",
++ "exit",
++ "result",
++ "usage",
++ "error"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "Minted by Run; the platform.job@1 id and the stem of every usage reading key (run:<id>:<n>)."
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The project the caller's token names; usage is billed to it."
++ },
++ "requester": {
++ "$ref": "../identity/subject-ref.v1.json",
++ "description": "The token principal that submitted the job."
++ },
++ "state": {
++ "enum": [
++ "queued",
++ "placed",
++ "running",
++ "succeeded",
++ "failed",
++ "cancelled",
++ "expired"
++ ]
++ },
++ "class": {
++ "$ref": "../platform/runtime-class.v1.json"
++ },
++ "artifact": {
++ "anyOf": [
++ { "$ref": "job-create-request.v1.json#/$defs/artifact" },
++ { "type": "null" }
++ ]
++ },
++ "inputs": {
++ "type": "array",
++ "maxItems": 32,
++ "items": { "$ref": "job-create-request.v1.json#/$defs/input" }
++ },
++ "limits": {
++ "$ref": "job-create-request.v1.json#/$defs/limits"
++ },
++ "ttl_ms": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "The job's lifetime from created_at, the default filled in."
++ },
++ "egress": {
++ "$ref": "job-create-request.v1.json#/$defs/egress"
++ },
++ "requirements": {
++ "$ref": "../platform/workload-requirements.v1.json",
++ "description": "What the job was placed against, the default filled in."
++ },
++ "idempotency_key": {
++ "anyOf": [
++ { "$ref": "job-create-request.v1.json#/$defs/idempotency_key" },
++ { "type": "null" }
++ ]
++ },
++ "placement": {
++ "anyOf": [
++ { "$ref": "#/$defs/placement" },
++ { "type": "null" }
++ ],
++ "description": "Where the job was sent; null while queued, and for a job cancelled or expired before placement."
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["created_at", "placed_at", "started_at", "finished_at"],
++ "properties": {
++ "created_at": { "type": "string", "format": "date-time" },
++ "placed_at": { "type": ["string", "null"], "format": "date-time" },
++ "started_at": { "type": ["string", "null"], "format": "date-time", "description": "From job_started started_ms; null until the process started." },
++ "finished_at": { "type": ["string", "null"], "format": "date-time", "description": "When the job reached its end state; null before." }
++ }
++ },
++ "exit": {
++ "anyOf": [
++ { "$ref": "#/$defs/exit" },
++ { "type": "null" }
++ ],
++ "description": "The node's job_exit; null until it arrived, and for a job that ended before a node ran it."
++ },
++ "result": {
++ "description": "The artifact's declared output (job_exit result, any JSON); null unless state is succeeded."
++ },
++ "usage": {
++ "$ref": "#/$defs/usage"
++ },
++ "error": {
++ "anyOf": [
++ { "$ref": "#/$defs/error" },
++ { "type": "null" }
++ ],
++ "description": "Why the job failed or expired; null in every other state."
++ }
++ },
++ "$defs": {
++ "placement": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["node", "provider", "region"],
++ "properties": {
++ "node": { "type": "string", "minLength": 1, "maxLength": 128, "description": "The worker's device id (the usage reading's `node`)." },
++ "provider": { "type": ["string", "null"], "maxLength": 128, "description": "The offer's provider (the usage reading's `provider`); null for a first-party node." },
++ "region": { "type": ["string", "null"], "maxLength": 64 }
++ }
++ },
++ "exit": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["reason", "code"],
++ "properties": {
++ "reason": {
++ "enum": ["exited", "cancelled", "ttl", "limit", "stopped", "failed"],
++ "description": "platform.job-frame@1 job_exit reason."
++ },
++ "code": { "type": ["integer", "null"], "description": "The process's exit status; null when it was killed or never ran." }
++ }
++ },
++ "usage": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["seconds", "wall_ms", "cpu_ms", "mem_peak_bytes"],
++ "properties": {
++ "seconds": { "type": "number", "minimum": 0, "description": "Metered wall-clock seconds, wall_ms / 1000 (unit s); 0 when the job never started." },
++ "wall_ms": { "type": "integer", "minimum": 0 },
++ "cpu_ms": { "type": ["integer", "null"], "minimum": 0, "description": "Informational, never billed." },
++ "mem_peak_bytes": { "type": ["integer", "null"], "minimum": 0, "description": "Informational, never billed." }
++ }
++ },
++ "error": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["code", "detail"],
++ "properties": {
++ "code": {
++ "type": "string",
++ "pattern": "^run\\.[a-z0-9_]+(\\.[a-z0-9_]+)*$",
++ "description": "run.job.exit_nonzero, run.job.limit, run.job.stopped, run.job.worker_failed, run.job.input_digest_mismatch, run.job.unplaceable (expired: no node offered the class, egress and requirements), run.job.ttl."
++ },
++ "detail": { "type": ["string", "null"], "maxLength": 1024 }
++ }
++ }
++ },
++ "allOf": [
++ {
++ "if": { "properties": { "state": { "const": "queued" } }, "required": ["state"] },
++ "then": { "properties": { "placement": { "type": "null" }, "exit": { "type": "null" }, "timings": { "type": "object", "properties": { "placed_at": { "type": "null" }, "started_at": { "type": "null" }, "finished_at": { "type": "null" } } } } }
++ },
++ {
++ "if": { "properties": { "state": { "enum": ["placed", "running"] } }, "required": ["state"] },
++ "then": { "properties": { "placement": { "type": "object" }, "exit": { "type": "null" }, "timings": { "type": "object", "properties": { "placed_at": { "type": "string" }, "finished_at": { "type": "null" } } } } }
++ },
++ {
++ "if": { "properties": { "state": { "const": "running" } }, "required": ["state"] },
++ "then": { "properties": { "timings": { "type": "object", "properties": { "started_at": { "type": "string" } } } } }
++ },
++ {
++ "if": { "properties": { "state": { "enum": ["succeeded", "failed", "cancelled", "expired"] } }, "required": ["state"] },
++ "then": { "properties": { "timings": { "type": "object", "properties": { "finished_at": { "type": "string" } } } } }
++ },
++ {
++ "if": { "properties": { "state": { "const": "succeeded" } }, "required": ["state"] },
++ "then": { "properties": { "placement": { "type": "object" }, "exit": { "type": "object", "properties": { "reason": { "const": "exited" }, "code": { "const": 0 } } }, "timings": { "type": "object", "properties": { "started_at": { "type": "string" } } } } }
++ },
++ {
++ "if": { "properties": { "state": { "enum": ["failed", "expired"] } }, "required": ["state"] },
++ "then": { "properties": { "error": { "type": "object" } } },
++ "else": { "properties": { "error": { "type": "null" } } }
++ },
++ {
++ "if": { "properties": { "state": { "const": "succeeded" } }, "required": ["state"] },
++ "else": { "properties": { "result": { "type": "null" } } }
++ },
++ {
++ "if": { "properties": { "class": { "enum": ["function", "code"] } }, "required": ["class"] },
++ "then": { "properties": { "artifact": { "type": "object" } } }
++ }
++ ]
++}
ddiff --git a/contracts/run/job-stream-event.v1.json b/contracts/run/job-stream-event.v1.json
nnew file mode 100644
iindex 00000000..96c89da2
---- /dev/null
++++ b/contracts/run/job-stream-event.v1.json
@@@ -0,0 +1,55 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/job-stream-event.v1.json",
++ "title": "RunJobStreamEvent",
++ "description": "run.job-stream-event@1 (PLANNED; plan T14 R1): the JSON `data` of one server-sent event on GET /api/v1/jobs/{id}/stream (capability run.job.stream). The SSE `event` field is the type and the SSE `id` is `seq`, so a reconnect with Last-Event-ID resumes after it. output: a chunk of the job's stdout (platform.job-frame@1 job_stdout, best effort: a chunk lost on the node's link is never resent, and Run keeps at most the last 1 MiB to replay). state: the job moved to a new state. end: the job reached its end state; the server closes the stream after it. A stream opened on an ended job replays what is kept, then end. Output is never metered and never in an event.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "type",
++ "job_id",
++ "seq",
++ "at"
++ ],
++ "properties": {
++ "type": {
++ "enum": ["output", "state", "end"]
++ },
++ "job_id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "The job's own stream counter from 1; the SSE id."
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "chunk": {
++ "type": "string",
++ "maxLength": 65536,
++ "description": "output only: UTF-8 text."
++ },
++ "state": {
++ "enum": ["queued", "placed", "running", "succeeded", "failed", "cancelled", "expired"],
++ "description": "state and end: the job's state."
++ }
++ },
++ "allOf": [
++ {
++ "if": { "properties": { "type": { "const": "output" } }, "required": ["type"] },
++ "then": { "required": ["chunk"], "not": { "required": ["state"] } }
++ },
++ {
++ "if": { "properties": { "type": { "enum": ["state", "end"] } }, "required": ["type"] },
++ "then": { "required": ["state"], "not": { "required": ["chunk"] } }
++ },
++ {
++ "if": { "properties": { "type": { "const": "end" } }, "required": ["type"] },
++ "then": { "properties": { "state": { "enum": ["succeeded", "failed", "cancelled", "expired"] } } }
++ }
++ ]
++}
ddiff --git a/contracts/run/job-stream-ticket-result.v1.json b/contracts/run/job-stream-ticket-result.v1.json
nnew file mode 100644
iindex 00000000..5ab725f6
---- /dev/null
++++ b/contracts/run/job-stream-ticket-result.v1.json
@@@ -0,0 +1,42 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/job-stream-ticket-result.v1.json",
++ "title": "RunJobStreamTicketResult",
++ "description": "run.job-stream-ticket-result@1 (PLANNED; plan T14 R1): the answer of POST /api/v1/jobs/{id}/stream/ticket on OpenVibe.Run (capability run.job.stream), modelled on Events' realtime ticket (network.realtime-ticket-result@1). A browser cannot put a header on an EventSource, so it opens `${stream_url}?ticket=${ticket}` (plus Last-Event-ID when it resumes) at once. The ticket is a two-minute RS256 JWT that Run signs: audience openvibe.run, typ run-stream (so it is never a session token), it names this one job, and Run refuses a jti it has already seen while the ticket is valid, so a ticket opens one stream; ask for a fresh one for every reconnect. Never logged, never stored. A server caller may instead send its Bearer token to GET /api/v1/jobs/{id}/stream. The stream is text/event-stream of run.job-stream-event@1.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "ticket",
++ "expires_at",
++ "expires_in",
++ "stream_url",
++ "job_id"
++ ],
++ "properties": {
++ "ticket": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$",
++ "maxLength": 4096,
++ "description": "The signed ticket (a compact JWS)."
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "expires_in": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 300,
++ "description": "Seconds until the ticket expires (120)."
++ },
++ "stream_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://[^?#]+/api/v1/jobs/job_[0-9A-HJKMNP-TV-Z]{26}/stream$"
++ },
++ "job_id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++}
ddiff --git a/fixtures/run.job-create-request/invalid/function-without-artifact.json b/fixtures/run.job-create-request/invalid/function-without-artifact.json
nnew file mode 100644
iindex 00000000..6b75c8dc
---- /dev/null
++++ b/fixtures/run.job-create-request/invalid/function-without-artifact.json
@@@ -0,0 +1,8 @@
++{
++ "class": "function",
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ }
++}
ddiff --git a/fixtures/run.job-create-request/invalid/input-url.json b/fixtures/run.job-create-request/invalid/input-url.json
nnew file mode 100644
iindex 00000000..86ec0315
---- /dev/null
++++ b/fixtures/run.job-create-request/invalid/input-url.json
@@@ -0,0 +1,27 @@
++{
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "args": {
++ "width": 320
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113,
++ "url": "https://example.com/photo.jpg"
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "idempotency_key": "thumb:photo-7"
++}
ddiff --git a/fixtures/run.job-create-request/invalid/input-without-digest.json b/fixtures/run.job-create-request/invalid/input-without-digest.json
nnew file mode 100644
iindex 00000000..a6cbccb6
---- /dev/null
++++ b/fixtures/run.job-create-request/invalid/input-without-digest.json
@@@ -0,0 +1,24 @@
++{
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "args": {
++ "width": 320
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS"
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "idempotency_key": "thumb:photo-7"
++}
ddiff --git a/fixtures/run.job-create-request/invalid/limits-without-memory.json b/fixtures/run.job-create-request/invalid/limits-without-memory.json
nnew file mode 100644
iindex 00000000..4fbfb83a
---- /dev/null
++++ b/fixtures/run.job-create-request/invalid/limits-without-memory.json
@@@ -0,0 +1,25 @@
++{
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "args": {
++ "width": 320
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "idempotency_key": "thumb:photo-7"
++}
ddiff --git a/fixtures/run.job-create-request/invalid/unknown-egress.json b/fixtures/run.job-create-request/invalid/unknown-egress.json
nnew file mode 100644
iindex 00000000..bd55f975
---- /dev/null
++++ b/fixtures/run.job-create-request/invalid/unknown-egress.json
@@@ -0,0 +1,26 @@
++{
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "args": {
++ "width": 320
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "lan",
++ "idempotency_key": "thumb:photo-7"
++}
ddiff --git a/fixtures/run.job-create-request/invalid/version-range.json b/fixtures/run.job-create-request/invalid/version-range.json
nnew file mode 100644
iindex 00000000..8b82d82a
---- /dev/null
++++ b/fixtures/run.job-create-request/invalid/version-range.json
@@@ -0,0 +1,26 @@
++{
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "^1.2"
++ },
++ "args": {
++ "width": 320
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "idempotency_key": "thumb:photo-7"
++}
ddiff --git a/fixtures/run.job-create-request/valid/code-public-egress.json b/fixtures/run.job-create-request/valid/code-public-egress.json
nnew file mode 100644
iindex 00000000..117ad2c1
---- /dev/null
++++ b/fixtures/run.job-create-request/valid/code-public-egress.json
@@@ -0,0 +1,26 @@
++{
++ "class": "code",
++ "artifact": {
++ "name": "py-runner",
++ "version": "3.12.1"
++ },
++ "args": {
++ "entry": "main.py"
++ },
++ "limits": {
++ "wall_ms": 600000,
++ "cpu_ms": 600000,
++ "mem_bytes": 1073741824
++ },
++ "egress": "public",
++ "requirements": {
++ "kind": "run.code",
++ "mobility": "job",
++ "latency_class": "background",
++ "objective": "cheapest",
++ "trust": [
++ "first-party",
++ "user-owned"
++ ]
++ }
++}
ddiff --git a/fixtures/run.job-create-request/valid/function-with-input.json b/fixtures/run.job-create-request/valid/function-with-input.json
nnew file mode 100644
iindex 00000000..ade99a4c
---- /dev/null
++++ b/fixtures/run.job-create-request/valid/function-with-input.json
@@@ -0,0 +1,26 @@
++{
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "args": {
++ "width": 320
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "idempotency_key": "thumb:photo-7"
++}
ddiff --git a/fixtures/run.job-create-result/invalid/without-created.json b/fixtures/run.job-create-result/invalid/without-created.json
nnew file mode 100644
iindex 00000000..0679da2f
---- /dev/null
++++ b/fixtures/run.job-create-result/invalid/without-created.json
@@@ -0,0 +1,54 @@
++{
++ "job": {
++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "requirements": {
++ "kind": "run.function",
++ "mobility": "job",
++ "latency_class": "background",
++ "objective": "balanced"
++ },
++ "idempotency_key": "thumb:photo-7",
++ "state": "queued",
++ "placement": null,
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": null,
++ "started_at": null,
++ "finished_at": null
++ },
++ "exit": null,
++ "result": null,
++ "usage": {
++ "seconds": 0,
++ "wall_ms": 0,
++ "cpu_ms": null,
++ "mem_peak_bytes": null
++ },
++ "error": null
++ }
++}
ddiff --git a/fixtures/run.job-create-result/valid/created.json b/fixtures/run.job-create-result/valid/created.json
nnew file mode 100644
iindex 00000000..53e64d04
---- /dev/null
++++ b/fixtures/run.job-create-result/valid/created.json
@@@ -0,0 +1,55 @@
++{
++ "created": true,
++ "job": {
++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "requirements": {
++ "kind": "run.function",
++ "mobility": "job",
++ "latency_class": "background",
++ "objective": "balanced"
++ },
++ "idempotency_key": "thumb:photo-7",
++ "state": "queued",
++ "placement": null,
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": null,
++ "started_at": null,
++ "finished_at": null
++ },
++ "exit": null,
++ "result": null,
++ "usage": {
++ "seconds": 0,
++ "wall_ms": 0,
++ "cpu_ms": null,
++ "mem_peak_bytes": null
++ },
++ "error": null
++ }
++}
ddiff --git a/fixtures/run.job-create-result/valid/idempotent-repeat.json b/fixtures/run.job-create-result/valid/idempotent-repeat.json
nnew file mode 100644
iindex 00000000..52e42241
---- /dev/null
++++ b/fixtures/run.job-create-result/valid/idempotent-repeat.json
@@@ -0,0 +1,59 @@
++{
++ "created": false,
++ "job": {
++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "requirements": {
++ "kind": "run.function",
++ "mobility": "job",
++ "latency_class": "background",
++ "objective": "balanced"
++ },
++ "idempotency_key": "thumb:photo-7",
++ "state": "running",
++ "placement": {
++ "node": "dev_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "provider": null,
++ "region": "us-west"
++ },
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": "2026-10-04T10:00:00.400Z",
++ "started_at": "2026-10-04T10:00:00.650Z",
++ "finished_at": null
++ },
++ "exit": null,
++ "result": null,
++ "usage": {
++ "seconds": 0,
++ "wall_ms": 0,
++ "cpu_ms": null,
++ "mem_peak_bytes": null
++ },
++ "error": null
++ }
++}
ddiff --git a/fixtures/run.job-list-query/invalid/limit-too-high.json b/fixtures/run.job-list-query/invalid/limit-too-high.json
nnew file mode 100644
iindex 00000000..743006c6
---- /dev/null
++++ b/fixtures/run.job-list-query/invalid/limit-too-high.json
@@@ -0,0 +1,3 @@
++{
++ "limit": "201"
++}
ddiff --git a/fixtures/run.job-list-query/invalid/unknown-state.json b/fixtures/run.job-list-query/invalid/unknown-state.json
nnew file mode 100644
iindex 00000000..e65f858f
---- /dev/null
++++ b/fixtures/run.job-list-query/invalid/unknown-state.json
@@@ -0,0 +1,3 @@
++{
++ "state": "done"
++}
ddiff --git a/fixtures/run.job-list-query/valid/admin-project.json b/fixtures/run.job-list-query/valid/admin-project.json
nnew file mode 100644
iindex 00000000..89a16057
---- /dev/null
++++ b/fixtures/run.job-list-query/valid/admin-project.json
@@@ -0,0 +1,4 @@
++{
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "limit": 200
++}
ddiff --git a/fixtures/run.job-list-query/valid/running-page.json b/fixtures/run.job-list-query/valid/running-page.json
nnew file mode 100644
iindex 00000000..7eaf95a2
---- /dev/null
++++ b/fixtures/run.job-list-query/valid/running-page.json
@@@ -0,0 +1,5 @@
++{
++ "state": "running",
++ "limit": "50",
++ "cursor": "eyJpZCI6ImpvYl8wMSJ9"
++}
ddiff --git a/fixtures/run.job-list-result/invalid/missing-cursor.json b/fixtures/run.job-list-result/invalid/missing-cursor.json
nnew file mode 100644
iindex 00000000..df68d7bf
---- /dev/null
++++ b/fixtures/run.job-list-result/invalid/missing-cursor.json
@@@ -0,0 +1,3 @@
++{
++ "jobs": []
++}
ddiff --git a/fixtures/run.job-list-result/valid/empty-last-page.json b/fixtures/run.job-list-result/valid/empty-last-page.json
nnew file mode 100644
iindex 00000000..4952f904
---- /dev/null
++++ b/fixtures/run.job-list-result/valid/empty-last-page.json
@@@ -0,0 +1,4 @@
++{
++ "jobs": [],
++ "next_cursor": null
++}
ddiff --git a/fixtures/run.job-list-result/valid/page.json b/fixtures/run.job-list-result/valid/page.json
nnew file mode 100644
iindex 00000000..282a532d
---- /dev/null
++++ b/fixtures/run.job-list-result/valid/page.json
@@@ -0,0 +1,118 @@
++{
++ "jobs": [
++ {
++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "requirements": {
++ "kind": "run.function",
++ "mobility": "job",
++ "latency_class": "background",
++ "objective": "balanced"
++ },
++ "idempotency_key": "thumb:photo-7",
++ "state": "succeeded",
++ "placement": {
++ "node": "dev_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "provider": null,
++ "region": "us-west"
++ },
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": "2026-10-04T10:00:00.400Z",
++ "started_at": "2026-10-04T10:00:00.650Z",
++ "finished_at": "2026-10-04T10:00:03.000Z"
++ },
++ "exit": {
++ "reason": "exited",
++ "code": 0
++ },
++ "result": {
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPW"
++ },
++ "usage": {
++ "seconds": 2.35,
++ "wall_ms": 2350,
++ "cpu_ms": 1800,
++ "mem_peak_bytes": 41943040
++ },
++ "error": null
++ },
++ {
++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "requirements": {
++ "kind": "run.function",
++ "mobility": "job",
++ "latency_class": "background",
++ "objective": "balanced"
++ },
++ "idempotency_key": "thumb:photo-7",
++ "state": "queued",
++ "placement": null,
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": null,
++ "started_at": null,
++ "finished_at": null
++ },
++ "exit": null,
++ "result": null,
++ "usage": {
++ "seconds": 0,
++ "wall_ms": 0,
++ "cpu_ms": null,
++ "mem_peak_bytes": null
++ },
++ "error": null
++ }
++ ],
++ "next_cursor": "eyJpZCI6ImpvYl8wMSJ9"
++}
ddiff --git a/fixtures/run.job-read-result/invalid/failed-without-error.json b/fixtures/run.job-read-result/invalid/failed-without-error.json
nnew file mode 100644
iindex 00000000..c718dbd7
---- /dev/null
++++ b/fixtures/run.job-read-result/invalid/failed-without-error.json
@@@ -0,0 +1,59 @@
++{
++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "requirements": {
++ "kind": "run.function",
++ "mobility": "job",
++ "latency_class": "background",
++ "objective": "balanced"
++ },
++ "idempotency_key": "thumb:photo-7",
++ "state": "failed",
++ "placement": {
++ "node": "dev_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "provider": null,
++ "region": "us-west"
++ },
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": "2026-10-04T10:00:00.400Z",
++ "started_at": "2026-10-04T10:00:00.650Z",
++ "finished_at": "2026-10-04T10:00:03.000Z"
++ },
++ "exit": {
++ "reason": "limit",
++ "code": null
++ },
++ "result": null,
++ "usage": {
++ "seconds": 2.35,
++ "wall_ms": 2350,
++ "cpu_ms": 1800,
++ "mem_peak_bytes": 41943040
++ },
++ "error": null
++}
ddiff --git a/fixtures/run.job-read-result/invalid/queued-with-result.json b/fixtures/run.job-read-result/invalid/queued-with-result.json
nnew file mode 100644
iindex 00000000..daeae075
---- /dev/null
++++ b/fixtures/run.job-read-result/invalid/queued-with-result.json
@@@ -0,0 +1,54 @@
++{
++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "requirements": {
++ "kind": "run.function",
++ "mobility": "job",
++ "latency_class": "background",
++ "objective": "balanced"
++ },
++ "idempotency_key": "thumb:photo-7",
++ "state": "queued",
++ "placement": null,
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": null,
++ "started_at": null,
++ "finished_at": null
++ },
++ "exit": null,
++ "result": {
++ "ok": true
++ },
++ "usage": {
++ "seconds": 0,
++ "wall_ms": 0,
++ "cpu_ms": null,
++ "mem_peak_bytes": null
++ },
++ "error": null
++}
ddiff --git a/fixtures/run.job-read-result/invalid/running-without-placement.json b/fixtures/run.job-read-result/invalid/running-without-placement.json
nnew file mode 100644
iindex 00000000..6f8d4acd
---- /dev/null
++++ b/fixtures/run.job-read-result/invalid/running-without-placement.json
@@@ -0,0 +1,52 @@
++{
++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "requirements": {
++ "kind": "run.function",
++ "mobility": "job",
++ "latency_class": "background",
++ "objective": "balanced"
++ },
++ "idempotency_key": "thumb:photo-7",
++ "state": "running",
++ "placement": null,
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": "2026-10-04T10:00:00.400Z",
++ "started_at": "2026-10-04T10:00:00.650Z",
++ "finished_at": null
++ },
++ "exit": null,
++ "result": null,
++ "usage": {
++ "seconds": 0,
++ "wall_ms": 0,
++ "cpu_ms": null,
++ "mem_peak_bytes": null
++ },
++ "error": null
++}
ddiff --git a/fixtures/run.job-read-result/invalid/succeeded-nonzero-exit.json b/fixtures/run.job-read-result/invalid/succeeded-nonzero-exit.json
nnew file mode 100644
iindex 00000000..5898811b
---- /dev/null
++++ b/fixtures/run.job-read-result/invalid/succeeded-nonzero-exit.json
@@@ -0,0 +1,61 @@
++{
++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "requirements": {
++ "kind": "run.function",
++ "mobility": "job",
++ "latency_class": "background",
++ "objective": "balanced"
++ },
++ "idempotency_key": "thumb:photo-7",
++ "state": "succeeded",
++ "placement": {
++ "node": "dev_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "provider": null,
++ "region": "us-west"
++ },
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": "2026-10-04T10:00:00.400Z",
++ "started_at": "2026-10-04T10:00:00.650Z",
++ "finished_at": "2026-10-04T10:00:03.000Z"
++ },
++ "exit": {
++ "reason": "exited",
++ "code": 1
++ },
++ "result": {
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPW"
++ },
++ "usage": {
++ "seconds": 2.35,
++ "wall_ms": 2350,
++ "cpu_ms": 1800,
++ "mem_peak_bytes": 41943040
++ },
++ "error": null
++}
ddiff --git a/fixtures/run.job-read-result/invalid/unknown-state.json b/fixtures/run.job-read-result/invalid/unknown-state.json
nnew file mode 100644
iindex 00000000..ea215aea
---- /dev/null
++++ b/fixtures/run.job-read-result/invalid/unknown-state.json
@@@ -0,0 +1,52 @@
++{
++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "requirements": {
++ "kind": "run.function",
++ "mobility": "job",
++ "latency_class": "background",
++ "objective": "balanced"
++ },
++ "idempotency_key": "thumb:photo-7",
++ "state": "paused",
++ "placement": null,
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": null,
++ "started_at": null,
++ "finished_at": null
++ },
++ "exit": null,
++ "result": null,
++ "usage": {
++ "seconds": 0,
++ "wall_ms": 0,
++ "cpu_ms": null,
++ "mem_peak_bytes": null
++ },
++ "error": null
++}
ddiff --git a/fixtures/run.job-read-result/valid/expired-unplaceable.json b/fixtures/run.job-read-result/valid/expired-unplaceable.json
nnew file mode 100644
iindex 00000000..5c661770
---- /dev/null
++++ b/fixtures/run.job-read-result/valid/expired-unplaceable.json
@@@ -0,0 +1,55 @@
++{
++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "requirements": {
++ "kind": "run.function",
++ "mobility": "job",
++ "latency_class": "background",
++ "objective": "balanced"
++ },
++ "idempotency_key": "thumb:photo-7",
++ "state": "expired",
++ "placement": null,
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": null,
++ "started_at": null,
++ "finished_at": "2026-10-04T10:10:30.000Z"
++ },
++ "exit": null,
++ "result": null,
++ "usage": {
++ "seconds": 0,
++ "wall_ms": 0,
++ "cpu_ms": null,
++ "mem_peak_bytes": null
++ },
++ "error": {
++ "code": "run.job.unplaceable",
++ "detail": "no node offers worker:function with egress none"
++ }
++}
ddiff --git a/fixtures/run.job-read-result/valid/failed-limit.json b/fixtures/run.job-read-result/valid/failed-limit.json
nnew file mode 100644
iindex 00000000..9a7d1c1b
---- /dev/null
++++ b/fixtures/run.job-read-result/valid/failed-limit.json
@@@ -0,0 +1,62 @@
++{
++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "requirements": {
++ "kind": "run.function",
++ "mobility": "job",
++ "latency_class": "background",
++ "objective": "balanced"
++ },
++ "idempotency_key": "thumb:photo-7",
++ "state": "failed",
++ "placement": {
++ "node": "dev_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "provider": null,
++ "region": "us-west"
++ },
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": "2026-10-04T10:00:00.400Z",
++ "started_at": "2026-10-04T10:00:00.650Z",
++ "finished_at": "2026-10-04T10:00:03.000Z"
++ },
++ "exit": {
++ "reason": "limit",
++ "code": null
++ },
++ "result": null,
++ "usage": {
++ "seconds": 2.35,
++ "wall_ms": 2350,
++ "cpu_ms": 1800,
++ "mem_peak_bytes": 41943040
++ },
++ "error": {
++ "code": "run.job.limit",
++ "detail": "mem_bytes exceeded"
++ }
++}
ddiff --git a/fixtures/run.job-read-result/valid/queued.json b/fixtures/run.job-read-result/valid/queued.json
nnew file mode 100644
iindex 00000000..43adc548
---- /dev/null
++++ b/fixtures/run.job-read-result/valid/queued.json
@@@ -0,0 +1,52 @@
++{
++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "requirements": {
++ "kind": "run.function",
++ "mobility": "job",
++ "latency_class": "background",
++ "objective": "balanced"
++ },
++ "idempotency_key": "thumb:photo-7",
++ "state": "queued",
++ "placement": null,
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": null,
++ "started_at": null,
++ "finished_at": null
++ },
++ "exit": null,
++ "result": null,
++ "usage": {
++ "seconds": 0,
++ "wall_ms": 0,
++ "cpu_ms": null,
++ "mem_peak_bytes": null
++ },
++ "error": null
++}
ddiff --git a/fixtures/run.job-read-result/valid/running.json b/fixtures/run.job-read-result/valid/running.json
nnew file mode 100644
iindex 00000000..05489398
---- /dev/null
++++ b/fixtures/run.job-read-result/valid/running.json
@@@ -0,0 +1,56 @@
++{
++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "requirements": {
++ "kind": "run.function",
++ "mobility": "job",
++ "latency_class": "background",
++ "objective": "balanced"
++ },
++ "idempotency_key": "thumb:photo-7",
++ "state": "running",
++ "placement": {
++ "node": "dev_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "provider": null,
++ "region": "us-west"
++ },
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": "2026-10-04T10:00:00.400Z",
++ "started_at": "2026-10-04T10:00:00.650Z",
++ "finished_at": null
++ },
++ "exit": null,
++ "result": null,
++ "usage": {
++ "seconds": 0,
++ "wall_ms": 0,
++ "cpu_ms": null,
++ "mem_peak_bytes": null
++ },
++ "error": null
++}
ddiff --git a/fixtures/run.job-read-result/valid/succeeded.json b/fixtures/run.job-read-result/valid/succeeded.json
nnew file mode 100644
iindex 00000000..5f92bce7
---- /dev/null
++++ b/fixtures/run.job-read-result/valid/succeeded.json
@@@ -0,0 +1,61 @@
++{
++ "id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "ttl_ms": 630000,
++ "egress": "none",
++ "requirements": {
++ "kind": "run.function",
++ "mobility": "job",
++ "latency_class": "background",
++ "objective": "balanced"
++ },
++ "idempotency_key": "thumb:photo-7",
++ "state": "succeeded",
++ "placement": {
++ "node": "dev_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "provider": null,
++ "region": "us-west"
++ },
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": "2026-10-04T10:00:00.400Z",
++ "started_at": "2026-10-04T10:00:00.650Z",
++ "finished_at": "2026-10-04T10:00:03.000Z"
++ },
++ "exit": {
++ "reason": "exited",
++ "code": 0
++ },
++ "result": {
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPW"
++ },
++ "usage": {
++ "seconds": 2.35,
++ "wall_ms": 2350,
++ "cpu_ms": 1800,
++ "mem_peak_bytes": 41943040
++ },
++ "error": null
++}
ddiff --git a/fixtures/run.job-stream-event/invalid/end-while-running.json b/fixtures/run.job-stream-event/invalid/end-while-running.json
nnew file mode 100644
iindex 00000000..abb21114
---- /dev/null
++++ b/fixtures/run.job-stream-event/invalid/end-while-running.json
@@@ -0,0 +1,7 @@
++{
++ "type": "end",
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "seq": 4,
++ "at": "2026-10-04T10:00:03.000Z",
++ "state": "running"
++}
ddiff --git a/fixtures/run.job-stream-event/invalid/output-without-chunk.json b/fixtures/run.job-stream-event/invalid/output-without-chunk.json
nnew file mode 100644
iindex 00000000..f5f61e5d
---- /dev/null
++++ b/fixtures/run.job-stream-event/invalid/output-without-chunk.json
@@@ -0,0 +1,6 @@
++{
++ "type": "output",
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "seq": 3,
++ "at": "2026-10-04T10:00:01.000Z"
++}
ddiff --git a/fixtures/run.job-stream-event/valid/end.json b/fixtures/run.job-stream-event/valid/end.json
nnew file mode 100644
iindex 00000000..a6755030
---- /dev/null
++++ b/fixtures/run.job-stream-event/valid/end.json
@@@ -0,0 +1,7 @@
++{
++ "type": "end",
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "seq": 4,
++ "at": "2026-10-04T10:00:03.000Z",
++ "state": "succeeded"
++}
ddiff --git a/fixtures/run.job-stream-event/valid/output.json b/fixtures/run.job-stream-event/valid/output.json
nnew file mode 100644
iindex 00000000..4bf2df9d
---- /dev/null
++++ b/fixtures/run.job-stream-event/valid/output.json
@@@ -0,0 +1,7 @@
++{
++ "type": "output",
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "seq": 3,
++ "at": "2026-10-04T10:00:01.000Z",
++ "chunk": "resized 1 of 1\n"
++}
ddiff --git a/fixtures/run.job-stream-event/valid/state.json b/fixtures/run.job-stream-event/valid/state.json
nnew file mode 100644
iindex 00000000..78af7f3f
---- /dev/null
++++ b/fixtures/run.job-stream-event/valid/state.json
@@@ -0,0 +1,7 @@
++{
++ "type": "state",
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "seq": 2,
++ "at": "2026-10-04T10:00:00.650Z",
++ "state": "running"
++}
ddiff --git a/fixtures/run.job-stream-ticket-result/invalid/long-lived.json b/fixtures/run.job-stream-ticket-result/invalid/long-lived.json
nnew file mode 100644
iindex 00000000..0fb841f7
---- /dev/null
++++ b/fixtures/run.job-stream-ticket-result/invalid/long-lived.json
@@@ -0,0 +1,7 @@
++{
++ "ticket": "sample-ticket.not-a-jwt.placeholder",
++ "expires_at": "2026-10-04T10:02:00.000Z",
++ "expires_in": 3600,
++ "stream_url": "https://openvibe.run/api/v1/jobs/job_01JAB2C3D4E5F6G7H8J9K0MNPQ/stream",
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++}
ddiff --git a/fixtures/run.job-stream-ticket-result/invalid/other-stream-url.json b/fixtures/run.job-stream-ticket-result/invalid/other-stream-url.json
nnew file mode 100644
iindex 00000000..eedca4a3
---- /dev/null
++++ b/fixtures/run.job-stream-ticket-result/invalid/other-stream-url.json
@@@ -0,0 +1,7 @@
++{
++ "ticket": "sample-ticket.not-a-jwt.placeholder",
++ "expires_at": "2026-10-04T10:02:00.000Z",
++ "expires_in": 120,
++ "stream_url": "https://openvibe.run/api/v1/jobs",
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++}
ddiff --git a/fixtures/run.job-stream-ticket-result/valid/ticket.json b/fixtures/run.job-stream-ticket-result/valid/ticket.json
nnew file mode 100644
iindex 00000000..ff56ba79
---- /dev/null
++++ b/fixtures/run.job-stream-ticket-result/valid/ticket.json
@@@ -0,0 +1,7 @@
++{
++ "ticket": "sample-ticket.not-a-jwt.placeholder",
++ "expires_at": "2026-10-04T10:02:00.000Z",
++ "expires_in": 120,
++ "stream_url": "https://openvibe.run/api/v1/jobs/job_01JAB2C3D4E5F6G7H8J9K0MNPQ/stream",
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++}
ddiff --git a/fixtures/run.job.cancelled/invalid/with-error.json b/fixtures/run.job.cancelled/invalid/with-error.json
nnew file mode 100644
iindex 00000000..5a8f0ebf
---- /dev/null
++++ b/fixtures/run.job.cancelled/invalid/with-error.json
@@@ -0,0 +1,40 @@
++{
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "state": "cancelled",
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "egress": "none",
++ "placement": {
++ "node": "dev_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "provider": null,
++ "region": "us-west"
++ },
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": "2026-10-04T10:00:00.400Z",
++ "started_at": "2026-10-04T10:00:00.650Z",
++ "finished_at": "2026-10-04T10:00:03.000Z"
++ },
++ "exit": {
++ "reason": "cancelled",
++ "code": null
++ },
++ "usage": {
++ "seconds": 2.35,
++ "wall_ms": 2350,
++ "cpu_ms": 1800,
++ "mem_peak_bytes": 41943040
++ },
++ "error": {
++ "code": "run.job.limit",
++ "detail": null
++ }
++}
ddiff --git a/fixtures/run.job.cancelled/valid/before-placement.json b/fixtures/run.job.cancelled/valid/before-placement.json
nnew file mode 100644
iindex 00000000..e8f74e5c
---- /dev/null
++++ b/fixtures/run.job.cancelled/valid/before-placement.json
@@@ -0,0 +1,30 @@
++{
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "state": "cancelled",
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "egress": "none",
++ "placement": null,
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": null,
++ "started_at": null,
++ "finished_at": "2026-10-04T10:00:05.000Z"
++ },
++ "exit": null,
++ "usage": {
++ "seconds": 0,
++ "wall_ms": 0,
++ "cpu_ms": null,
++ "mem_peak_bytes": null
++ },
++ "error": null
++}
ddiff --git a/fixtures/run.job.cancelled/valid/while-running.json b/fixtures/run.job.cancelled/valid/while-running.json
nnew file mode 100644
iindex 00000000..dcb50c63
---- /dev/null
++++ b/fixtures/run.job.cancelled/valid/while-running.json
@@@ -0,0 +1,37 @@
++{
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "state": "cancelled",
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "egress": "none",
++ "placement": {
++ "node": "dev_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "provider": null,
++ "region": "us-west"
++ },
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": "2026-10-04T10:00:00.400Z",
++ "started_at": "2026-10-04T10:00:00.650Z",
++ "finished_at": "2026-10-04T10:00:03.000Z"
++ },
++ "exit": {
++ "reason": "cancelled",
++ "code": null
++ },
++ "usage": {
++ "seconds": 2.35,
++ "wall_ms": 2350,
++ "cpu_ms": 1800,
++ "mem_peak_bytes": 41943040
++ },
++ "error": null
++}
ddiff --git a/fixtures/run.job.expired/invalid/still-open.json b/fixtures/run.job.expired/invalid/still-open.json
nnew file mode 100644
iindex 00000000..2414b285
---- /dev/null
++++ b/fixtures/run.job.expired/invalid/still-open.json
@@@ -0,0 +1,33 @@
++{
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "state": "expired",
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "egress": "none",
++ "placement": null,
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": null,
++ "started_at": null,
++ "finished_at": null
++ },
++ "exit": null,
++ "usage": {
++ "seconds": 0,
++ "wall_ms": 0,
++ "cpu_ms": null,
++ "mem_peak_bytes": null
++ },
++ "error": {
++ "code": "run.job.unplaceable",
++ "detail": "no node offers worker:function with egress none"
++ }
++}
ddiff --git a/fixtures/run.job.expired/valid/unplaceable.json b/fixtures/run.job.expired/valid/unplaceable.json
nnew file mode 100644
iindex 00000000..4fed3027
---- /dev/null
++++ b/fixtures/run.job.expired/valid/unplaceable.json
@@@ -0,0 +1,33 @@
++{
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "state": "expired",
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "egress": "none",
++ "placement": null,
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": null,
++ "started_at": null,
++ "finished_at": "2026-10-04T10:10:30.000Z"
++ },
++ "exit": null,
++ "usage": {
++ "seconds": 0,
++ "wall_ms": 0,
++ "cpu_ms": null,
++ "mem_peak_bytes": null
++ },
++ "error": {
++ "code": "run.job.unplaceable",
++ "detail": "no node offers worker:function with egress none"
++ }
++}
ddiff --git a/fixtures/run.job.failed/invalid/without-error.json b/fixtures/run.job.failed/invalid/without-error.json
nnew file mode 100644
iindex 00000000..4fea5857
---- /dev/null
++++ b/fixtures/run.job.failed/invalid/without-error.json
@@@ -0,0 +1,37 @@
++{
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "state": "failed",
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "egress": "none",
++ "placement": {
++ "node": "dev_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "provider": null,
++ "region": "us-west"
++ },
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": "2026-10-04T10:00:00.400Z",
++ "started_at": "2026-10-04T10:00:00.650Z",
++ "finished_at": "2026-10-04T10:00:03.000Z"
++ },
++ "exit": {
++ "reason": "limit",
++ "code": null
++ },
++ "usage": {
++ "seconds": 2.35,
++ "wall_ms": 2350,
++ "cpu_ms": 1800,
++ "mem_peak_bytes": 41943040
++ },
++ "error": null
++}
ddiff --git a/fixtures/run.job.failed/valid/limit.json b/fixtures/run.job.failed/valid/limit.json
nnew file mode 100644
iindex 00000000..dfb5fcaa
---- /dev/null
++++ b/fixtures/run.job.failed/valid/limit.json
@@@ -0,0 +1,40 @@
++{
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "state": "failed",
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "egress": "none",
++ "placement": {
++ "node": "dev_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "provider": null,
++ "region": "us-west"
++ },
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": "2026-10-04T10:00:00.400Z",
++ "started_at": "2026-10-04T10:00:00.650Z",
++ "finished_at": "2026-10-04T10:00:03.000Z"
++ },
++ "exit": {
++ "reason": "limit",
++ "code": null
++ },
++ "usage": {
++ "seconds": 2.35,
++ "wall_ms": 2350,
++ "cpu_ms": 1800,
++ "mem_peak_bytes": 41943040
++ },
++ "error": {
++ "code": "run.job.limit",
++ "detail": "mem_bytes exceeded"
++ }
++}
ddiff --git a/fixtures/run.job.queued/invalid/finished.json b/fixtures/run.job.queued/invalid/finished.json
nnew file mode 100644
iindex 00000000..1943d952
---- /dev/null
++++ b/fixtures/run.job.queued/invalid/finished.json
@@@ -0,0 +1,30 @@
++{
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "state": "queued",
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "egress": "none",
++ "placement": null,
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": "2026-10-04T10:00:00.400Z",
++ "started_at": "2026-10-04T10:00:00.650Z",
++ "finished_at": "2026-10-04T10:00:03.000Z"
++ },
++ "exit": null,
++ "usage": {
++ "seconds": 0,
++ "wall_ms": 0,
++ "cpu_ms": null,
++ "mem_peak_bytes": null
++ },
++ "error": null
++}
ddiff --git a/fixtures/run.job.queued/valid/live.json b/fixtures/run.job.queued/valid/live.json
nnew file mode 100644
iindex 00000000..467bda5c
---- /dev/null
++++ b/fixtures/run.job.queued/valid/live.json
@@@ -0,0 +1,30 @@
++{
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "state": "queued",
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "egress": "none",
++ "placement": null,
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": null,
++ "started_at": null,
++ "finished_at": null
++ },
++ "exit": null,
++ "usage": {
++ "seconds": 0,
++ "wall_ms": 0,
++ "cpu_ms": null,
++ "mem_peak_bytes": null
++ },
++ "error": null
++}
ddiff --git a/fixtures/run.job.started/invalid/without-placement.json b/fixtures/run.job.started/invalid/without-placement.json
nnew file mode 100644
iindex 00000000..678ca61a
---- /dev/null
++++ b/fixtures/run.job.started/invalid/without-placement.json
@@@ -0,0 +1,30 @@
++{
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "state": "running",
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "egress": "none",
++ "placement": null,
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": "2026-10-04T10:00:00.400Z",
++ "started_at": "2026-10-04T10:00:00.650Z",
++ "finished_at": null
++ },
++ "exit": null,
++ "usage": {
++ "seconds": 0,
++ "wall_ms": 0,
++ "cpu_ms": null,
++ "mem_peak_bytes": null
++ },
++ "error": null
++}
ddiff --git a/fixtures/run.job.started/valid/running.json b/fixtures/run.job.started/valid/running.json
nnew file mode 100644
iindex 00000000..54506290
---- /dev/null
++++ b/fixtures/run.job.started/valid/running.json
@@@ -0,0 +1,34 @@
++{
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "state": "running",
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "egress": "none",
++ "placement": {
++ "node": "dev_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "provider": null,
++ "region": "us-west"
++ },
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": "2026-10-04T10:00:00.400Z",
++ "started_at": "2026-10-04T10:00:00.650Z",
++ "finished_at": null
++ },
++ "exit": null,
++ "usage": {
++ "seconds": 0,
++ "wall_ms": 0,
++ "cpu_ms": null,
++ "mem_peak_bytes": null
++ },
++ "error": null
++}
ddiff --git a/fixtures/run.job.succeeded/invalid/nonzero-exit.json b/fixtures/run.job.succeeded/invalid/nonzero-exit.json
nnew file mode 100644
iindex 00000000..0378e737
---- /dev/null
++++ b/fixtures/run.job.succeeded/invalid/nonzero-exit.json
@@@ -0,0 +1,37 @@
++{
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "state": "succeeded",
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "egress": "none",
++ "placement": {
++ "node": "dev_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "provider": null,
++ "region": "us-west"
++ },
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": "2026-10-04T10:00:00.400Z",
++ "started_at": "2026-10-04T10:00:00.650Z",
++ "finished_at": "2026-10-04T10:00:03.000Z"
++ },
++ "exit": {
++ "reason": "exited",
++ "code": 2
++ },
++ "usage": {
++ "seconds": 2.35,
++ "wall_ms": 2350,
++ "cpu_ms": 1800,
++ "mem_peak_bytes": 41943040
++ },
++ "error": null
++}
ddiff --git a/fixtures/run.job.succeeded/valid/exited-zero.json b/fixtures/run.job.succeeded/valid/exited-zero.json
nnew file mode 100644
iindex 00000000..3fbf2c13
---- /dev/null
++++ b/fixtures/run.job.succeeded/valid/exited-zero.json
@@@ -0,0 +1,37 @@
++{
++ "job_id": "job_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPV"
++ },
++ "state": "succeeded",
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "egress": "none",
++ "placement": {
++ "node": "dev_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "provider": null,
++ "region": "us-west"
++ },
++ "timings": {
++ "created_at": "2026-10-04T10:00:00.000Z",
++ "placed_at": "2026-10-04T10:00:00.400Z",
++ "started_at": "2026-10-04T10:00:00.650Z",
++ "finished_at": "2026-10-04T10:00:03.000Z"
++ },
++ "exit": {
++ "reason": "exited",
++ "code": 0
++ },
++ "usage": {
++ "seconds": 2.35,
++ "wall_ms": 2350,
++ "cpu_ms": 1800,
++ "mem_peak_bytes": 41943040
++ },
++ "error": null
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex 65b742b9..5a5a438e 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -63363,6 +63363,1701 @@
}
]
}
++ },
++ "run.job-create-request": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "planned",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/job-create-request.v1.json",
++ "title": "RunJobCreateRequest",
++ "description": "run.job-create-request@1 (PLANNED; plan T14 R1, ADR-034): the body of POST /api/v1/jobs on OpenVibe.Run (capability run.job.submit). The job belongs to the project the caller's token names. CLASS is a platform.runtime-class@1 name; Run accepts any of them but places a job only on a node that advertises worker:<class> (platform.resource-offer@1), so a class no node runs today (browser, linux, desktop, gpu) ends expired with run.job.unplaceable. ARTIFACT is the pre-registered artifact to run (name and one exact version), required for function and code, as platform.job@1 carries it. ARGS is the artifact's declared input as JSON (at most 512 KiB serialized, else 413). INPUTS are files placed in the job directory before the process starts, each an OpenVibe.Media object pinned by its sha256: a size or digest mismatch fails the job (run.job.input_digest_mismatch) before the process ever runs; Run never fetches a caller-chosen URL. LIMITS are hard caps merged stricter with the node's own (platform.job@1 limits); a request over the project's tier cap is refused 422 run.limits.exceeded, never silently lowered. EGRESS is the network the job may reach, the values of OpenVibe.Node's worker.egress: none (default; any node, sent as net deny), public or openvibe-only (only a node whose worker.egress is exactly that policy, until platform.job@1 net carries it per job). REQUIREMENTS (platform.workload-requirements@1) narrow placement; absent means { kind: run.<class>, mobility: job, latency_class: background, objective: balanced }. IDEMPOTENCY_KEY: a repeat from the same project within 24 h with an identical body answers the first job (200, created false); with a different body 409 run.idempotency.conflict.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "class",
++ "limits"
++ ],
++ "properties": {
++ "class": {
++ "$ref": "../platform/runtime-class.v1.json"
++ },
++ "artifact": {
++ "$ref": "#/$defs/artifact"
++ },
++ "args": {
++ "type": "object",
++ "description": "The artifact's declared input as JSON; default {}."
++ },
++ "inputs": {
++ "type": "array",
++ "maxItems": 32,
++ "items": {
++ "$ref": "#/$defs/input"
++ },
++ "description": "Files placed in the job directory before the process starts; names are unique (else 422)."
++ },
++ "limits": {
++ "$ref": "#/$defs/limits"
++ },
++ "ttl_ms": {
++ "type": "integer",
++ "minimum": 1000,
++ "maximum": 86400000,
++ "description": "Lifetime from creation, covering the wait for a node and the run (platform.job@1 ttl_ms); when it runs out the job ends expired. Default: limits.wall_ms plus 600000."
++ },
++ "egress": {
++ "$ref": "#/$defs/egress"
++ },
++ "requirements": {
++ "$ref": "../platform/workload-requirements.v1.json"
++ },
++ "idempotency_key": {
++ "$ref": "#/$defs/idempotency_key"
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "required": [
++ "class"
++ ],
++ "properties": {
++ "class": {
++ "enum": [
++ "function",
++ "code"
++ ]
++ }
++ }
++ },
++ "then": {
++ "required": [
++ "artifact"
++ ]
++ }
++ }
++ ],
++ "$defs": {
++ "artifact": {
++ "type": "object",
++ "additionalProperties": false,
++ "description": "The pre-registered artifact to run (platform.job@1 artifact).",
++ "required": [
++ "name",
++ "version"
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "pattern": "^[a-z0-9][a-z0-9._-]{0,127}$"
++ },
++ "version": {
++ "type": "string",
++ "pattern": "^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$",
++ "description": "One exact version, never a range"
++ }
++ }
++ },
++ "input": {
++ "type": "object",
++ "additionalProperties": false,
++ "description": "One input file: an OpenVibe.Media object the caller's project can read, pinned by digest.",
++ "required": [
++ "name",
++ "media_id",
++ "sha256"
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$",
++ "description": "File name in the job directory; no path separators."
++ },
++ "media_id": {
++ "type": "string",
++ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "sha256": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{64}$",
++ "description": "Lowercase hex SHA-256 of the object's bytes."
++ },
++ "size_bytes": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ },
++ "limits": {
++ "type": "object",
++ "additionalProperties": false,
++ "description": "Caps on the running process (platform.job@1 limits); hitting one ends the job failed with run.job.limit.",
++ "required": [
++ "wall_ms",
++ "cpu_ms",
++ "mem_bytes"
++ ],
++ "properties": {
++ "wall_ms": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Running time from `job_started`"
++ },
++ "cpu_ms": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "CPU time, user plus system"
++ },
++ "mem_bytes": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Peak resident memory"
++ }
++ }
++ },
++ "egress": {
++ "type": "string",
++ "enum": [
++ "none",
++ "public",
++ "openvibe-only"
++ ],
++ "default": "none",
++ "description": "OpenVibe.Node worker.egress values: none (no network), public (the internet without private, link-local or the network's own ranges), openvibe-only (the network's own endpoints only)."
++ },
++ "idempotency_key": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9._:-]{1,128}$"
++ }
++ },
++ "examples": [
++ {
++ "class": "function",
++ "artifact": {
++ "name": "thumbnail",
++ "version": "1.2.0"
++ },
++ "args": {
++ "width": 320
++ },
++ "inputs": [
++ {
++ "name": "photo.jpg",
++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
++ "size_bytes": 482113
++ }
++ ],
++ "limits": {
++ "wall_ms": 30000,
++ "cpu_ms": 30000,
++ "mem_bytes": 268435456
++ },
++ "egress": "none",
++ "idempotency_key": "thumb:photo-7"
++ }
++ ]
++ }
++ },
++ "run.job-create-result": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "planned",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/job-create-result.v1.json",
++ "title": "RunJobCreateResult",
++ "description": "run.job-create-result@1 (PLANNED; plan T14 R1, ADR-034): the answer of POST /api/v1/jobs on OpenVibe.Run (capability run.job.submit). 201 with created true and the new job, queued; 200 with created false and the job first created under the same idempotency_key (in whatever state it has reached). Refusals are application/problem+json: 400 (body invalid), 413 (args over 512 KiB), 422 run.limits.exceeded or a duplicate input name, 409 run.idempotency.conflict, 429 (quota).",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created",
++ "job"
++ ],
++ "properties": {
++ "created": {
++ "type": "boolean",
++ "description": "false when the idempotency_key matched an earlier job."
++ },
++ "job": {
++ "$ref": "job-read-result.v1.json"
++ }
++ }
++ }
++ },
++ "run.job-read-result": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "planned",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/job-read-result.v1.json",
++ "title": "RunJobReadResult",
++ "description": "run.job-read-result@1 (PLANNED; plan T14 R1, ADR-034): one job on OpenVibe.Run, as GET /api/v1/jobs/{id} answers it (capability run.job.read), POST /api/v1/jobs/{id}/cancel answers it after the cancel (run.job.cancel), and run.job-create-result@1 and run.job-list-result@1 carry it. STATES: queued (stored, waiting for a node) -> placed (sent to a node as platform.job@1, not yet started) -> running (the node sent job_started) -> one end state: succeeded (job_exit reason exited with code 0), failed (exited with another code, or reason limit, stopped or failed), cancelled (POST /api/v1/jobs/{id}/cancel, or job_exit reason cancelled), expired (ttl_ms ran out: before placement, before start, or while running, job_exit reason ttl). An end state never changes. Run mints the id (job_<ULID>) and sends it unchanged as platform.job@1 `id`, so every metering reading of the job is `run:<id>:<n>` (platform.job-frame@1 job_usage). usage.seconds is wall_ms / 1000: the sum of the job's readings (service run, operation function.invoke, unit s). Never carries the job's stdout (GET /api/v1/jobs/{id}/stream) or a credential. More fields may be added.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "project_id",
++ "requester",
++ "state",
++ "class",
++ "artifact",
++ "inputs",
++ "limits",
++ "ttl_ms",
++ "egress",
++ "requirements",
++ "idempotency_key",
++ "placement",
++ "timings",
++ "exit",
++ "result",
++ "usage",
++ "error"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "Minted by Run; the platform.job@1 id and the stem of every usage reading key (run:<id>:<n>)."
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The project the caller's token names; usage is billed to it."
++ },
++ "requester": {
++ "$ref": "../identity/subject-ref.v1.json",
++ "description": "The token principal that submitted the job."
++ },
++ "state": {
++ "enum": [
++ "queued",
++ "placed",
++ "running",
++ "succeeded",
++ "failed",
++ "cancelled",
++ "expired"
++ ]
++ },
++ "class": {
++ "$ref": "../platform/runtime-class.v1.json"
++ },
++ "artifact": {
++ "anyOf": [
++ {
++ "$ref": "job-create-request.v1.json#/$defs/artifact"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "inputs": {
++ "type": "array",
++ "maxItems": 32,
++ "items": {
++ "$ref": "job-create-request.v1.json#/$defs/input"
++ }
++ },
++ "limits": {
++ "$ref": "job-create-request.v1.json#/$defs/limits"
++ },
++ "ttl_ms": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "The job's lifetime from created_at, the default filled in."
++ },
++ "egress": {
++ "$ref": "job-create-request.v1.json#/$defs/egress"
++ },
++ "requirements": {
++ "$ref": "../platform/workload-requirements.v1.json",
++ "description": "What the job was placed against, the default filled in."
++ },
++ "idempotency_key": {
++ "anyOf": [
++ {
++ "$ref": "job-create-request.v1.json#/$defs/idempotency_key"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "placement": {
++ "anyOf": [
++ {
++ "$ref": "#/$defs/placement"
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "Where the job was sent; null while queued, and for a job cancelled or expired before placement."
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created_at",
++ "placed_at",
++ "started_at",
++ "finished_at"
++ ],
++ "properties": {
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "placed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "started_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "From job_started started_ms; null until the process started."
++ },
++ "finished_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "When the job reached its end state; null before."
++ }
++ }
++ },
++ "exit": {
++ "anyOf": [
++ {
++ "$ref": "#/$defs/exit"
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The node's job_exit; null until it arrived, and for a job that ended before a node ran it."
++ },
++ "result": {
++ "description": "The artifact's declared output (job_exit result, any JSON); null unless state is succeeded."
++ },
++ "usage": {
++ "$ref": "#/$defs/usage"
++ },
++ "error": {
++ "anyOf": [
++ {
++ "$ref": "#/$defs/error"
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "Why the job failed or expired; null in every other state."
++ }
++ },
++ "$defs": {
++ "placement": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "node",
++ "provider",
++ "region"
++ ],
++ "properties": {
++ "node": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 128,
++ "description": "The worker's device id (the usage reading's `node`)."
++ },
++ "provider": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 128,
++ "description": "The offer's provider (the usage reading's `provider`); null for a first-party node."
++ },
++ "region": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 64
++ }
++ }
++ },
++ "exit": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "reason",
++ "code"
++ ],
++ "properties": {
++ "reason": {
++ "enum": [
++ "exited",
++ "cancelled",
++ "ttl",
++ "limit",
++ "stopped",
++ "failed"
++ ],
++ "description": "platform.job-frame@1 job_exit reason."
++ },
++ "code": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "description": "The process's exit status; null when it was killed or never ran."
++ }
++ }
++ },
++ "usage": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "seconds",
++ "wall_ms",
++ "cpu_ms",
++ "mem_peak_bytes"
++ ],
++ "properties": {
++ "seconds": {
++ "type": "number",
++ "minimum": 0,
++ "description": "Metered wall-clock seconds, wall_ms / 1000 (unit s); 0 when the job never started."
++ },
++ "wall_ms": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "cpu_ms": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "Informational, never billed."
++ },
++ "mem_peak_bytes": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "Informational, never billed."
++ }
++ }
++ },
++ "error": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "code",
++ "detail"
++ ],
++ "properties": {
++ "code": {
++ "type": "string",
++ "pattern": "^run\\.[a-z0-9_]+(\\.[a-z0-9_]+)*$",
++ "description": "run.job.exit_nonzero, run.job.limit, run.job.stopped, run.job.worker_failed, run.job.input_digest_mismatch, run.job.unplaceable (expired: no node offered the class, egress and requirements), run.job.ttl."
++ },
++ "detail": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 1024
++ }
++ }
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "properties": {
++ "state": {
++ "const": "queued"
++ }
++ },
++ "required": [
++ "state"
++ ]
++ },
++ "then": {
++ "properties": {
++ "placement": {
++ "type": "null"
++ },
++ "exit": {
++ "type": "null"
++ },
++ "timings": {
++ "type": "object",
++ "properties": {
++ "placed_at": {
++ "type": "null"
++ },
++ "started_at": {
++ "type": "null"
++ },
++ "finished_at": {
++ "type": "null"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "state": {
++ "enum": [
++ "placed",
++ "running"
++ ]
++ }
++ },
++ "required": [
++ "state"
++ ]
++ },
++ "then": {
++ "properties": {
++ "placement": {
++ "type": "object"
++ },
++ "exit": {
++ "type": "null"
++ },
++ "timings": {
++ "type": "object",
++ "properties": {
++ "placed_at": {
++ "type": "string"
++ },
++ "finished_at": {
++ "type": "null"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "state": {
++ "const": "running"
++ }
++ },
++ "required": [
++ "state"
++ ]
++ },
++ "then": {
++ "properties": {
++ "timings": {
++ "type": "object",
++ "properties": {
++ "started_at": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "state": {
++ "enum": [
++ "succeeded",
++ "failed",
++ "cancelled",
++ "expired"
++ ]
++ }
++ },
++ "required": [
++ "state"
++ ]
++ },
++ "then": {
++ "properties": {
++ "timings": {
++ "type": "object",
++ "properties": {
++ "finished_at": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "state": {
++ "const": "succeeded"
++ }
++ },
++ "required": [
++ "state"
++ ]
++ },
++ "then": {
++ "properties": {
++ "placement": {
++ "type": "object"
++ },
++ "exit": {
++ "type": "object",
++ "properties": {
++ "reason": {
++ "const": "exited"
++ },
++ "code": {
++ "const": 0
++ }
++ }
++ },
++ "timings": {
++ "type": "object",
++ "properties": {
++ "started_at": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "state": {
++ "enum": [
++ "failed",
++ "expired"
++ ]
++ }
++ },
++ "required": [
++ "state"
++ ]
++ },
++ "then": {
++ "properties": {
++ "error": {
++ "type": "object"
++ }
++ }
++ },
++ "else": {
++ "properties": {
++ "error": {
++ "type": "null"
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "state": {
++ "const": "succeeded"
++ }
++ },
++ "required": [
++ "state"
++ ]
++ },
++ "else": {
++ "properties": {
++ "result": {
++ "type": "null"
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "class": {
++ "enum": [
++ "function",
++ "code"
++ ]
++ }
++ },
++ "required": [
++ "class"
++ ]
++ },
++ "then": {
++ "properties": {
++ "artifact": {
++ "type": "object"
++ }
++ }
++ }
++ }
++ ]
++ }
++ },
++ "run.job-list-query": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "planned",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/job-list-query.v1.json",
++ "title": "RunJobListQuery",
++ "description": "run.job-list-query@1 (PLANNED; plan T14 R1): the query string of GET /api/v1/jobs on OpenVibe.Run (capability run.job.list) and of GET /api/v1/admin/jobs (run.job.admin, which alone may pass project_id). Without project_id it lists the caller's token project.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "state": {
++ "enum": [
++ "queued",
++ "placed",
++ "running",
++ "succeeded",
++ "failed",
++ "cancelled",
++ "expired"
++ ]
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "Admin only (run.job.admin): list this project's jobs; with run.job.list it is refused 403."
++ },
++ "limit": {
++ "oneOf": [
++ {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 200
++ },
++ {
++ "type": "string",
++ "pattern": "^(?:[1-9][0-9]?|1[0-9]{2}|200)$"
++ }
++ ],
++ "description": "Items per page, 1 to 200 (default 50). A query string carries it as a string, which must name the same range."
++ },
++ "cursor": {
++ "type": "string",
++ "maxLength": 512,
++ "description": "next_cursor of the previous page."
++ }
++ }
++ }
++ },
++ "run.job-list-result": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "planned",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/job-list-result.v1.json",
++ "title": "RunJobListResult",
++ "description": "run.job-list-result@1 (PLANNED; plan T14 R1): the answer of GET /api/v1/jobs on OpenVibe.Run (capability run.job.list) and GET /api/v1/admin/jobs (run.job.admin), filtered by run.job-list-query@1. Jobs come newest first (by id). More fields may be added.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "jobs",
++ "next_cursor"
++ ],
++ "properties": {
++ "jobs": {
++ "type": "array",
++ "maxItems": 200,
++ "items": {
++ "$ref": "job-read-result.v1.json"
++ }
++ },
++ "next_cursor": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 512,
++ "description": "Pass as cursor for the next page; null on the last page."
++ }
++ }
++ }
++ },
++ "run.job-stream-ticket-result": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "planned",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/job-stream-ticket-result.v1.json",
++ "title": "RunJobStreamTicketResult",
++ "description": "run.job-stream-ticket-result@1 (PLANNED; plan T14 R1): the answer of POST /api/v1/jobs/{id}/stream/ticket on OpenVibe.Run (capability run.job.stream), modelled on Events' realtime ticket (network.realtime-ticket-result@1). A browser cannot put a header on an EventSource, so it opens `${stream_url}?ticket=${ticket}` (plus Last-Event-ID when it resumes) at once. The ticket is a two-minute RS256 JWT that Run signs: audience openvibe.run, typ run-stream (so it is never a session token), it names this one job, and Run refuses a jti it has already seen while the ticket is valid, so a ticket opens one stream; ask for a fresh one for every reconnect. Never logged, never stored. A server caller may instead send its Bearer token to GET /api/v1/jobs/{id}/stream. The stream is text/event-stream of run.job-stream-event@1.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "ticket",
++ "expires_at",
++ "expires_in",
++ "stream_url",
++ "job_id"
++ ],
++ "properties": {
++ "ticket": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$",
++ "maxLength": 4096,
++ "description": "The signed ticket (a compact JWS)."
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "expires_in": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 300,
++ "description": "Seconds until the ticket expires (120)."
++ },
++ "stream_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://[^?#]+/api/v1/jobs/job_[0-9A-HJKMNP-TV-Z]{26}/stream$"
++ },
++ "job_id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ }
++ },
++ "run.job-stream-event": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "planned",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/run/job-stream-event.v1.json",
++ "title": "RunJobStreamEvent",
++ "description": "run.job-stream-event@1 (PLANNED; plan T14 R1): the JSON `data` of one server-sent event on GET /api/v1/jobs/{id}/stream (capability run.job.stream). The SSE `event` field is the type and the SSE `id` is `seq`, so a reconnect with Last-Event-ID resumes after it. output: a chunk of the job's stdout (platform.job-frame@1 job_stdout, best effort: a chunk lost on the node's link is never resent, and Run keeps at most the last 1 MiB to replay). state: the job moved to a new state. end: the job reached its end state; the server closes the stream after it. A stream opened on an ended job replays what is kept, then end. Output is never metered and never in an event.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "type",
++ "job_id",
++ "seq",
++ "at"
++ ],
++ "properties": {
++ "type": {
++ "enum": [
++ "output",
++ "state",
++ "end"
++ ]
++ },
++ "job_id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "seq": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "The job's own stream counter from 1; the SSE id."
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "chunk": {
++ "type": "string",
++ "maxLength": 65536,
++ "description": "output only: UTF-8 text."
++ },
++ "state": {
++ "enum": [
++ "queued",
++ "placed",
++ "running",
++ "succeeded",
++ "failed",
++ "cancelled",
++ "expired"
++ ],
++ "description": "state and end: the job's state."
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "properties": {
++ "type": {
++ "const": "output"
++ }
++ },
++ "required": [
++ "type"
++ ]
++ },
++ "then": {
++ "required": [
++ "chunk"
++ ],
++ "not": {
++ "required": [
++ "state"
++ ]
++ }
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "type": {
++ "enum": [
++ "state",
++ "end"
++ ]
++ }
++ },
++ "required": [
++ "type"
++ ]
++ },
++ "then": {
++ "required": [
++ "state"
++ ],
++ "not": {
++ "required": [
++ "chunk"
++ ]
++ }
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "type": {
++ "const": "end"
++ }
++ },
++ "required": [
++ "type"
++ ]
++ },
++ "then": {
++ "properties": {
++ "state": {
++ "enum": [
++ "succeeded",
++ "failed",
++ "cancelled",
++ "expired"
++ ]
++ }
++ }
++ }
++ }
++ ]
++ }
++ },
++ "run.job.queued": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "planned",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/run.job.queued.v1.json",
++ "title": "RunJobQueuedPayload",
++ "description": "run.job.queued v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). A job was stored queued (POST /api/v1/jobs created it; not published for an idempotent repeat). A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority low, actor service:run.",
++ "type": "object",
++ "required": [
++ "job_id",
++ "project_id",
++ "requester",
++ "state",
++ "class",
++ "artifact",
++ "egress",
++ "placement",
++ "timings",
++ "exit",
++ "usage",
++ "error"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "job_id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "requester": {
++ "$ref": "../../identity/subject-ref.v1.json"
++ },
++ "state": {
++ "const": "queued",
++ "description": "The job state (run.job-read-result@1 state)."
++ },
++ "class": {
++ "$ref": "../../platform/runtime-class.v1.json"
++ },
++ "artifact": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/artifact"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "egress": {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/egress"
++ },
++ "placement": {
++ "type": "null"
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created_at",
++ "placed_at",
++ "started_at",
++ "finished_at"
++ ],
++ "properties": {
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "placed_at": {
++ "type": "null"
++ },
++ "started_at": {
++ "type": "null"
++ },
++ "finished_at": {
++ "type": "null"
++ }
++ }
++ },
++ "exit": {
++ "type": "null"
++ },
++ "usage": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/usage"
++ },
++ "error": {
++ "type": "null"
++ }
++ }
++ }
++ },
++ "run.job.started": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "planned",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/run.job.started.v1.json",
++ "title": "RunJobStartedPayload",
++ "description": "run.job.started v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). The node sent job_started: the process runs. Usage seconds count from started_at. A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority low, actor service:run.",
++ "type": "object",
++ "required": [
++ "job_id",
++ "project_id",
++ "requester",
++ "state",
++ "class",
++ "artifact",
++ "egress",
++ "placement",
++ "timings",
++ "exit",
++ "usage",
++ "error"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "job_id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "requester": {
++ "$ref": "../../identity/subject-ref.v1.json"
++ },
++ "state": {
++ "const": "running",
++ "description": "The job state (run.job-read-result@1 state)."
++ },
++ "class": {
++ "$ref": "../../platform/runtime-class.v1.json"
++ },
++ "artifact": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/artifact"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "egress": {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/egress"
++ },
++ "placement": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/placement"
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created_at",
++ "placed_at",
++ "started_at",
++ "finished_at"
++ ],
++ "properties": {
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "placed_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "started_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "finished_at": {
++ "type": "null"
++ }
++ }
++ },
++ "exit": {
++ "type": "null"
++ },
++ "usage": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/usage"
++ },
++ "error": {
++ "type": "null"
++ }
++ }
++ }
++ },
++ "run.job.succeeded": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "planned",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/run.job.succeeded.v1.json",
++ "title": "RunJobSucceededPayload",
++ "description": "run.job.succeeded v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). The job ended succeeded: job_exit reason exited with code 0. Carries no result (read it with GET /api/v1/jobs/{id}). A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority normal, actor service:run.",
++ "type": "object",
++ "required": [
++ "job_id",
++ "project_id",
++ "requester",
++ "state",
++ "class",
++ "artifact",
++ "egress",
++ "placement",
++ "timings",
++ "exit",
++ "usage",
++ "error"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "job_id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "requester": {
++ "$ref": "../../identity/subject-ref.v1.json"
++ },
++ "state": {
++ "const": "succeeded",
++ "description": "The job state (run.job-read-result@1 state)."
++ },
++ "class": {
++ "$ref": "../../platform/runtime-class.v1.json"
++ },
++ "artifact": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/artifact"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "egress": {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/egress"
++ },
++ "placement": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/placement"
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created_at",
++ "placed_at",
++ "started_at",
++ "finished_at"
++ ],
++ "properties": {
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "placed_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "started_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "finished_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "exit": {
++ "allOf": [
++ {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/exit"
++ },
++ {
++ "type": "object",
++ "properties": {
++ "reason": {
++ "const": "exited"
++ },
++ "code": {
++ "const": 0
++ }
++ }
++ }
++ ]
++ },
++ "usage": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/usage"
++ },
++ "error": {
++ "type": "null"
++ }
++ }
++ }
++ },
++ "run.job.failed": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "planned",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/run.job.failed.v1.json",
++ "title": "RunJobFailedPayload",
++ "description": "run.job.failed v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). The job ended failed: job_exit with a non-zero code or reason limit, stopped or failed, or an input digest mismatch before the process started. A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority important, actor service:run.",
++ "type": "object",
++ "required": [
++ "job_id",
++ "project_id",
++ "requester",
++ "state",
++ "class",
++ "artifact",
++ "egress",
++ "placement",
++ "timings",
++ "exit",
++ "usage",
++ "error"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "job_id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "requester": {
++ "$ref": "../../identity/subject-ref.v1.json"
++ },
++ "state": {
++ "const": "failed",
++ "description": "The job state (run.job-read-result@1 state)."
++ },
++ "class": {
++ "$ref": "../../platform/runtime-class.v1.json"
++ },
++ "artifact": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/artifact"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "egress": {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/egress"
++ },
++ "placement": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/placement"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created_at",
++ "placed_at",
++ "started_at",
++ "finished_at"
++ ],
++ "properties": {
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "placed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "started_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "finished_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "exit": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/exit"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "usage": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/usage"
++ },
++ "error": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/error"
++ }
++ }
++ }
++ },
++ "run.job.cancelled": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "planned",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/run.job.cancelled.v1.json",
++ "title": "RunJobCancelledPayload",
++ "description": "run.job.cancelled v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). The job ended cancelled (POST /api/v1/jobs/{id}/cancel by its project or run.job.admin, or job_exit reason cancelled). A job cancelled before placement has no placement and no exit. A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority normal, actor service:run.",
++ "type": "object",
++ "required": [
++ "job_id",
++ "project_id",
++ "requester",
++ "state",
++ "class",
++ "artifact",
++ "egress",
++ "placement",
++ "timings",
++ "exit",
++ "usage",
++ "error"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "job_id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "requester": {
++ "$ref": "../../identity/subject-ref.v1.json"
++ },
++ "state": {
++ "const": "cancelled",
++ "description": "The job state (run.job-read-result@1 state)."
++ },
++ "class": {
++ "$ref": "../../platform/runtime-class.v1.json"
++ },
++ "artifact": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/artifact"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "egress": {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/egress"
++ },
++ "placement": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/placement"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created_at",
++ "placed_at",
++ "started_at",
++ "finished_at"
++ ],
++ "properties": {
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "placed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "started_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "finished_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "exit": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/exit"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "usage": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/usage"
++ },
++ "error": {
++ "type": "null"
++ }
++ }
++ }
++ },
++ "run.job.expired": {
++ "version": "1.0.0",
++ "owner": "run",
++ "status": "planned",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/run.job.expired.v1.json",
++ "title": "RunJobExpiredPayload",
++ "description": "run.job.expired v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). The job ended expired: its ttl_ms ran out before a node took it (run.job.unplaceable), before it started, or while it ran (job_exit reason ttl, run.job.ttl). A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority normal, actor service:run.",
++ "type": "object",
++ "required": [
++ "job_id",
++ "project_id",
++ "requester",
++ "state",
++ "class",
++ "artifact",
++ "egress",
++ "placement",
++ "timings",
++ "exit",
++ "usage",
++ "error"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "job_id": {
++ "type": "string",
++ "pattern": "^job_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "requester": {
++ "$ref": "../../identity/subject-ref.v1.json"
++ },
++ "state": {
++ "const": "expired",
++ "description": "The job state (run.job-read-result@1 state)."
++ },
++ "class": {
++ "$ref": "../../platform/runtime-class.v1.json"
++ },
++ "artifact": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/artifact"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "egress": {
++ "$ref": "../../run/job-create-request.v1.json#/$defs/egress"
++ },
++ "placement": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/placement"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "timings": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "created_at",
++ "placed_at",
++ "started_at",
++ "finished_at"
++ ],
++ "properties": {
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "placed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "started_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "finished_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "exit": {
++ "anyOf": [
++ {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/exit"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "usage": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/usage"
++ },
++ "error": {
++ "$ref": "../../run/job-read-result.v1.json#/$defs/error"
++ }
++ }
++ }
}
}
}
ddiff --git a/generated/openapi/ai.json b/generated/openapi/ai.json
iindex 0e3efbf5..5f1f91c0 100644
---- a/generated/openapi/ai.json
++++ b/generated/openapi/ai.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.AI API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/billing.json b/generated/openapi/billing.json
iindex dd6ba61b..67ccc9f6 100644
---- a/generated/openapi/billing.json
++++ b/generated/openapi/billing.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Billing API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/blog.json b/generated/openapi/blog.json
iindex c8118ace..21fcf349 100644
---- a/generated/openapi/blog.json
++++ b/generated/openapi/blog.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Blog API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/bot.json b/generated/openapi/bot.json
iindex 78b75d02..0db98cf0 100644
---- a/generated/openapi/bot.json
++++ b/generated/openapi/bot.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Bot API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/chat.json b/generated/openapi/chat.json
iindex 4496f142..e49508c8 100644
---- a/generated/openapi/chat.json
++++ b/generated/openapi/chat.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Chat API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/codes.json b/generated/openapi/codes.json
iindex 5a9ec185..ea9cdd5b 100644
---- a/generated/openapi/codes.json
++++ b/generated/openapi/codes.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Codes API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/community.json b/generated/openapi/community.json
iindex 6b58484e..3ed481d7 100644
---- a/generated/openapi/community.json
++++ b/generated/openapi/community.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Community API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/coupons.json b/generated/openapi/coupons.json
iindex e0026892..c0d807e8 100644
---- a/generated/openapi/coupons.json
++++ b/generated/openapi/coupons.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Coupons API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/deals.json b/generated/openapi/deals.json
iindex 8d05cc23..61bc236b 100644
---- a/generated/openapi/deals.json
++++ b/generated/openapi/deals.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Deals API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/events.json b/generated/openapi/events.json
iindex 0935a7a2..5a1e57ee 100644
---- a/generated/openapi/events.json
++++ b/generated/openapi/events.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Events API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/games.json b/generated/openapi/games.json
iindex 8585d1d3..e2f5481d 100644
---- a/generated/openapi/games.json
++++ b/generated/openapi/games.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Games API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/host.json b/generated/openapi/host.json
iindex 41012145..09c29b46 100644
---- a/generated/openapi/host.json
++++ b/generated/openapi/host.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Host API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/live.json b/generated/openapi/live.json
iindex 5a51279f..5620e744 100644
---- a/generated/openapi/live.json
++++ b/generated/openapi/live.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Live API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/media.json b/generated/openapi/media.json
iindex f6e28ac2..490ce8bf 100644
---- a/generated/openapi/media.json
++++ b/generated/openapi/media.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Media API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/network.json b/generated/openapi/network.json
iindex 44623dcf..85a8d734 100644
---- a/generated/openapi/network.json
++++ b/generated/openapi/network.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Network API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/news.json b/generated/openapi/news.json
iindex ebdec442..9236b022 100644
---- a/generated/openapi/news.json
++++ b/generated/openapi/news.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.News API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/openre.json b/generated/openapi/openre.json
iindex 2a304201..53ed09df 100644
---- a/generated/openapi/openre.json
++++ b/generated/openapi/openre.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenRe.Stream API",
-- "version": "0.89.0",
-- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/reviews.json b/generated/openapi/reviews.json
iindex 9c5930c7..bbed9c67 100644
---- a/generated/openapi/reviews.json
++++ b/generated/openapi/reviews.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Reviews API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/search.json b/generated/openapi/search.json
iindex c9ad4cca..47141ae1 100644
---- a/generated/openapi/search.json
++++ b/generated/openapi/search.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Search API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/sources.json b/generated/openapi/sources.json
iindex 912600c6..87ef3511 100644
---- a/generated/openapi/sources.json
++++ b/generated/openapi/sources.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Sources API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/space.json b/generated/openapi/space.json
iindex 91cb10d5..a6d7d3e0 100644
---- a/generated/openapi/space.json
++++ b/generated/openapi/space.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Space API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tips.json b/generated/openapi/tips.json
iindex 178a6702..87e28a10 100644
---- a/generated/openapi/tips.json
++++ b/generated/openapi/tips.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tips API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tools.json b/generated/openapi/tools.json
iindex de6ad0f8..f27860d6 100644
---- a/generated/openapi/tools.json
++++ b/generated/openapi/tools.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tools API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/trade.json b/generated/openapi/trade.json
iindex c0adac99..04f5110e 100644
---- a/generated/openapi/trade.json
++++ b/generated/openapi/trade.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Trade API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/vip.json b/generated/openapi/vip.json
iindex f9280faa..33a23ccb 100644
---- a/generated/openapi/vip.json
++++ b/generated/openapi/vip.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.VIP API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/wiki.json b/generated/openapi/wiki.json
iindex 0ea2a92f..19f5939f 100644
---- a/generated/openapi/wiki.json
++++ b/generated/openapi/wiki.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Wiki API",
-- "version": "0.89.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.89.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.90.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex d07c2276..e3024909 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -52408,3 +52408,700 @@ export type BotDeviceConnectResult =
| {
device: BotDevice;
};
++
++/** [email protected] (owner: run) */
++/**
++ * run.job-create-request@1 (PLANNED; plan T14 R1, ADR-034): the body of POST /api/v1/jobs on OpenVibe.Run (capability run.job.submit). The job belongs to the project the caller's token names. CLASS is a platform.runtime-class@1 name; Run accepts any of them but places a job only on a node that advertises worker:<class> (platform.resource-offer@1), so a class no node runs today (browser, linux, desktop, gpu) ends expired with run.job.unplaceable. ARTIFACT is the pre-registered artifact to run (name and one exact version), required for function and code, as platform.job@1 carries it. ARGS is the artifact's declared input as JSON (at most 512 KiB serialized, else 413). INPUTS are files placed in the job directory before the process starts, each an OpenVibe.Media object pinned by its sha256: a size or digest mismatch fails the job (run.job.input_digest_mismatch) before the process ever runs; Run never fetches a caller-chosen URL. LIMITS are hard caps merged stricter with the node's own (platform.job@1 limits); a request over the project's tier cap is refused 422 run.limits.exceeded, never silently lowered. EGRESS is the network the job may reach, the values of OpenVibe.Node's worker.egress: none (default; any node, sent as net deny), public or openvibe-only (only a node whose worker.egress is exactly that policy, until platform.job@1 net carries it per job). REQUIREMENTS (platform.workload-requirements@1) narrow placement; absent means { kind: run.<class>, mobility: job, latency_class: background, objective: balanced }. IDEMPOTENCY_KEY: a repeat from the same project within 24 h with an identical body answers the first job (200, created false); with a different body 409 run.idempotency.conflict.
++ */
++export type RunJobCreateRequest = {
++ [k: string]: unknown | undefined;
++} & {
++ class: RuntimeClass;
++ /**
++ * The pre-registered artifact to run (platform.job@1 artifact).
++ */
++ artifact?: {
++ name: string;
++ /**
++ * One exact version, never a range
++ */
++ version: string;
++ };
++ /**
++ * The artifact's declared input as JSON; default {}.
++ */
++ args?: {};
++ /**
++ * Files placed in the job directory before the process starts; names are unique (else 422).
++ *
++ * @maxItems 32
++ */
++ inputs?: {
++ /**
++ * File name in the job directory; no path separators.
++ */
++ name: string;
++ media_id: string;
++ /**
++ * Lowercase hex SHA-256 of the object's bytes.
++ */
++ sha256: string;
++ size_bytes?: number;
++ }[];
++ /**
++ * Caps on the running process (platform.job@1 limits); hitting one ends the job failed with run.job.limit.
++ */
++ limits: {
++ /**
++ * Running time from `job_started`
++ */
++ wall_ms: number;
++ /**
++ * CPU time, user plus system
++ */
++ cpu_ms: number;
++ /**
++ * Peak resident memory
++ */
++ mem_bytes: number;
++ };
++ /**
++ * Lifetime from creation, covering the wait for a node and the run (platform.job@1 ttl_ms); when it runs out the job ends expired. Default: limits.wall_ms plus 600000.
++ */
++ ttl_ms?: number;
++ /**
++ * OpenVibe.Node worker.egress values: none (no network), public (the internet without private, link-local or the network's own ranges), openvibe-only (the network's own endpoints only).
++ */
++ egress?: "none" | "public" | "openvibe-only";
++ requirements?: WorkloadRequirements;
++ idempotency_key?: string;
++};
++
++/** [email protected] (owner: run) */
++/**
++ * run.job-create-result@1 (PLANNED; plan T14 R1, ADR-034): the answer of POST /api/v1/jobs on OpenVibe.Run (capability run.job.submit). 201 with created true and the new job, queued; 200 with created false and the job first created under the same idempotency_key (in whatever state it has reached). Refusals are application/problem+json: 400 (body invalid), 413 (args over 512 KiB), 422 run.limits.exceeded or a duplicate input name, 409 run.idempotency.conflict, 429 (quota).
++ */
++export interface RunJobCreateResult {
++ /**
++ * false when the idempotency_key matched an earlier job.
++ */
++ created: boolean;
++ job: RunJobReadResult;
++}
++
++/** [email protected] (owner: run) */
++/**
++ * run.job-read-result@1 (PLANNED; plan T14 R1, ADR-034): one job on OpenVibe.Run, as GET /api/v1/jobs/{id} answers it (capability run.job.read), POST /api/v1/jobs/{id}/cancel answers it after the cancel (run.job.cancel), and run.job-create-result@1 and run.job-list-result@1 carry it. STATES: queued (stored, waiting for a node) -> placed (sent to a node as platform.job@1, not yet started) -> running (the node sent job_started) -> one end state: succeeded (job_exit reason exited with code 0), failed (exited with another code, or reason limit, stopped or failed), cancelled (POST /api/v1/jobs/{id}/cancel, or job_exit reason cancelled), expired (ttl_ms ran out: before placement, before start, or while running, job_exit reason ttl). An end state never changes. Run mints the id (job_<ULID>) and sends it unchanged as platform.job@1 `id`, so every metering reading of the job is `run:<id>:<n>` (platform.job-frame@1 job_usage). usage.seconds is wall_ms / 1000: the sum of the job's readings (service run, operation function.invoke, unit s). Never carries the job's stdout (GET /api/v1/jobs/{id}/stream) or a credential. More fields may be added.
++ */
++export type RunJobReadResult = {
++ [k: string]: unknown | undefined;
++} & {
++ /**
++ * Minted by Run; the platform.job@1 id and the stem of every usage reading key (run:<id>:<n>).
++ */
++ id: string;
++ /**
++ * The project the caller's token names; usage is billed to it.
++ */
++ project_id: string;
++ requester: SubjectRef;
++ state: "queued" | "placed" | "running" | "succeeded" | "failed" | "cancelled" | "expired";
++ class: RuntimeClass;
++ artifact: {
++ name: string;
++ /**
++ * One exact version, never a range
++ */
++ version: string;
++ } | null;
++ /**
++ * @maxItems 32
++ */
++ inputs: {
++ /**
++ * File name in the job directory; no path separators.
++ */
++ name: string;
++ media_id: string;
++ /**
++ * Lowercase hex SHA-256 of the object's bytes.
++ */
++ sha256: string;
++ size_bytes?: number;
++ }[];
++ /**
++ * Caps on the running process (platform.job@1 limits); hitting one ends the job failed with run.job.limit.
++ */
++ limits: {
++ /**
++ * Running time from `job_started`
++ */
++ wall_ms: number;
++ /**
++ * CPU time, user plus system
++ */
++ cpu_ms: number;
++ /**
++ * Peak resident memory
++ */
++ mem_bytes: number;
++ };
++ /**
++ * The job's lifetime from created_at, the default filled in.
++ */
++ ttl_ms: number;
++ /**
++ * OpenVibe.Node worker.egress values: none (no network), public (the internet without private, link-local or the network's own ranges), openvibe-only (the network's own endpoints only).
++ */
++ egress: "none" | "public" | "openvibe-only";
++ requirements: WorkloadRequirements;
++ idempotency_key: string | null;
++ /**
++ * Where the job was sent; null while queued, and for a job cancelled or expired before placement.
++ */
++ placement: {
++ /**
++ * The worker's device id (the usage reading's `node`).
++ */
++ node: string;
++ /**
++ * The offer's provider (the usage reading's `provider`); null for a first-party node.
++ */
++ provider: string | null;
++ region: string | null;
++ } | null;
++ timings: {
++ created_at: string;
++ placed_at: string | null;
++ /**
++ * From job_started started_ms; null until the process started.
++ */
++ started_at: string | null;
++ /**
++ * When the job reached its end state; null before.
++ */
++ finished_at: string | null;
++ };
++ /**
++ * The node's job_exit; null until it arrived, and for a job that ended before a node ran it.
++ */
++ exit: {
++ /**
++ * platform.job-frame@1 job_exit reason.
++ */
++ reason: "exited" | "cancelled" | "ttl" | "limit" | "stopped" | "failed";
++ /**
++ * The process's exit status; null when it was killed or never ran.
++ */
++ code: number | null;
++ } | null;
++ /**
++ * The artifact's declared output (job_exit result, any JSON); null unless state is succeeded.
++ */
++ result: {
++ [k: string]: unknown | undefined;
++ };
++ usage: {
++ /**
++ * Metered wall-clock seconds, wall_ms / 1000 (unit s); 0 when the job never started.
++ */
++ seconds: number;
++ wall_ms: number;
++ /**
++ * Informational, never billed.
++ */
++ cpu_ms: number | null;
++ /**
++ * Informational, never billed.
++ */
++ mem_peak_bytes: number | null;
++ };
++ /**
++ * Why the job failed or expired; null in every other state.
++ */
++ error: {
++ /**
++ * run.job.exit_nonzero, run.job.limit, run.job.stopped, run.job.worker_failed, run.job.input_digest_mismatch, run.job.unplaceable (expired: no node offered the class, egress and requirements), run.job.ttl.
++ */
++ code: string;
++ detail: string | null;
++ } | null;
++};
++
++/** [email protected] (owner: run) */
++/**
++ * run.job-list-query@1 (PLANNED; plan T14 R1): the query string of GET /api/v1/jobs on OpenVibe.Run (capability run.job.list) and of GET /api/v1/admin/jobs (run.job.admin, which alone may pass project_id). Without project_id it lists the caller's token project.
++ */
++export interface RunJobListQuery {
++ state?: "queued" | "placed" | "running" | "succeeded" | "failed" | "cancelled" | "expired";
++ /**
++ * Admin only (run.job.admin): list this project's jobs; with run.job.list it is refused 403.
++ */
++ project_id?: string;
++ /**
++ * Items per page, 1 to 200 (default 50). A query string carries it as a string, which must name the same range.
++ */
++ limit?: number | string;
++ /**
++ * next_cursor of the previous page.
++ */
++ cursor?: string;
++}
++
++/** [email protected] (owner: run) */
++/**
++ * run.job-list-result@1 (PLANNED; plan T14 R1): the answer of GET /api/v1/jobs on OpenVibe.Run (capability run.job.list) and GET /api/v1/admin/jobs (run.job.admin), filtered by run.job-list-query@1. Jobs come newest first (by id). More fields may be added.
++ */
++export interface RunJobListResult {
++ /**
++ * @maxItems 200
++ */
++ jobs: RunJobReadResult[];
++ /**
++ * Pass as cursor for the next page; null on the last page.
++ */
++ next_cursor: string | null;
++}
++
++/** [email protected] (owner: run) */
++/**
++ * run.job-stream-ticket-result@1 (PLANNED; plan T14 R1): the answer of POST /api/v1/jobs/{id}/stream/ticket on OpenVibe.Run (capability run.job.stream), modelled on Events' realtime ticket (network.realtime-ticket-result@1). A browser cannot put a header on an EventSource, so it opens `${stream_url}?ticket=${ticket}` (plus Last-Event-ID when it resumes) at once. The ticket is a two-minute RS256 JWT that Run signs: audience openvibe.run, typ run-stream (so it is never a session token), it names this one job, and Run refuses a jti it has already seen while the ticket is valid, so a ticket opens one stream; ask for a fresh one for every reconnect. Never logged, never stored. A server caller may instead send its Bearer token to GET /api/v1/jobs/{id}/stream. The stream is text/event-stream of run.job-stream-event@1.
++ */
++export interface RunJobStreamTicketResult {
++ /**
++ * The signed ticket (a compact JWS).
++ */
++ ticket: string;
++ expires_at: string;
++ /**
++ * Seconds until the ticket expires (120).
++ */
++ expires_in: number;
++ stream_url: string;
++ job_id: string;
++}
++
++/** [email protected] (owner: run) */
++/**
++ * run.job-stream-event@1 (PLANNED; plan T14 R1): the JSON `data` of one server-sent event on GET /api/v1/jobs/{id}/stream (capability run.job.stream). The SSE `event` field is the type and the SSE `id` is `seq`, so a reconnect with Last-Event-ID resumes after it. output: a chunk of the job's stdout (platform.job-frame@1 job_stdout, best effort: a chunk lost on the node's link is never resent, and Run keeps at most the last 1 MiB to replay). state: the job moved to a new state. end: the job reached its end state; the server closes the stream after it. A stream opened on an ended job replays what is kept, then end. Output is never metered and never in an event.
++ */
++export type RunJobStreamEvent = {
++ [k: string]: unknown | undefined;
++} & {
++ type: "output" | "state" | "end";
++ job_id: string;
++ /**
++ * The job's own stream counter from 1; the SSE id.
++ */
++ seq: number;
++ at: string;
++ /**
++ * output only: UTF-8 text.
++ */
++ chunk?: string;
++ /**
++ * state and end: the job's state.
++ */
++ state?: "queued" | "placed" | "running" | "succeeded" | "failed" | "cancelled" | "expired";
++};
++
++/** [email protected] (owner: run) */
++/**
++ * run.job.queued v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). A job was stored queued (POST /api/v1/jobs created it; not published for an idempotent repeat). A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority low, actor service:run.
++ */
++export interface RunJobQueuedPayload {
++ job_id: string;
++ project_id: string;
++ requester: SubjectRef;
++ /**
++ * The job state (run.job-read-result@1 state).
++ */
++ state: "queued";
++ class: RuntimeClass;
++ artifact: {
++ name: string;
++ /**
++ * One exact version, never a range
++ */
++ version: string;
++ } | null;
++ /**
++ * OpenVibe.Node worker.egress values: none (no network), public (the internet without private, link-local or the network's own ranges), openvibe-only (the network's own endpoints only).
++ */
++ egress: "none" | "public" | "openvibe-only";
++ placement: null;
++ timings: {
++ created_at: string;
++ placed_at: null;
++ started_at: null;
++ finished_at: null;
++ };
++ exit: null;
++ usage: {
++ /**
++ * Metered wall-clock seconds, wall_ms / 1000 (unit s); 0 when the job never started.
++ */
++ seconds: number;
++ wall_ms: number;
++ /**
++ * Informational, never billed.
++ */
++ cpu_ms: number | null;
++ /**
++ * Informational, never billed.
++ */
++ mem_peak_bytes: number | null;
++ };
++ error: null;
++}
++
++/** [email protected] (owner: run) */
++/**
++ * run.job.started v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). The node sent job_started: the process runs. Usage seconds count from started_at. A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority low, actor service:run.
++ */
++export interface RunJobStartedPayload {
++ job_id: string;
++ project_id: string;
++ requester: SubjectRef;
++ /**
++ * The job state (run.job-read-result@1 state).
++ */
++ state: "running";
++ class: RuntimeClass;
++ artifact: {
++ name: string;
++ /**
++ * One exact version, never a range
++ */
++ version: string;
++ } | null;
++ /**
++ * OpenVibe.Node worker.egress values: none (no network), public (the internet without private, link-local or the network's own ranges), openvibe-only (the network's own endpoints only).
++ */
++ egress: "none" | "public" | "openvibe-only";
++ placement: {
++ /**
++ * The worker's device id (the usage reading's `node`).
++ */
++ node: string;
++ /**
++ * The offer's provider (the usage reading's `provider`); null for a first-party node.
++ */
++ provider: string | null;
++ region: string | null;
++ };
++ timings: {
++ created_at: string;
++ placed_at: string;
++ started_at: string;
++ finished_at: null;
++ };
++ exit: null;
++ usage: {
++ /**
++ * Metered wall-clock seconds, wall_ms / 1000 (unit s); 0 when the job never started.
++ */
++ seconds: number;
++ wall_ms: number;
++ /**
++ * Informational, never billed.
++ */
++ cpu_ms: number | null;
++ /**
++ * Informational, never billed.
++ */
++ mem_peak_bytes: number | null;
++ };
++ error: null;
++}
++
++/** [email protected] (owner: run) */
++/**
++ * run.job.succeeded v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). The job ended succeeded: job_exit reason exited with code 0. Carries no result (read it with GET /api/v1/jobs/{id}). A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority normal, actor service:run.
++ */
++export interface RunJobSucceededPayload {
++ job_id: string;
++ project_id: string;
++ requester: SubjectRef;
++ /**
++ * The job state (run.job-read-result@1 state).
++ */
++ state: "succeeded";
++ class: RuntimeClass;
++ artifact: {
++ name: string;
++ /**
++ * One exact version, never a range
++ */
++ version: string;
++ } | null;
++ /**
++ * OpenVibe.Node worker.egress values: none (no network), public (the internet without private, link-local or the network's own ranges), openvibe-only (the network's own endpoints only).
++ */
++ egress: "none" | "public" | "openvibe-only";
++ placement: {
++ /**
++ * The worker's device id (the usage reading's `node`).
++ */
++ node: string;
++ /**
++ * The offer's provider (the usage reading's `provider`); null for a first-party node.
++ */
++ provider: string | null;
++ region: string | null;
++ };
++ timings: {
++ created_at: string;
++ placed_at: string;
++ started_at: string;
++ finished_at: string;
++ };
++ exit: {
++ /**
++ * platform.job-frame@1 job_exit reason.
++ */
++ reason: "exited" | "cancelled" | "ttl" | "limit" | "stopped" | "failed";
++ /**
++ * The process's exit status; null when it was killed or never ran.
++ */
++ code: number | null;
++ } & {
++ reason?: "exited";
++ code?: 0;
++ };
++ usage: {
++ /**
++ * Metered wall-clock seconds, wall_ms / 1000 (unit s); 0 when the job never started.
++ */
++ seconds: number;
++ wall_ms: number;
++ /**
++ * Informational, never billed.
++ */
++ cpu_ms: number | null;
++ /**
++ * Informational, never billed.
++ */
++ mem_peak_bytes: number | null;
++ };
++ error: null;
++}
++
++/** [email protected] (owner: run) */
++/**
++ * run.job.failed v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). The job ended failed: job_exit with a non-zero code or reason limit, stopped or failed, or an input digest mismatch before the process started. A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority important, actor service:run.
++ */
++export interface RunJobFailedPayload {
++ job_id: string;
++ project_id: string;
++ requester: SubjectRef;
++ /**
++ * The job state (run.job-read-result@1 state).
++ */
++ state: "failed";
++ class: RuntimeClass;
++ artifact: {
++ name: string;
++ /**
++ * One exact version, never a range
++ */
++ version: string;
++ } | null;
++ /**
++ * OpenVibe.Node worker.egress values: none (no network), public (the internet without private, link-local or the network's own ranges), openvibe-only (the network's own endpoints only).
++ */
++ egress: "none" | "public" | "openvibe-only";
++ placement: {
++ /**
++ * The worker's device id (the usage reading's `node`).
++ */
++ node: string;
++ /**
++ * The offer's provider (the usage reading's `provider`); null for a first-party node.
++ */
++ provider: string | null;
++ region: string | null;
++ } | null;
++ timings: {
++ created_at: string;
++ placed_at: string | null;
++ started_at: string | null;
++ finished_at: string;
++ };
++ exit: {
++ /**
++ * platform.job-frame@1 job_exit reason.
++ */
++ reason: "exited" | "cancelled" | "ttl" | "limit" | "stopped" | "failed";
++ /**
++ * The process's exit status; null when it was killed or never ran.
++ */
++ code: number | null;
++ } | null;
++ usage: {
++ /**
++ * Metered wall-clock seconds, wall_ms / 1000 (unit s); 0 when the job never started.
++ */
++ seconds: number;
++ wall_ms: number;
++ /**
++ * Informational, never billed.
++ */
++ cpu_ms: number | null;
++ /**
++ * Informational, never billed.
++ */
++ mem_peak_bytes: number | null;
++ };
++ error: {
++ /**
++ * run.job.exit_nonzero, run.job.limit, run.job.stopped, run.job.worker_failed, run.job.input_digest_mismatch, run.job.unplaceable (expired: no node offered the class, egress and requirements), run.job.ttl.
++ */
++ code: string;
++ detail: string | null;
++ };
++}
++
++/** [email protected] (owner: run) */
++/**
++ * run.job.cancelled v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). The job ended cancelled (POST /api/v1/jobs/{id}/cancel by its project or run.job.admin, or job_exit reason cancelled). A job cancelled before placement has no placement and no exit. A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority normal, actor service:run.
++ */
++export interface RunJobCancelledPayload {
++ job_id: string;
++ project_id: string;
++ requester: SubjectRef;
++ /**
++ * The job state (run.job-read-result@1 state).
++ */
++ state: "cancelled";
++ class: RuntimeClass;
++ artifact: {
++ name: string;
++ /**
++ * One exact version, never a range
++ */
++ version: string;
++ } | null;
++ /**
++ * OpenVibe.Node worker.egress values: none (no network), public (the internet without private, link-local or the network's own ranges), openvibe-only (the network's own endpoints only).
++ */
++ egress: "none" | "public" | "openvibe-only";
++ placement: {
++ /**
++ * The worker's device id (the usage reading's `node`).
++ */
++ node: string;
++ /**
++ * The offer's provider (the usage reading's `provider`); null for a first-party node.
++ */
++ provider: string | null;
++ region: string | null;
++ } | null;
++ timings: {
++ created_at: string;
++ placed_at: string | null;
++ started_at: string | null;
++ finished_at: string;
++ };
++ exit: {
++ /**
++ * platform.job-frame@1 job_exit reason.
++ */
++ reason: "exited" | "cancelled" | "ttl" | "limit" | "stopped" | "failed";
++ /**
++ * The process's exit status; null when it was killed or never ran.
++ */
++ code: number | null;
++ } | null;
++ usage: {
++ /**
++ * Metered wall-clock seconds, wall_ms / 1000 (unit s); 0 when the job never started.
++ */
++ seconds: number;
++ wall_ms: number;
++ /**
++ * Informational, never billed.
++ */
++ cpu_ms: number | null;
++ /**
++ * Informational, never billed.
++ */
++ mem_peak_bytes: number | null;
++ };
++ error: null;
++}
++
++/** [email protected] (owner: run) */
++/**
++ * run.job.expired v1 (OpenVibe.Run; PLANNED: the Run service does not exist yet, plan T14 R1). The job ended expired: its ttl_ms ran out before a node took it (run.job.unplaceable), before it started, or while it ran (job_exit reason ttl, run.job.ttl). A projection of run.job-read-result@1 without args, inputs, result or output. Envelope: subject { type: job, id: <job_id> }, visibility internal, priority normal, actor service:run.
++ */
++export interface RunJobExpiredPayload {
++ job_id: string;
++ project_id: string;
++ requester: SubjectRef;
++ /**
++ * The job state (run.job-read-result@1 state).
++ */
++ state: "expired";
++ class: RuntimeClass;
++ artifact: {
++ name: string;
++ /**
++ * One exact version, never a range
++ */
++ version: string;
++ } | null;
++ /**
++ * OpenVibe.Node worker.egress values: none (no network), public (the internet without private, link-local or the network's own ranges), openvibe-only (the network's own endpoints only).
++ */
++ egress: "none" | "public" | "openvibe-only";
++ placement: {
++ /**
++ * The worker's device id (the usage reading's `node`).
++ */
++ node: string;
++ /**
++ * The offer's provider (the usage reading's `provider`); null for a first-party node.
++ */
++ provider: string | null;
++ region: string | null;
++ } | null;
++ timings: {
++ created_at: string;
++ placed_at: string | null;
++ started_at: string | null;
++ finished_at: string;
++ };
++ exit: {
++ /**
++ * platform.job-frame@1 job_exit reason.
++ */
++ reason: "exited" | "cancelled" | "ttl" | "limit" | "stopped" | "failed";
++ /**
++ * The process's exit status; null when it was killed or never ran.
++ */
++ code: number | null;
++ } | null;
++ usage: {
++ /**
++ * Metered wall-clock seconds, wall_ms / 1000 (unit s); 0 when the job never started.
++ */
++ seconds: number;
++ wall_ms: number;
++ /**
++ * Informational, never billed.
++ */
++ cpu_ms: number | null;
++ /**
++ * Informational, never billed.
++ */
++ mem_peak_bytes: number | null;
++ };
++ error: {
++ /**
++ * run.job.exit_nonzero, run.job.limit, run.job.stopped, run.job.worker_failed, run.job.input_digest_mismatch, run.job.unplaceable (expired: no node offered the class, egress and requirements), run.job.ttl.
++ */
++ code: string;
++ detail: string | null;
++ };
++}
ddiff --git a/lib/ids.js b/lib/ids.js
iindex eb43b2df..5f552ed3 100644
---- a/lib/ids.js
++++ b/lib/ids.js
@@@ -6,7 +6,7 @@
const crypto = require('crypto');
const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
--const PREFIX = { user: 'usr', guest: 'gst', app: 'app', mod: 'mod', agent: 'agt', node: 'nod', event: 'evt', media: 'med', project: 'prj', confirmation: 'cnf', watch: 'wch', observation: 'wco', check: 'ckr' };
++const PREFIX = { user: 'usr', guest: 'gst', app: 'app', mod: 'mod', agent: 'agt', node: 'nod', event: 'evt', media: 'med', project: 'prj', confirmation: 'cnf', job: 'job', watch: 'wch', observation: 'wco', check: 'ckr' };
const SLUG_TYPES = new Set(['service', 'system']);
const ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
const SLUG_RE = /^[a-z][a-z0-9-]{1,39}$/;
ddiff --git a/manifests/capabilities/run.job.admin.json b/manifests/capabilities/run.job.admin.json
nnew file mode 100644
iindex 00000000..367cfb68
---- /dev/null
++++ b/manifests/capabilities/run.job.admin.json
@@@ -0,0 +1,22 @@
++{
++ "id": "run.job.admin",
++ "version": "1.0",
++ "owner": "run",
++ "status": "planned",
++ "visibility": "internal",
++ "sensitive": true,
++ "description": "PLANNED (plan T14 R1; OpenVibe.Run is not built yet): Operator access to every project's jobs: GET /api/v1/admin/jobs (run.job-list-query@1 with project_id), and the read and cancel routes for any job. Never delegated and never in an app or mod token. It becomes active, and enters generated/openapi/run.json, when OpenVibe.Run implements the routes.",
++ "inputSchema": "run.job-list-query@1",
++ "outputSchema": "run.job-list-result@1",
++ "permissions": [
++ "run:admin"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "run-admin",
++ "events": [
++ "run.job.cancelled"
++ ],
++ "implementedBy": []
++}
ddiff --git a/manifests/capabilities/run.job.cancel.json b/manifests/capabilities/run.job.cancel.json
nnew file mode 100644
iindex 00000000..4deb6e9a
---- /dev/null
++++ b/manifests/capabilities/run.job.cancel.json
@@@ -0,0 +1,21 @@
++{
++ "id": "run.job.cancel",
++ "version": "1.0",
++ "owner": "run",
++ "status": "planned",
++ "visibility": "public",
++ "description": "PLANNED (plan T14 R1; OpenVibe.Run is not built yet): Cancel one of the token project's jobs: POST /api/v1/jobs/:id/cancel. A queued or placed job never starts; a running one is killed (platform.job-frame@1 job_cancel); cancelling an ended job changes nothing and answers it as it is. It becomes active, and enters generated/openapi/run.json, when OpenVibe.Run implements the routes.",
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "run.job-read-result@1",
++ "permissions": [
++ "run:cancel"
++ ],
++ "resourceConstraints": [
++ "project"
++ ],
++ "quotaClass": "run-control",
++ "events": [
++ "run.job.cancelled"
++ ],
++ "implementedBy": []
++}
ddiff --git a/manifests/capabilities/run.job.list.json b/manifests/capabilities/run.job.list.json
nnew file mode 100644
iindex 00000000..28231d02
---- /dev/null
++++ b/manifests/capabilities/run.job.list.json
@@@ -0,0 +1,19 @@
++{
++ "id": "run.job.list",
++ "version": "1.0",
++ "owner": "run",
++ "status": "planned",
++ "visibility": "public",
++ "description": "PLANNED (plan T14 R1; OpenVibe.Run is not built yet): List the token project's jobs, newest first and cursor-paged: GET /api/v1/jobs?state=&limit=&cursor=. It becomes active, and enters generated/openapi/run.json, when OpenVibe.Run implements the routes.",
++ "inputSchema": "run.job-list-query@1",
++ "outputSchema": "run.job-list-result@1",
++ "permissions": [
++ "run:read"
++ ],
++ "resourceConstraints": [
++ "project"
++ ],
++ "quotaClass": "run-read",
++ "events": [],
++ "implementedBy": []
++}
ddiff --git a/manifests/capabilities/run.job.read.json b/manifests/capabilities/run.job.read.json
nnew file mode 100644
iindex 00000000..26fe1bd0
---- /dev/null
++++ b/manifests/capabilities/run.job.read.json
@@@ -0,0 +1,19 @@
++{
++ "id": "run.job.read",
++ "version": "1.0",
++ "owner": "run",
++ "status": "planned",
++ "visibility": "public",
++ "description": "PLANNED (plan T14 R1; OpenVibe.Run is not built yet): Read one of the token project's jobs: GET /api/v1/jobs/:id (state, placement, timings, exit, usage seconds, result). It becomes active, and enters generated/openapi/run.json, when OpenVibe.Run implements the routes.",
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "run.job-read-result@1",
++ "permissions": [
++ "run:read"
++ ],
++ "resourceConstraints": [
++ "project"
++ ],
++ "quotaClass": "run-read",
++ "events": [],
++ "implementedBy": []
++}
ddiff --git a/manifests/capabilities/run.job.stream.json b/manifests/capabilities/run.job.stream.json
nnew file mode 100644
iindex 00000000..09a8656d
---- /dev/null
++++ b/manifests/capabilities/run.job.stream.json
@@@ -0,0 +1,19 @@
++{
++ "id": "run.job.stream",
++ "version": "1.0",
++ "owner": "run",
++ "status": "planned",
++ "visibility": "public",
++ "description": "PLANNED (plan T14 R1; OpenVibe.Run is not built yet): Watch a job's live output: POST /api/v1/jobs/:id/stream/ticket mints a one-use, two-minute stream ticket (run.job-stream-ticket-result@1) and GET /api/v1/jobs/:id/stream?ticket= (or with a Bearer token) is text/event-stream of run.job-stream-event@1, resumable with Last-Event-ID. It becomes active, and enters generated/openapi/run.json, when OpenVibe.Run implements the routes.",
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "run.job-stream-ticket-result@1",
++ "permissions": [
++ "run:stream"
++ ],
++ "resourceConstraints": [
++ "project"
++ ],
++ "quotaClass": "run-stream",
++ "events": [],
++ "implementedBy": []
++}
ddiff --git a/manifests/capabilities/run.job.submit.json b/manifests/capabilities/run.job.submit.json
nnew file mode 100644
iindex 00000000..1b9564c5
---- /dev/null
++++ b/manifests/capabilities/run.job.submit.json
@@@ -0,0 +1,26 @@
++{
++ "id": "run.job.submit",
++ "version": "1.0",
++ "owner": "run",
++ "status": "planned",
++ "visibility": "public",
++ "sensitive": true,
++ "description": "PLANNED (plan T14 R1; OpenVibe.Run is not built yet): Submit a job to OpenVibe.Run: POST /api/v1/jobs. The job belongs to the token's project, which is billed per wall-clock second it runs (service run, operation function.invoke, unit s). An idempotency_key makes a retry return the first job. It becomes active, and enters generated/openapi/run.json, when OpenVibe.Run implements the routes.",
++ "inputSchema": "run.job-create-request@1",
++ "outputSchema": "run.job-create-result@1",
++ "permissions": [
++ "run:submit"
++ ],
++ "resourceConstraints": [
++ "project"
++ ],
++ "quotaClass": "run-submit",
++ "events": [
++ "run.job.queued",
++ "run.job.started",
++ "run.job.succeeded",
++ "run.job.failed",
++ "run.job.expired"
++ ],
++ "implementedBy": []
++}
ddiff --git a/manifests/products/openvibe.run.json b/manifests/products/openvibe.run.json
iindex f13f3c6e..760d90f5 100644
---- a/manifests/products/openvibe.run.json
++++ b/manifests/products/openvibe.run.json
@@@ -27,6 +27,7 @@
"keywords": "openvibe run, code sandbox, run code online, serverless functions, ai agent tasks",
"launch": "the sandbox runtime",
"relationships": {
++ "service": "run",
"noRepo": true,
"meanwhile": [
{
ddiff --git a/manifests/services/run.json b/manifests/services/run.json
nnew file mode 100644
iindex 00000000..d56310e3
---- /dev/null
++++ b/manifests/services/run.json
@@@ -0,0 +1,58 @@
++{
++ "version": "0.0.0",
++ "contractRanges": {
++ "openvibe-contracts": ">=0.90.0 <1.0.0"
++ },
++ "id": "run",
++ "name": "OpenVibe.Run",
++ "status": "placeholder",
++ "repository": "OpenVibers/OpenVibe.Run",
++ "domains": [
++ "openvibe.run"
++ ],
++ "publicOrigin": "https://openvibe.run",
++ "exposure": {
++ "state": "placeholder",
++ "publicSite": null,
++ "note": "planned (plan T14 R1); the OpenVibe.Run repository does not exist yet, and nothing serves openvibe.run"
++ },
++ "capabilities": [
++ "run.job.submit",
++ "run.job.read",
++ "run.job.list",
++ "run.job.cancel",
++ "run.job.stream",
++ "run.job.admin"
++ ],
++ "eventsProduced": [
++ "run.job.queued",
++ "run.job.started",
++ "run.job.succeeded",
++ "run.job.failed",
++ "run.job.cancelled",
++ "run.job.expired"
++ ],
++ "eventsConsumed": [],
++ "namespacesOwned": [],
++ "lifecycle": {
++ "liveness": {
++ "none": "not built yet; nothing listens on a loopback address"
++ },
++ "shutdown": {
++ "none": "not built yet; nothing runs or drains"
++ },
++ "startupRecovery": {
++ "none": "not built yet; nothing starts"
++ },
++ "rollback": {
++ "none": "not built yet; nothing is released or rolled back"
++ },
++ "contracts": {
++ "range": ">=0.90.0 <1.0.0"
++ },
++ "leases": {
++ "none": "not built yet; nothing runs"
++ }
++ },
++ "notes": "The job service of the network (plan T14, ADR-034): accepts jobs (run.job-create-request@1), places each on a node that advertises worker:<class> (platform.resource-offer@1, platform.runtime-offer@1, platform.workload-requirements@1), sends it as platform.job@1 over the node link and follows its platform.job-frame@1 frames. Consumes billing.usage.record to write one usage reading per wall-clock second (run:<job id>:<n>, service run, operation function.invoke, unit s) and media.object.read to resolve digest-pinned inputs. Planned: nothing is built."
++}
ddiff --git a/package-lock.json b/package-lock.json
iindex e1c47af5..be4bb30b 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-contracts",
-- "version": "0.89.0",
++ "version": "0.90.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-contracts",
-- "version": "0.89.0",
++ "version": "0.90.0",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 94b1ae08..1793a8ba 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.89.0",
++ "version": "0.90.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
ddiff --git a/test/platform-run.test.js b/test/platform-run.test.js
iindex 98cad41a..60b9d5cd 100644
---- a/test/platform-run.test.js
++++ b/test/platform-run.test.js
@@@ -74,4 +74,25 @@ checks += 3;
// job_usage only ever stands for a full second: it has no quantity of its own.
ok(!contracts.validate('platform.job-frame', { ...usage, quantity: 0.5 }).valid, 'a job_usage frame cannot carry a partial quantity');
++// OpenVibe.Run's job API (T14 R1): Run mints the platform.job@1 id, so its job ids, artifacts and limits are the
++// job's own; a job's usage seconds are the sum of its readings; every run.* capability and event is Run's.
++const create = contracts.schema('run.job-create-request');
++const job = contracts.schema('platform.job');
++ok(contracts.schema('run.job-read-result').properties.id.pattern === job.properties.id.pattern, 'a Run job id is a platform.job@1 id');
++assert.deepStrictEqual(create.$defs.artifact.properties, job.properties.artifact.properties);
++assert.deepStrictEqual(create.$defs.limits.properties, job.properties.limits.properties);
++assert.deepStrictEqual(create.$defs.egress.enum, ['none', 'public', 'openvibe-only']);
++checks += 3;
++ok(contracts.ids.newId('job').match(job.properties.id.pattern), 'newId(job) mints a platform.job@1 id');
++const succeeded = fixture('run.job-read-result', 'valid', 'succeeded');
++ok(succeeded.usage.seconds === fromExit(exit).reduce((s, r) => s + r.quantity, 0) && succeeded.usage.wall_ms === exit.usage.wall_ms, 'usage.seconds is the sum of the job\'s readings');
++const run = contracts.services.get('run');
++ok(run && run.status === 'placeholder' && contracts.products.get('openvibe.run').relationships.noRepo === true, 'run is a placeholder service and openvibe.run still has no repository');
++for (const id of ['run.job.submit', 'run.job.read', 'run.job.list', 'run.job.cancel', 'run.job.stream', 'run.job.admin']) {
++ const c = contracts.capabilities.get(id);
++ ok(c && c.owner === 'run' && c.status === 'planned' && c.description.startsWith('PLANNED') && run.capabilities.includes(id), `${id} is a planned capability Run serves`);
++}
++ok(contracts.capabilities.get('run.job.admin').visibility === 'internal', 'run.job.admin is internal');
++for (const s of ['queued', 'started', 'succeeded', 'failed', 'cancelled', 'expired']) ok(run.eventsProduced.includes(`run.job.${s}`), `run produces run.job.${s}`);
++
console.log(`platform run: ${checks} job, frame and metering checks passed`);
ddiff --git a/test/run.js b/test/run.js
iindex ec711c05..7211e2ab 100644
---- a/test/run.js
++++ b/test/run.js
@@@ -529,7 +529,7 @@ ok(nodeId.startsWith('nod_') && ids.principalSub({ type: 'node', id: nodeId }) =
assert.throws(() => ids.principalSub({ type: 'node', id: 'nod_42' }), /principals/);
ok(!ids.SUBJECT_TYPES.includes('node') && ids.parseSubject(`node:${nodeId}`) === null, 'a node is a principal, never a subject');
assert.throws(() => ids.principalSub({ type: 'user', id: ids.newId('user') }), /principals/);
--for (const [kind, prefix] of [['watch', 'wch'], ['observation', 'wco'], ['check', 'ckr']]) ok(ids.newId(kind).startsWith(`${prefix}_`), `newId(${kind}) is ${prefix}_<ULID>`);
++for (const [kind, prefix] of [['watch', 'wch'], ['observation', 'wco'], ['check', 'ckr'], ['job', 'job']]) ok(ids.newId(kind).startsWith(`${prefix}_`), `newId(${kind}) is ${prefix}_<ULID>`);
ok(contracts.validate('media.media-ref', { media_id: ids.legacyMediaId('live', 'vod', 42) }).valid, 'legacyMediaId is a valid MediaRef');
ok(contracts.validate('events.event-envelope', { event_id: ids.newId('event'), event_type: 'network.user.created', version: 1, source: 'network', actor: { type: 'system', id: 'network' }, timestamp: new Date().toISOString(), subject: { type: 'user', id: ids.newId('user') }, payload: {} }).valid, 'newId(event) builds a valid envelope');