openvibers/openvibe.contracts Commits

ADR-044 (Actor runtime) and the planned platform.task@1 contract; release 0.101.0 (#46)

OP OpenVibers committed 5 Oct 2026 · 5d98483093 · 50 files, +2131 −59

* ADR-044 (Actor runtime) and the planned platform.task@1 contract; release 0.101.0

**Owner of `platform.task`: `network`.** `manifests/services/` has no actor manifest. OpenVibe.Actor exists only as a product page, `manifests/products/openvibe.actor.json` (`relationships.noRepo: true`), so there is no service to own the contract; every other `platform.*` contract is owned by `network`, and the ADR says so (§Context, §1). No line-number in the brief differed from the code, except that this absence was confirmed rather than assumed; the two paths the brief cited were re-read and match (`test/run.js:42` catalog check, `test/run.js:66-77` fixtures check; the 0.100.0 commit changed `package.json`, `package-lock.json`, `STATUS.json`, `CHANGELOG.md` and `generated/` exactly as described).

### Changes (file:line)

**New contract + fixtures**
- `contracts/platform/task.v1.json:1` — new `platform.task@1`: task resource for `POST /v1/tasks` / `GET /v1/tasks/{id}` (`id` `tsk_<ULID>`, `project_id`, `requester`, `task`, `mode` cheapest/balanced/best/fastest/private, `budget.per_task_usd`/`per_day_usd`, `state` queued/running/verifying/succeeded/failed/cancelled, `result`, `cost`, `explanation` chaining `platform.placement-result@1`, SSE `progress`, `cancel`, HTTPS `webhooks`); `allOf` ties `succeeded` to a non-null result/cost/explanation, `else` to a null result, and `cancelled` to a cancel record; 3 examples.
- `fixtures/platform.task/valid/{queued,running,succeeded,cancelled}.json:1` — passing examples.
- `fixtures/platform.task/invalid/{missing-task,unknown-mode,budget-missing-per-day,negative-budget,succeeded-without-result,failed-with-result,cancelled-without-cancel,plain-http-webhook}.json:1` — failing counter-examples.

**ADR**
- `docs/adr/ADR-044-actor-runtime.md:1` — new Proposed ADR (plan T17, after the paused engine): task contract; adapter interface (capabilities, limits, reviewed rate card, trust class, BYO keys — wire shape left open); modes as `openvibe-sdk/placement` objectives; signals/auto-balancing with hysteresis and failover; `verifying` state for cheap-first cross-family checks and the task's own tests; trust classes and `private`; progress/cancel/webhooks; budgets/metering; authority boundaries; open questions; "what this does not claim".
- `docs/adr/README.md:43` — table row for ADR-044.

**Catalog, ids, release**
- `contracts/catalog.json:5623` — `platform.task` entry (v1.0.0, owner `network`, public, `planned`, adr `ADR-044`).
- `lib/ids.js:9` — added the `task: 'tsk'` prefix so `ids.newId('task')` mints the contract's id.
- `package.json:3`, `package-lock.json:3,9` — version `0.101.0`.
- `STATUS.json:10,23,25` — contracts v0.101.0, new feature line, updated date.
- `CHANGELOG.md:7` — new `0.101.0 — 2026-10-05` section.
- `generated/` (bundle.json, typescript/index.d.ts, 26 openapi docs) — `npm run generate`; openapi diffs are the version line only, the bundle/types are additive.

### Test results
- `npm run generate` → written; `node scripts/generate.js --check` → `generated/: up to date`, exit 0.
- `node scripts/compat.js v0.100.0` → `no breaking changes`, exit 0.
- `ov test test/platform-fabric.test.js test/product-catalog.test.js` → 2 passed (23 s).
- Focused replica of `test/run.js`'s catalog-integrity, all-fixture and generated-types checks → **11660 passed** (the environment hook blocks invoking `node test/run.js` as a whole suite, so this ran those exact checks; an earlier `ov test` pass had surfaced a real failure, `generated types reference undeclared names: SubjectRef1`, caused by a `$ref` wit

* Test the tsk prefix; leave 'best' unmapped until the SDK ranks quality (review)

Both findings fixed.

- **Finding 1** — added `['task', 'tsk']` to the id-prefix check loop: `test/run.js:534`. Verified `ids.newId('task')` → `tsk_<ULID>` (`lib/ids.js:9` already carried the prefix).
- **Finding 2** — ADR-044 §3 no longer maps `best → correctness`; it states only the four direct mappings (`cheapest`, `balanced`, `fastest`, `private`) and says `best` is unmapped because the SDK's `correctness` objective keeps only `req.authority`/current placement (ranks like `balanced` for a new task) and `high-reliability` has no branch: `docs/adr/ADR-044-actor-runtime.md:81-86`. Recorded as open question 6 at `docs/adr/ADR-044-actor-runtime.md:174`.

Left `docs/adr/README.md` and its #45 conflict untouched, as instructed. No commit or push.

Tests: `ov test test/platform-fabric.test.js test/platform-run.test.js` → 1 passed (`platform-run`), 1 skipped by the tool (`platform-fabric`, unchanged since green), 0 failed, 23 s.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 7d6fbca4..192b9da7 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,25 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
the notes were in the tag and commit messages (`git tag -n1`).
++## 0.101.0 — 2026-10-05
++
++**The task contract** (plan T17, ADR-044). Additive: one new planned contract and its ADR, no existing schema changed.
++- `platform.task@1` (new, owner `network`, public, **planned**): one task at OpenVibe.Actor's router — the resource
++ `POST /v1/tasks` creates and `GET /v1/tasks/{id}` reads, modelled on Run's `platform.job@1`. It carries `id`
++ (`tsk_<ULID>`), `project_id`, `requester` (a person or an `agent` subject), the `task` text, `mode`
++ (`cheapest | balanced | best | fastest | private`), `budget` (`per_task_usd`, `per_day_usd`), `state`
++ (`queued | running | verifying | succeeded | failed | cancelled`), `created_at`/`finished_at`, `result`, `cost`,
++ `explanation` (a chain of `platform.placement-result@1`, an escalation appending one), the SSE `progress`
++ descriptor, the `cancel` record and registered `webhooks` (HTTPS only, secret by reference). Valid and invalid
++ fixtures in `fixtures/platform.task/`.
++- `docs/adr/ADR-044-actor-runtime.md` (new, **Proposed**; plan T17, the build-order item after the engine): the task
++ contract, the adapter interface (capabilities, limits, a reviewed rate card, trust class and BYO keys), modes as
++ `openvibe-sdk/placement` objectives, live signals with hysteresis and immediate failover, a `verifying` state for
++ cheap-first cross-family checks and the task's own tests, and the five ADR-046 trust classes with `private`
++ narrowing to OpenVibe-only. Owner is `network` because no Actor service manifest exists: OpenVibe.Actor is a product
++ page (`manifests/products/openvibe.actor.json`, `noRepo`), and every other `platform.*` contract is Network's too.
++- `lib/ids.js`: the `task` prefix (`tsk`), so `ids.newId('task')` mints the contract's id.
++
## 0.100.0 — 2026-10-05
**Three small contract fixes** (plan follow-ups). Additive and widening, no contract removed.
ddiff --git a/STATUS.json b/STATUS.json
iindex 7b01d508..8ec4b88d 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -7,7 +7,7 @@
"plan": "OpenVibe Development Roadmap (combined), Wave 1; implementation plan rev 3 section 3.1",
"deployed": false,
"deployment": "none of its own: a library and artifact repository published as release tags; services pin a tag and deploy it with their own releases",
-- "contracts": "openvibe-contracts v0.100.0 (this package; the newest tag)",
++ "contracts": "openvibe-contracts v0.101.0 (this package; the newest tag)",
"features": [
"589 JSON Schemas with valid and invalid fixtures",
"31 service manifests with lifecycle declarations",
@@@ -19,7 +19,8 @@
"compatibility check against the previous tag (scripts/compat.js)",
"documentation-currency check (scripts/docs-currency.js)",
"generated estate: docs/ESTATE.md and manifests/repositories/ from the checkouts (scripts/estate.js; --check fails on drift)",
-- "chat.ticket-conversation@1 and the planned chat.ticket.write capability: a per-ticket conversation keyed by ticket id and calling service, with author_kind person|agent|staff"
++ "chat.ticket-conversation@1 and the planned chat.ticket.write capability: a per-ticket conversation keyed by ticket id and calling service, with author_kind person|agent|staff",
++ "platform.task@1 (planned, owner network) and ADR-044: one task at OpenVibe.Actor's router with a mode, a per-task and per-day budget, result, cost and explanation, SSE progress, cancel and webhooks"
],
"updated": "2026-10-05"
}
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 0ff19b56..0275a5ae 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -5619,6 +5619,16 @@
"status": "active",
"adr": "ADR-034"
},
++ {
++ "id": "platform.task",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "platform/task.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "planned",
++ "adr": "ADR-044"
++ },
{
"id": "events.delivery-policy",
"version": "1.0.0",
ddiff --git a/contracts/platform/task.v1.json b/contracts/platform/task.v1.json
nnew file mode 100644
iindex 00000000..69e0ab5d
---- /dev/null
++++ b/contracts/platform/task.v1.json
@@@ -0,0 +1,428 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/platform/task.v1.json",
++ "title": "Task",
++ "description": "platform.task@1 (PLANNED; plan T17, ADR-044): one task at OpenVibe.Actor's task router. POST /v1/tasks creates it from a plain-language `task` plus a `mode` and a `budget`; the router mints `id`, sets `state` and `created_at`, picks a backend from the registered adapters, runs the task, verifies the result, escalates or fails over, and answers with `result`, `cost` and `explanation`. GET /v1/tasks/{id} reads the same resource. Progress streams as server-sent events from `progress.stream_url` (a task's own counter in `progress.last_seq`, as platform.job-frame@1 counts on a link); `cancel` records that a cancel was requested (POST /v1/tasks/{id}/cancel), and `webhooks` are the registered endpoints the router delivers each state change to. No Actor service exists yet: OpenVibe.Actor is a product page (manifests/products/openvibe.actor.json, noRepo) with no service manifest, so the contract is owned by `network`, like every other platform.* contract, and stays PLANNED until T17 ships. This v1 contract may grow optional fields.",
++ "$comment": "Additive optional fields may extend this v1 contract. It is the task resource, not a device frame: Actor's link is HTTP/SSE where Run's is the Node WebSocket.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "project_id",
++ "requester",
++ "task",
++ "mode",
++ "budget",
++ "state",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "Minted by the router (tsk_<ULID>): the idempotency key of the task and the stem of its usage readings, as platform.job@1's id is for a job. Never reused."
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The project the caller's token names (ADR-034 §5): budgets, usage and the bill are the project's."
++ },
++ "requester": {
++ "description": "The person or agent the task belongs to: a user subject for a person, an agent subject for an agent acting under the grants its owner delegated (roadmap WS-Z2).",
++ "allOf": [
++ {
++ "$ref": "../identity/subject-ref.v1.json"
++ }
++ ]
++ },
++ "task": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 100000,
++ "description": "The instruction in plain words, exactly as the caller gave it. Not a backend command and not a model prompt: the planner turns it into one."
++ },
++ "mode": {
++ "type": "string",
++ "enum": [
++ "cheapest",
++ "balanced",
++ "best",
++ "fastest",
++ "private"
++ ],
++ "default": "balanced",
++ "description": "The plan's five modes, in contract spelling: Cheapest, Balanced (the default), Best, Fastest and Private are `cheapest`, `balanced`, `best`, `fastest` and `private`. A mode is the planner objective, never a fixed backend list (plan T17). `cheapest`: the lowest expected cost that still meets the task class's quality bar. `balanced` (the default): cost, quality and speed weighed together. `best`: the highest chance of a right answer first time. `fastest`: the shortest time to a checked result. `private`: only backends that keep the data on OpenVibe (first-party, and a user-owned node only when the task names it), per ADR-046."
++ },
++ "budget": {
++ "type": "object",
++ "additionalProperties": false,
++ "description": "Hard limits the router never exceeds (plan T17: 'a hard limit per task and per day'). A task whose cheapest eligible backend would take it over `per_task_usd` ends failed (actor.budget.exceeded) rather than running; `per_day_usd` caps the project's tasks in one UTC day and a new task waits or fails, it never overruns.",
++ "required": [
++ "per_task_usd",
++ "per_day_usd"
++ ],
++ "properties": {
++ "per_task_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "per_day_usd": {
++ "type": "number",
++ "minimum": 0
++ }
++ }
++ },
++ "state": {
++ "type": "string",
++ "enum": [
++ "queued",
++ "running",
++ "verifying",
++ "succeeded",
++ "failed",
++ "cancelled"
++ ],
++ "description": "queued: stored, waiting for the planner. running: a backend is working, or a cascade is between attempts. verifying: a result exists and a check (the task's own tests, a citation check, a postcondition) runs before it is delivered. succeeded, failed and cancelled are the end states; an end state never changes."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "finished_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "When the task reached its end state; null before."
++ },
++ "result": {
++ "description": "The checked deliverable (any JSON, as a platform.job@1 `result` is): for a code task a diff or branch reference, for research a brief with citations, for an action a postcondition report. Never a credential and never raw sandbox output. Null (or absent) in every state but succeeded, which the allOf below enforces."
++ },
++ "cost": {
++ "anyOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "usd"
++ ],
++ "properties": {
++ "usd": {
++ "type": "number",
++ "minimum": 0,
++ "description": "What this task was billed, to the cent, across every attempt it made."
++ },
++ "free_allowance_used": {
++ "type": "number",
++ "minimum": 0,
++ "description": "The part of `usd` covered by the project's free allowance; metered as platform.usage-sample@1."
++ }
++ }
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "Present once the task has run an attempt (also when it ends failed or cancelled); null while queued. One key, one bill (plan T17), through Billing (ADR-012)."
++ },
++ "explanation": {
++ "anyOf": [
++ {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "$ref": "placement-result.v1.json"
++ }
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "One platform.placement-result@1 per backend the planner tried, oldest first, an escalation appending a new one: which backend won, why and what else was considered, from openvibe-sdk/placement (ADR-046 §1). Null while queued."
++ },
++ "progress": {
++ "anyOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "stream_url",
++ "last_seq"
++ ],
++ "properties": {
++ "stream_url": {
++ "type": "string",
++ "format": "uri",
++ "description": "The text/event-stream endpoint that carries this task's progress. Each event mirrors platform.job-frame@1's job_stdout/state shape: a state change, an output chunk, or the end."
++ },
++ "last_seq": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "The task's own event counter, from 0 before the first event; a reconnecting reader resumes after it, as a job frame's seq counts on one link."
++ },
++ "events": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "enum": [
++ "output",
++ "state",
++ "end"
++ ]
++ },
++ "description": "The event kinds the stream delivers, run.job-stream-event@1's own three."
++ }
++ }
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "Streaming progress (plan T17). Null before the router opens a stream and after the end."
++ },
++ "cancel": {
++ "anyOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "requested_at"
++ ],
++ "properties": {
++ "requested_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When POST /v1/tasks/{id}/cancel was accepted. The running backend is stopped as its own contract allows; the state becomes cancelled and `finished_at` is set."
++ },
++ "requested_by": {
++ "$ref": "../identity/subject-ref.v1.json"
++ }
++ }
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "Cancellation (plan T17): null until a cancel is requested, then it records who asked and when; the task's outcome is `state`."
++ },
++ "webhooks": {
++ "type": "array",
++ "maxItems": 8,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "url",
++ "events"
++ ],
++ "properties": {
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://",
++ "description": "The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused."
++ },
++ "events": {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "type": "string",
++ "enum": [
++ "queued",
++ "running",
++ "verifying",
++ "succeeded",
++ "failed",
++ "cancelled"
++ ]
++ },
++ "description": "The state changes delivered; the same names as `state`."
++ },
++ "secret_ref": {
++ "type": "string",
++ "pattern": "^[A-Z0-9_]+$",
++ "description": "The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference."
++ }
++ }
++ },
++ "description": "Webhooks (plan T17), registered at creation. A delivery that fails is retried with backoff and never delays the task; the task's own record is the source of truth."
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "required": [
++ "state"
++ ],
++ "properties": {
++ "state": {
++ "const": "succeeded"
++ }
++ }
++ },
++ "then": {
++ "required": [
++ "result",
++ "cost",
++ "explanation"
++ ],
++ "properties": {
++ "result": {
++ "not": {
++ "type": "null"
++ }
++ },
++ "cost": {
++ "type": "object"
++ },
++ "explanation": {
++ "type": "array"
++ }
++ }
++ },
++ "else": {
++ "properties": {
++ "result": {
++ "type": "null"
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "required": [
++ "state"
++ ],
++ "properties": {
++ "state": {
++ "const": "cancelled"
++ }
++ }
++ },
++ "then": {
++ "required": [
++ "cancel"
++ ]
++ }
++ }
++ ],
++ "examples": [
++ {
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS"
++ },
++ "task": "Research five competitors of my shop and write a one-page brief with sources.",
++ "mode": "balanced",
++ "budget": {
++ "per_task_usd": 5,
++ "per_day_usd": 50
++ },
++ "state": "queued",
++ "created_at": "2026-10-05T09:00:00.000Z",
++ "webhooks": [
++ {
++ "url": "https://example.com/hooks/actor",
++ "events": [
++ "succeeded",
++ "failed"
++ ],
++ "secret_ref": "ACTOR_WEBHOOK_SECRET"
++ }
++ ]
++ },
++ {
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS"
++ },
++ "task": "Fix the failing thumbnail test in OpenVibe.Media and open a PR.",
++ "mode": "best",
++ "budget": {
++ "per_task_usd": 5,
++ "per_day_usd": 50
++ },
++ "state": "running",
++ "created_at": "2026-10-05T09:00:00.000Z",
++ "progress": {
++ "stream_url": "https://openvibe.actor/v1/tasks/tsk_01JAB2C3D4E5F6G7H8J9K0MNPR/events",
++ "last_seq": 12,
++ "events": [
++ "output",
++ "state",
++ "end"
++ ]
++ },
++ "explanation": [
++ {
++ "selected": "openvibe-runtime",
++ "objective": "correctness",
++ "reasons": [
++ "the task is code; the Codes adapter exposes a code harness"
++ ],
++ "candidates": [
++ {
++ "id": "openvibe-runtime",
++ "eligible": true,
++ "estimated_cost_usd": 0.06,
++ "estimated_latency_ms": 180000
++ }
++ ],
++ "decided_at": "2026-10-05T09:00:01.000Z"
++ }
++ ]
++ },
++ {
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "agent",
++ "id": "agt_01JAB2C3D4E5F6G7H8J9K0MNPT"
++ },
++ "task": "Check this week's outage report and summarize the causes.",
++ "mode": "cheapest",
++ "budget": {
++ "per_task_usd": 1,
++ "per_day_usd": 10
++ },
++ "state": "succeeded",
++ "created_at": "2026-10-05T09:00:00.000Z",
++ "finished_at": "2026-10-05T09:06:00.000Z",
++ "result": {
++ "brief": "Two outages; both were a saturated connection pool.",
++ "citations": [
++ "https://openvibe.status/incidents/41"
++ ]
++ },
++ "cost": {
++ "usd": 0.06,
++ "free_allowance_used": 0.06
++ },
++ "explanation": [
++ {
++ "selected": "open-model",
++ "objective": "cheapest",
++ "reasons": [
++ "research task; the checks are citations, met by the cheapest candidate"
++ ],
++ "candidates": [
++ {
++ "id": "open-model",
++ "eligible": true,
++ "estimated_cost_usd": 0.06,
++ "estimated_latency_ms": 420000
++ }
++ ],
++ "decided_at": "2026-10-05T09:00:01.000Z"
++ }
++ ],
++ "cancel": null,
++ "progress": null
++ }
++ ]
++}
ddiff --git a/docs/adr/ADR-044-actor-runtime.md b/docs/adr/ADR-044-actor-runtime.md
nnew file mode 100644
iindex 00000000..bb8caeae
---- /dev/null
++++ b/docs/adr/ADR-044-actor-runtime.md
@@@ -0,0 +1,192 @@
++# ADR-044: The Actor runtime — the task contract, adapter interface, modes, signals, verification and trust
++
++**Status:** Proposed 2026-10-05 (plan track T17). This is the build-order item immediately after the engine: plan
++T17's build order puts "the engine first, where it already runs — harness adapter interface in `openvibe-agents` with
++the first new adapters (OpenCode, Antigravity CLI), capability-aware routing, vision inputs and a local sandbox
++(headless browser + virtual desktop, screenshots)" ahead of "ADR-044 (task contract, adapter interface, modes,
++signals, verification, trust)", and the engine's harness work is paused where it stands. Contract: `platform.task@1`.
++Builds on ADR-034 §2 (projects and resource names), §5 (control and data plane) and §9 (metering), on ADR-046 (the
++placer, its objectives and the five trust classes), on ADR-012 (one Billing authority) and on ADR-043 (Node control
++levels and the local kill switch). For the owner's review.
++
++## Context and current evidence
++
++- **Plan T17** describes the product: "one API and one site where any task goes to the best agent system for it, the
++ way OpenRouter does it for models — and one console where people command every OpenVibe resource in plain
++ language." Its first layer is the task router: "`POST /v1/tasks` with a task, a mode and a budget → the planner
++ picks the backend, runs it, verifies the result, escalates or fails over, and answers with the result, the cost and
++ the explanation. Streaming progress, cancellation, webhooks."
++- **The router has no contract.** `contracts/platform/` holds the Fabric's offers, requirements, placement results,
++ rate cards, telemetry and usage samples, and the Run sibling's `platform.job@1` and `platform.job-frame@1`; none of
++ them is a task. There is no `task.v1.json`, no route contract for `POST /v1/tasks`, and no adapter shape written
++ down.
++- **There is no Actor service to own it.** `manifests/services/` holds the estate's service manifests; none is
++ Actor's. Actor
++ exists only as a product page, `manifests/products/openvibe.actor.json`, with `relationships.noRepo: true` — a
++ site description, not an authority. Every `platform.*` contract is owned by `network`, and this one follows them.
++- **The engine already runs.** The plan's engine is `openvibe-agents`, the harness that runs coding, research and
++ review jobs across cheap agents with review and hand-off. Plan T17 names it as where the harness adapter interface
++ is built first; that work is not done, so the decisions below describe an interface the engine does not implement
++ yet.
++- **The pieces the router composes exist as contracts.** `openvibe-sdk/placement` is the one placer (ADR-046 §1);
++ `platform.placement-result@1` is its explanation; `platform.rate-card@1`, `platform.cost-snapshot@1`,
++ `platform.telemetry-sample@1` and `platform.usage-sample@1` are the cost and signal vocabulary (ADR-046 §§2–3);
++ `platform.runtime-class@1`, `platform.job@1` and `platform.job-frame@1` are Run's execution shape (T14); the
++ `agent` subject is already in `identity/subject-ref@1`.
++- **Actor's backend classes** are fixed by the plan: "agent platform, OpenVibe runtime, **Codes**, **Node**, **Run**.
++ Coding work is exposed to Actor **through Codes** — one coding-harness router in the estate, not two."
++
++## Decision
++
++### 1. One task contract: `platform.task@1`
++
++`contracts/platform/task.v1.json` (PLANNED, owner `network`) is one task at the router: the resource `POST /v1/tasks`
++creates and `GET /v1/tasks/{id}` reads. It carries `id` (`tsk_<ULID>`), `project_id`, `requester` (a person or an
++`agent` subject), the `task` text, the `mode`, the `budget`, the `state`, `created_at`/`finished_at`, the `result`,
++the `cost`, the `explanation` (one or more `platform.placement-result@1`, oldest first, an escalation appending one),
++the `progress` descriptor, the `cancel` record and the registered `webhooks`.
++
++- **`POST /v1/tasks` takes the task's creation subset** — `task`, `mode`, `budget`, and optionally `webhooks`. The
++ router mints `id`, sets `project_id` and `requester` from the token, and answers `state: queued`.
++- **It is the resource, not a device frame.** Run's `platform.job@1` is handed to a worker inside a
++ `platform.job-frame@1` link message because the link is OpenVibe.Node's WebSocket. Actor's link is HTTP and
++ server-sent events, so the frame vocabulary is not reused as a message type; `progress` and `explanation` carry
++ what the frames carry for a job. This is the shape, and whether Actor later needs an explicit stream-event contract
++ of its own (as `run.job-stream-event@1` is Run's) is left to the router's implementation.
++- **The contract is `status: planned`** until T17 ships: the catalog's not-yet-implemented value (there is no
++ `proposed` status), the same as the planned contracts of T14.
++
++### 2. The adapter interface
++
++An adapter is how Actor reaches one agent system, and the plan fixes what it declares: "Each adapter declares
++capabilities, limits, a revisioned rate card (never AI-written) and trust class; BYO keys for any backend." So an
++adapter has:
++
++- a **capability set** (which task classes it can take, including vision), checked before any mode or budget;
++- **limits** (context, concurrency, time, egress) merged stricter with the caller's, as `platform.job@1` limits are;
++- a **rate card** (`platform.rate-card@1`) with `source` and `verified_at`, revised by review and never written by an
++ AI on its own;
++- a **trust class** from ADR-046 §4;
++- **BYO key** support, so a caller can point the adapter at their own account and pay it directly.
++
++The plan puts the first adapters in the engine (OpenCode, Antigravity CLI) and demands that "a new agent platform is
++one adapter". The concrete interface — its field names, whether it is one manifest or a set of declared contracts,
++and how it is registered — is **not fixed by the plan and is left open**; this ADR fixes only what an adapter must
++declare, not the wire shape.
++
++### 3. Modes are planner objectives
++
++`mode` is one of `cheapest`, `balanced`, `best`, `fastest`, `private`; `balanced` is the default. The plan is
++explicit that "a mode is a planner objective, not a hard-coded list", so a mode is stated to the placer, not
++translated into a backend by hand. Four modes map onto ADR-046's `platform.placement-result@1` objectives
++directly: `cheapest → cheapest`, `balanced → balanced`, `fastest → lowest-latency`, `private → private`. `best`
++does **not** map to `correctness`: the SDK's `correctness` objective only preserves `req.authority` or the current
++placement, so for a new task with neither it ranks like `balanced`, and `high-reliability` has no branch either —
++`openvibe-sdk/placement` has no quality ranking that expresses "the highest chance of a right answer first time".
++What `best` maps to is therefore left as an open question below; the placer may refine the rest, and the exact
++tie-breaks inside a mode are left to `openvibe-sdk/placement`.
++
++### 4. Signals and auto-balancing
++
++Auto-balancing goes "through `openvibe-sdk/placement` over live signals per backend × task class; hysteresis keeps a
++task class unless another is clearly better; failover is immediate; every decision is stored and explained." The
++signals are the ones the Fabric already contracts: rate cards price a backend, `platform.telemetry-sample@1` reports
++its latency, health and status, and `platform.cost-snapshot@1` measures what a class actually cost against its
++alternatives. A capability the task needs is a hard filter before ranking, as `excluded()` is in the placer. The
++explanation is `platform.placement-result@1`, and it is part of the task, so "which agent ran, why it won, what else
++was considered and what it cost" survives the request.
++
++### 5. Verification and cascades
++
++"Cheap first, escalate on failure; cross-family checks; the task's own tests when it has them (code: tests; research:
++citations; actions: postconditions)." The task therefore has a `verifying` state between `running` and `succeeded`: a
++result that exists is checked before it is delivered, by a model family other than the one that produced it, and only
++then becomes `result`. A failed check moves the task to a stronger backend and appends a placement result; the plan
++puts the full cascade build after the router itself, so the first router may ship with a single check. A task that
++never passes ends `failed`; it never returns an unchecked result as `succeeded`.
++
++### 6. Trust
++
++ADR-046 §4's five classes apply unchanged: `first-party`, `user-owned`, `partner`, `community`, `external`, with
++`user-owned` eligible only when the task names it. `private` mode narrows the eligible set to backends that keep the
++data on OpenVibe — `first-party`, and a `user-owned` node only when the deployment allows it — which the plan
++describes as "only agents that keep your data on OpenVibe: its own runtime and open models on its hardware". Local
++execution on a person's own machine goes through OpenVibe.Node under ADR-043's control levels (Observe / Ask /
++Trusted / Full control), a local indicator and a kill switch the owner holds; Node's local policy always overrides a
++cloud command (plan T14).
++
++### 7. Progress, cancel and webhooks
++
++- **Progress** is `progress`: the server-sent-events `stream_url`, the task's own `last_seq`, and the event kinds
++ (`output`, `state`, `end`, the same three as `run.job-stream-event@1`). The stream is best-effort, as a job's stdout
++ is; the task record is the source of truth.
++- **Cancel** is `cancel`: `POST /v1/tasks/{id}/cancel` records who asked and when, the running backend is stopped as
++ its own contract allows (a Run job through `run.job.cancel`, a Node task through its link, an external platform
++ through its API), and the task ends `cancelled` with `finished_at` set. Cancel is not a promise that money already
++ spent is returned.
++- **Webhooks** are registered at creation: an HTTPS URL, the state changes to deliver, and an optional `secret_ref`
++ naming the project secret used to sign the delivery — the name, never the secret, as
++ `community.moderation-request@1` names a `webhook_url_ref` and ADR-043 keeps device credentials. A failed delivery
++ is retried with backoff and never delays the task.
++
++### 8. Cost and budgets
++
++`budget.per_task_usd` and `budget.per_day_usd` are hard limits the router never exceeds; a task that cannot be run
++inside `per_task_usd` fails (`actor.budget.exceeded`) instead of running, and `per_day_usd` caps the project's tasks
++in one UTC day. `cost.usd` is what the task was billed, to the cent, across every attempt, and the plan's money rule
++is "one key, one bill (Billing, T5), per-task cost reported to the cent; free tier from the bounded allowance". The
++metering is `platform.usage-sample@1`, written through Billing as Run's job seconds are.
++
++## Authority boundaries
++
++Actor plans and routes; it does not become an authority. The plan's own list of what it calls:
++
++| What Actor needs | Whose authority |
++|---|---|
++| Coding work | Codes (the one coding-harness router) |
++| Persistent conditions | Watch |
++| Cloud execution | Run (`platform.job@1` / `platform.job-frame@1`) |
++| Local execution and computer control | Node (ADR-043 control levels, kill switch) |
++| Resource control | Services (`common.resource-control-request@1`, ADR-048) |
++| Artifacts | Media |
++| Robots | Bot |
++| Progress and triggers | Events (ADR-042) |
++| Models | AI |
++
++## Out of scope
++
++- The router, the console (T17 layer 3), persistent agents, memory, schedules and the approval inbox.
++- The concrete adapter interface and its registration (decision 2 leaves it open).
++- Rankings from real traffic, templates, public agents and the MCP server (later T17 steps).
++- The Actor service manifest and its deploy: there is no `OpenVibe.Actor` repository yet, so the contract is owned by
++ `network` until one registers a service.
++
++## Open questions for the owner
++
++1. **The adapter interface's shape.** One contract with a discriminator, or one small contract per adapter kind? The
++ plan fixes what an adapter declares, not the wire shape.
++2. **Per-class success as a signal.** ADR-046 contracts telemetry and cost, but not a per-backend × task-class
++ success rate. Whether that is a new signal contract or an aggregation of `run.job.*`-style outcomes is open.
++3. **Whether a `user-owned` node may take other people's tasks** (ADR-046's own open question), which `private` mode
++ depends on.
++4. **Webhook delivery guarantees** — retry count, backoff and whether an exhausted webhook surfaces on the task.
++5. **Where an escalated task's partial results live** before it succeeds.
++6. **What `best` maps to.** No objective in `openvibe-sdk/placement` ranks quality: `correctness` only preserves
++ `req.authority` or the current placement and otherwise ranks like `balanced`, and `high-reliability` has no
++ branch. Until the placer grows a real quality ranking, `best → <objective>` is undecided.
++
++## Consequences
++
++- One contract for every task, whatever backend runs it; a new backend is an adapter, not a schema change.
++- Additive only: one new catalog id at 1.0.0, one id prefix (`tsk`), and a planned contract with no route and no
++ capability yet, so nothing in `generated/openapi/` changes.
++- The router can be built against a fixed task resource while the adapter interface stays open.
++- Rollback: consumers pin the previous release tag; nothing reads `platform.task@1` until T17's router is written.
++
++## What this ADR does not claim
++
++- No Actor service, router, adapter or `POST /v1/tasks` route exists. `platform.task@1` is a contract for work not yet
++ built.
++- The engine's harness adapter interface is not built; this ADR decides what an adapter declares, not its fields.
++- No backend is ranked, verified or metered by these rules yet; the contracts they rest on are themselves active but
++ unwired for this use.
ddiff --git a/docs/adr/README.md b/docs/adr/README.md
iindex f5d0c980..4a82a27c 100644
---- a/docs/adr/README.md
++++ b/docs/adr/README.md
@@@ -42,6 +42,7 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each
| [ADR-036](ADR-036-run-execution-authority.md) | Run is an execution authority, not a Host detail: Host deploys persistent applications, Run executes isolated jobs/environments (`function`, `code`, `browser`, `linux`, `desktop`, `gpu`), placed by the Fabric on the cheapest capable offer; provider adapters are offers, not code paths; per-second metering, TTL, idle suspend, cancel, snapshot/resume, stdout streaming, artifacts via Media, limits, network policy and strong isolation | Proposed 2026-10-05 (plan T14; plan §8 lists it to write before its code, and Run's code is owner-blocked), for the owner's review |
| [ADR-042](ADR-042-events-fabric.md) | The events fabric: the delivery-policy class is the one vocabulary and TOPIC/QUEUE/STREAM a derived carrier class; PostgreSQL stays the record, carriers are transports that fail open to `pg-v1`; a delivery lease before any second worker; per-key ordering; the SDK planner places deliveries and explains them; JetStream for STREAM only; opaque cursors replace the global seq after one overlap release; hot/replay/archive tiers; amends ADR-004's NATS rejection | Accepted 2026-09-29 (plan T7) |
| [ADR-043](ADR-043-bot-devices-and-control.md) | Bot — devices, pairing, control and safety: robots, devices and credentials are separate (one hashed, rotatable, revocable device credential); pairing is a one-time code; one outbound WebSocket per device with absolute deadlines, device-side stops and a latched e-stop; robot profiles rather than rows; owner/operators/viewers with a timed public queue; everything audited; OpenVibe.Bot is its own service | Accepted 2026-09-29 (plan track T15, "Bot complete"; owner direction the same evening: openvibe.bot is an open control panel for robots, streaming optional; the first robots are the owner's Adeept 4WD Smart Car for Raspberry Pi and a Cozmo-based robot) |
++| [ADR-044](ADR-044-actor-runtime.md) | The Actor runtime: one `platform.task@1` task (`POST /v1/tasks`) with a mode, a per-task and per-day budget, result, cost and explanation (a chain of `platform.placement-result@1`); adapters declare capabilities, limits, a reviewed rate card, a trust class and BYO keys; modes are `openvibe-sdk/placement` objectives; live signals auto-balance with hysteresis and immediate failover; a `verifying` state holds cheap-first cross-family checks and the task's own tests; the five ADR-046 trust classes, `private` narrowing to OpenVibe-only, Node control levels underneath; progress over SSE, cancel and webhooks; owner `network` (no Actor service manifest exists yet) | Proposed 2026-10-05 (plan T17), for the owner's review |
| [ADR-045](ADR-045-node-universal-runtime.md) | The Node is OpenVibe's universal runtime, not a Bot/Actor helper: one checksummed release binary and one control link serving the execution worker (`worker:<class>`, per ADR-046 §5), computer control under device-set control levels with a local kill switch and recording, and cache/storage/delivery; local policy always overrides cloud commands; ADR-043 stays the robot/device half | Proposed 2026-10-05 (plan T14 and §8 revision; consistent with ADR-046 and ADR-048, amends ADR-043's credential paragraph), for the owner's review |
| [ADR-046](ADR-046-universal-adaptive-fabric.md) | The universal adaptive fabric: offers, requirements, placement results and signed plans through one placer (`openvibe-sdk/placement`); health, cost, telemetry and usage samples; five trust classes, with `user-owned` eligible only when a workload names it; `worker:<class>` Node capabilities; the data plane keeps running on the last valid signed plan; T6 AI the reference consumer | Proposed 2026-10-03 (plan T1 step 3), for the owner's review |
| [ADR-048](ADR-048-services-control-plane.md) | The Services control plane: Services aggregates authorities and owns no other service's rows; every console action is a `common.resource-control-request@1` to the owning authority, answered with `common.resource-control-result@1` (idempotency keys, `confirmation_id` for sensitive actions); OVRN `ovrn:<service>:<project_id>:<type>/<id>` (`common.resource-name@1`) is the one cross-service resource name | Accepted 2026-10-04 (plan T13) |
ddiff --git a/fixtures/platform.task/invalid/budget-missing-per-day.json b/fixtures/platform.task/invalid/budget-missing-per-day.json
nnew file mode 100644
iindex 00000000..0e4564a6
---- /dev/null
++++ b/fixtures/platform.task/invalid/budget-missing-per-day.json
@@@ -0,0 +1,10 @@
++{
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS" },
++ "task": "Summarize the release notes.",
++ "mode": "balanced",
++ "budget": { "per_task_usd": 5 },
++ "state": "queued",
++ "created_at": "2026-10-05T09:00:00.000Z"
++}
ddiff --git a/fixtures/platform.task/invalid/cancelled-without-cancel.json b/fixtures/platform.task/invalid/cancelled-without-cancel.json
nnew file mode 100644
iindex 00000000..cec9baf8
---- /dev/null
++++ b/fixtures/platform.task/invalid/cancelled-without-cancel.json
@@@ -0,0 +1,13 @@
++{
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS" },
++ "task": "Draft a launch announcement for the new pricing page.",
++ "mode": "balanced",
++ "budget": { "per_task_usd": 2, "per_day_usd": 20 },
++ "state": "cancelled",
++ "created_at": "2026-10-05T09:00:00.000Z",
++ "finished_at": "2026-10-05T09:02:30.000Z",
++ "result": null,
++ "cost": { "usd": 0.01 }
++}
ddiff --git a/fixtures/platform.task/invalid/failed-with-result.json b/fixtures/platform.task/invalid/failed-with-result.json
nnew file mode 100644
iindex 00000000..13fc3e57
---- /dev/null
++++ b/fixtures/platform.task/invalid/failed-with-result.json
@@@ -0,0 +1,13 @@
++{
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS" },
++ "task": "Summarize the release notes.",
++ "mode": "balanced",
++ "budget": { "per_task_usd": 5, "per_day_usd": 50 },
++ "state": "failed",
++ "created_at": "2026-10-05T09:00:00.000Z",
++ "finished_at": "2026-10-05T09:01:00.000Z",
++ "result": { "partial": "a half-written brief" },
++ "cost": { "usd": 0.02 }
++}
ddiff --git a/fixtures/platform.task/invalid/missing-task.json b/fixtures/platform.task/invalid/missing-task.json
nnew file mode 100644
iindex 00000000..068e4edd
---- /dev/null
++++ b/fixtures/platform.task/invalid/missing-task.json
@@@ -0,0 +1,9 @@
++{
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS" },
++ "mode": "balanced",
++ "budget": { "per_task_usd": 5, "per_day_usd": 50 },
++ "state": "queued",
++ "created_at": "2026-10-05T09:00:00.000Z"
++}
ddiff --git a/fixtures/platform.task/invalid/negative-budget.json b/fixtures/platform.task/invalid/negative-budget.json
nnew file mode 100644
iindex 00000000..0066088d
---- /dev/null
++++ b/fixtures/platform.task/invalid/negative-budget.json
@@@ -0,0 +1,10 @@
++{
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS" },
++ "task": "Summarize the release notes.",
++ "mode": "balanced",
++ "budget": { "per_task_usd": -1, "per_day_usd": 50 },
++ "state": "queued",
++ "created_at": "2026-10-05T09:00:00.000Z"
++}
ddiff --git a/fixtures/platform.task/invalid/plain-http-webhook.json b/fixtures/platform.task/invalid/plain-http-webhook.json
nnew file mode 100644
iindex 00000000..7695a98e
---- /dev/null
++++ b/fixtures/platform.task/invalid/plain-http-webhook.json
@@@ -0,0 +1,13 @@
++{
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS" },
++ "task": "Summarize the release notes.",
++ "mode": "balanced",
++ "budget": { "per_task_usd": 5, "per_day_usd": 50 },
++ "state": "queued",
++ "created_at": "2026-10-05T09:00:00.000Z",
++ "webhooks": [
++ { "url": "http://example.com/hooks/actor", "events": ["succeeded"] }
++ ]
++}
ddiff --git a/fixtures/platform.task/invalid/succeeded-without-result.json b/fixtures/platform.task/invalid/succeeded-without-result.json
nnew file mode 100644
iindex 00000000..704a06fd
---- /dev/null
++++ b/fixtures/platform.task/invalid/succeeded-without-result.json
@@@ -0,0 +1,20 @@
++{
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": { "type": "agent", "id": "agt_01JAB2C3D4E5F6G7H8J9K0MNPT" },
++ "task": "Check this week's outage report and summarize the causes.",
++ "mode": "cheapest",
++ "budget": { "per_task_usd": 1, "per_day_usd": 10 },
++ "state": "succeeded",
++ "created_at": "2026-10-05T09:00:00.000Z",
++ "finished_at": "2026-10-05T09:06:00.000Z",
++ "cost": { "usd": 0.06 },
++ "explanation": [
++ {
++ "selected": "open-model",
++ "objective": "cheapest",
++ "candidates": [{ "id": "open-model", "eligible": true }],
++ "decided_at": "2026-10-05T09:00:01.000Z"
++ }
++ ]
++}
ddiff --git a/fixtures/platform.task/invalid/unknown-mode.json b/fixtures/platform.task/invalid/unknown-mode.json
nnew file mode 100644
iindex 00000000..95106dc5
---- /dev/null
++++ b/fixtures/platform.task/invalid/unknown-mode.json
@@@ -0,0 +1,10 @@
++{
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS" },
++ "task": "Summarize the release notes.",
++ "mode": "smart",
++ "budget": { "per_task_usd": 5, "per_day_usd": 50 },
++ "state": "queued",
++ "created_at": "2026-10-05T09:00:00.000Z"
++}
ddiff --git a/fixtures/platform.task/valid/cancelled.json b/fixtures/platform.task/valid/cancelled.json
nnew file mode 100644
iindex 00000000..8fa4fd68
---- /dev/null
++++ b/fixtures/platform.task/valid/cancelled.json
@@@ -0,0 +1,27 @@
++{
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS" },
++ "task": "Draft a launch announcement for the new pricing page.",
++ "mode": "balanced",
++ "budget": { "per_task_usd": 2, "per_day_usd": 20 },
++ "state": "cancelled",
++ "created_at": "2026-10-05T09:00:00.000Z",
++ "finished_at": "2026-10-05T09:02:30.000Z",
++ "result": null,
++ "cost": { "usd": 0.01 },
++ "explanation": [
++ {
++ "selected": "hosted-agent",
++ "objective": "balanced",
++ "reasons": ["writing task; first attempt still running when the caller cancelled"],
++ "candidates": [{ "id": "hosted-agent", "eligible": true, "estimated_cost_usd": 0.31 }],
++ "decided_at": "2026-10-05T09:00:01.000Z"
++ }
++ ],
++ "cancel": {
++ "requested_at": "2026-10-05T09:02:29.000Z",
++ "requested_by": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS" }
++ },
++ "progress": null
++}
ddiff --git a/fixtures/platform.task/valid/queued.json b/fixtures/platform.task/valid/queued.json
nnew file mode 100644
iindex 00000000..1d8a46b9
---- /dev/null
++++ b/fixtures/platform.task/valid/queued.json
@@@ -0,0 +1,13 @@
++{
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS" },
++ "task": "Research five competitors of my shop and write a one-page brief with sources.",
++ "mode": "balanced",
++ "budget": { "per_task_usd": 5, "per_day_usd": 50 },
++ "state": "queued",
++ "created_at": "2026-10-05T09:00:00.000Z",
++ "webhooks": [
++ { "url": "https://example.com/hooks/actor", "events": ["succeeded", "failed"], "secret_ref": "ACTOR_WEBHOOK_SECRET" }
++ ]
++}
ddiff --git a/fixtures/platform.task/valid/running.json b/fixtures/platform.task/valid/running.json
nnew file mode 100644
iindex 00000000..22cb038a
---- /dev/null
++++ b/fixtures/platform.task/valid/running.json
@@@ -0,0 +1,24 @@
++{
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS" },
++ "task": "Fix the failing thumbnail test in OpenVibe.Media and open a PR.",
++ "mode": "best",
++ "budget": { "per_task_usd": 5, "per_day_usd": 50 },
++ "state": "running",
++ "created_at": "2026-10-05T09:00:00.000Z",
++ "progress": {
++ "stream_url": "https://openvibe.actor/v1/tasks/tsk_01JAB2C3D4E5F6G7H8J9K0MNPR/events",
++ "last_seq": 12,
++ "events": ["output", "state", "end"]
++ },
++ "explanation": [
++ {
++ "selected": "openvibe-runtime",
++ "objective": "correctness",
++ "reasons": ["the task is code; the Codes adapter exposes a code harness"],
++ "candidates": [{ "id": "openvibe-runtime", "eligible": true, "estimated_cost_usd": 0.06 }],
++ "decided_at": "2026-10-05T09:00:01.000Z"
++ }
++ ]
++}
ddiff --git a/fixtures/platform.task/valid/succeeded.json b/fixtures/platform.task/valid/succeeded.json
nnew file mode 100644
iindex 00000000..e7d18d4e
---- /dev/null
++++ b/fixtures/platform.task/valid/succeeded.json
@@@ -0,0 +1,24 @@
++{
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": { "type": "agent", "id": "agt_01JAB2C3D4E5F6G7H8J9K0MNPT" },
++ "task": "Check this week's outage report and summarize the causes.",
++ "mode": "cheapest",
++ "budget": { "per_task_usd": 1, "per_day_usd": 10 },
++ "state": "succeeded",
++ "created_at": "2026-10-05T09:00:00.000Z",
++ "finished_at": "2026-10-05T09:06:00.000Z",
++ "result": { "brief": "Two outages; both were a saturated connection pool.", "citations": ["https://openvibe.status/incidents/41"] },
++ "cost": { "usd": 0.06, "free_allowance_used": 0.06 },
++ "explanation": [
++ {
++ "selected": "open-model",
++ "objective": "cheapest",
++ "reasons": ["research task; the checks are citations, met by the cheapest candidate"],
++ "candidates": [{ "id": "open-model", "eligible": true, "estimated_cost_usd": 0.06 }],
++ "decided_at": "2026-10-05T09:00:01.000Z"
++ }
++ ],
++ "cancel": null,
++ "progress": null
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex 6b15d50a..83203743 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -64599,6 +64599,439 @@
]
}
},
++ "platform.task": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "planned",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/platform/task.v1.json",
++ "title": "Task",
++ "description": "platform.task@1 (PLANNED; plan T17, ADR-044): one task at OpenVibe.Actor's task router. POST /v1/tasks creates it from a plain-language `task` plus a `mode` and a `budget`; the router mints `id`, sets `state` and `created_at`, picks a backend from the registered adapters, runs the task, verifies the result, escalates or fails over, and answers with `result`, `cost` and `explanation`. GET /v1/tasks/{id} reads the same resource. Progress streams as server-sent events from `progress.stream_url` (a task's own counter in `progress.last_seq`, as platform.job-frame@1 counts on a link); `cancel` records that a cancel was requested (POST /v1/tasks/{id}/cancel), and `webhooks` are the registered endpoints the router delivers each state change to. No Actor service exists yet: OpenVibe.Actor is a product page (manifests/products/openvibe.actor.json, noRepo) with no service manifest, so the contract is owned by `network`, like every other platform.* contract, and stays PLANNED until T17 ships. This v1 contract may grow optional fields.",
++ "$comment": "Additive optional fields may extend this v1 contract. It is the task resource, not a device frame: Actor's link is HTTP/SSE where Run's is the Node WebSocket.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "project_id",
++ "requester",
++ "task",
++ "mode",
++ "budget",
++ "state",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "Minted by the router (tsk_<ULID>): the idempotency key of the task and the stem of its usage readings, as platform.job@1's id is for a job. Never reused."
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The project the caller's token names (ADR-034 §5): budgets, usage and the bill are the project's."
++ },
++ "requester": {
++ "description": "The person or agent the task belongs to: a user subject for a person, an agent subject for an agent acting under the grants its owner delegated (roadmap WS-Z2).",
++ "allOf": [
++ {
++ "$ref": "../identity/subject-ref.v1.json"
++ }
++ ]
++ },
++ "task": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 100000,
++ "description": "The instruction in plain words, exactly as the caller gave it. Not a backend command and not a model prompt: the planner turns it into one."
++ },
++ "mode": {
++ "type": "string",
++ "enum": [
++ "cheapest",
++ "balanced",
++ "best",
++ "fastest",
++ "private"
++ ],
++ "default": "balanced",
++ "description": "The plan's five modes, in contract spelling: Cheapest, Balanced (the default), Best, Fastest and Private are `cheapest`, `balanced`, `best`, `fastest` and `private`. A mode is the planner objective, never a fixed backend list (plan T17). `cheapest`: the lowest expected cost that still meets the task class's quality bar. `balanced` (the default): cost, quality and speed weighed together. `best`: the highest chance of a right answer first time. `fastest`: the shortest time to a checked result. `private`: only backends that keep the data on OpenVibe (first-party, and a user-owned node only when the task names it), per ADR-046."
++ },
++ "budget": {
++ "type": "object",
++ "additionalProperties": false,
++ "description": "Hard limits the router never exceeds (plan T17: 'a hard limit per task and per day'). A task whose cheapest eligible backend would take it over `per_task_usd` ends failed (actor.budget.exceeded) rather than running; `per_day_usd` caps the project's tasks in one UTC day and a new task waits or fails, it never overruns.",
++ "required": [
++ "per_task_usd",
++ "per_day_usd"
++ ],
++ "properties": {
++ "per_task_usd": {
++ "type": "number",
++ "minimum": 0
++ },
++ "per_day_usd": {
++ "type": "number",
++ "minimum": 0
++ }
++ }
++ },
++ "state": {
++ "type": "string",
++ "enum": [
++ "queued",
++ "running",
++ "verifying",
++ "succeeded",
++ "failed",
++ "cancelled"
++ ],
++ "description": "queued: stored, waiting for the planner. running: a backend is working, or a cascade is between attempts. verifying: a result exists and a check (the task's own tests, a citation check, a postcondition) runs before it is delivered. succeeded, failed and cancelled are the end states; an end state never changes."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "finished_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "When the task reached its end state; null before."
++ },
++ "result": {
++ "description": "The checked deliverable (any JSON, as a platform.job@1 `result` is): for a code task a diff or branch reference, for research a brief with citations, for an action a postcondition report. Never a credential and never raw sandbox output. Null (or absent) in every state but succeeded, which the allOf below enforces."
++ },
++ "cost": {
++ "anyOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "usd"
++ ],
++ "properties": {
++ "usd": {
++ "type": "number",
++ "minimum": 0,
++ "description": "What this task was billed, to the cent, across every attempt it made."
++ },
++ "free_allowance_used": {
++ "type": "number",
++ "minimum": 0,
++ "description": "The part of `usd` covered by the project's free allowance; metered as platform.usage-sample@1."
++ }
++ }
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "Present once the task has run an attempt (also when it ends failed or cancelled); null while queued. One key, one bill (plan T17), through Billing (ADR-012)."
++ },
++ "explanation": {
++ "anyOf": [
++ {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "$ref": "placement-result.v1.json"
++ }
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "One platform.placement-result@1 per backend the planner tried, oldest first, an escalation appending a new one: which backend won, why and what else was considered, from openvibe-sdk/placement (ADR-046 §1). Null while queued."
++ },
++ "progress": {
++ "anyOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "stream_url",
++ "last_seq"
++ ],
++ "properties": {
++ "stream_url": {
++ "type": "string",
++ "format": "uri",
++ "description": "The text/event-stream endpoint that carries this task's progress. Each event mirrors platform.job-frame@1's job_stdout/state shape: a state change, an output chunk, or the end."
++ },
++ "last_seq": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "The task's own event counter, from 0 before the first event; a reconnecting reader resumes after it, as a job frame's seq counts on one link."
++ },
++ "events": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "enum": [
++ "output",
++ "state",
++ "end"
++ ]
++ },
++ "description": "The event kinds the stream delivers, run.job-stream-event@1's own three."
++ }
++ }
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "Streaming progress (plan T17). Null before the router opens a stream and after the end."
++ },
++ "cancel": {
++ "anyOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "requested_at"
++ ],
++ "properties": {
++ "requested_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When POST /v1/tasks/{id}/cancel was accepted. The running backend is stopped as its own contract allows; the state becomes cancelled and `finished_at` is set."
++ },
++ "requested_by": {
++ "$ref": "../identity/subject-ref.v1.json"
++ }
++ }
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "Cancellation (plan T17): null until a cancel is requested, then it records who asked and when; the task's outcome is `state`."
++ },
++ "webhooks": {
++ "type": "array",
++ "maxItems": 8,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "url",
++ "events"
++ ],
++ "properties": {
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://",
++ "description": "The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused."
++ },
++ "events": {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "type": "string",
++ "enum": [
++ "queued",
++ "running",
++ "verifying",
++ "succeeded",
++ "failed",
++ "cancelled"
++ ]
++ },
++ "description": "The state changes delivered; the same names as `state`."
++ },
++ "secret_ref": {
++ "type": "string",
++ "pattern": "^[A-Z0-9_]+$",
++ "description": "The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference."
++ }
++ }
++ },
++ "description": "Webhooks (plan T17), registered at creation. A delivery that fails is retried with backoff and never delays the task; the task's own record is the source of truth."
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "required": [
++ "state"
++ ],
++ "properties": {
++ "state": {
++ "const": "succeeded"
++ }
++ }
++ },
++ "then": {
++ "required": [
++ "result",
++ "cost",
++ "explanation"
++ ],
++ "properties": {
++ "result": {
++ "not": {
++ "type": "null"
++ }
++ },
++ "cost": {
++ "type": "object"
++ },
++ "explanation": {
++ "type": "array"
++ }
++ }
++ },
++ "else": {
++ "properties": {
++ "result": {
++ "type": "null"
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "required": [
++ "state"
++ ],
++ "properties": {
++ "state": {
++ "const": "cancelled"
++ }
++ }
++ },
++ "then": {
++ "required": [
++ "cancel"
++ ]
++ }
++ }
++ ],
++ "examples": [
++ {
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS"
++ },
++ "task": "Research five competitors of my shop and write a one-page brief with sources.",
++ "mode": "balanced",
++ "budget": {
++ "per_task_usd": 5,
++ "per_day_usd": 50
++ },
++ "state": "queued",
++ "created_at": "2026-10-05T09:00:00.000Z",
++ "webhooks": [
++ {
++ "url": "https://example.com/hooks/actor",
++ "events": [
++ "succeeded",
++ "failed"
++ ],
++ "secret_ref": "ACTOR_WEBHOOK_SECRET"
++ }
++ ]
++ },
++ {
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPS"
++ },
++ "task": "Fix the failing thumbnail test in OpenVibe.Media and open a PR.",
++ "mode": "best",
++ "budget": {
++ "per_task_usd": 5,
++ "per_day_usd": 50
++ },
++ "state": "running",
++ "created_at": "2026-10-05T09:00:00.000Z",
++ "progress": {
++ "stream_url": "https://openvibe.actor/v1/tasks/tsk_01JAB2C3D4E5F6G7H8J9K0MNPR/events",
++ "last_seq": 12,
++ "events": [
++ "output",
++ "state",
++ "end"
++ ]
++ },
++ "explanation": [
++ {
++ "selected": "openvibe-runtime",
++ "objective": "correctness",
++ "reasons": [
++ "the task is code; the Codes adapter exposes a code harness"
++ ],
++ "candidates": [
++ {
++ "id": "openvibe-runtime",
++ "eligible": true,
++ "estimated_cost_usd": 0.06,
++ "estimated_latency_ms": 180000
++ }
++ ],
++ "decided_at": "2026-10-05T09:00:01.000Z"
++ }
++ ]
++ },
++ {
++ "id": "tsk_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "requester": {
++ "type": "agent",
++ "id": "agt_01JAB2C3D4E5F6G7H8J9K0MNPT"
++ },
++ "task": "Check this week's outage report and summarize the causes.",
++ "mode": "cheapest",
++ "budget": {
++ "per_task_usd": 1,
++ "per_day_usd": 10
++ },
++ "state": "succeeded",
++ "created_at": "2026-10-05T09:00:00.000Z",
++ "finished_at": "2026-10-05T09:06:00.000Z",
++ "result": {
++ "brief": "Two outages; both were a saturated connection pool.",
++ "citations": [
++ "https://openvibe.status/incidents/41"
++ ]
++ },
++ "cost": {
++ "usd": 0.06,
++ "free_allowance_used": 0.06
++ },
++ "explanation": [
++ {
++ "selected": "open-model",
++ "objective": "cheapest",
++ "reasons": [
++ "research task; the checks are citations, met by the cheapest candidate"
++ ],
++ "candidates": [
++ {
++ "id": "open-model",
++ "eligible": true,
++ "estimated_cost_usd": 0.06,
++ "estimated_latency_ms": 420000
++ }
++ ],
++ "decided_at": "2026-10-05T09:00:01.000Z"
++ }
++ ],
++ "cancel": null,
++ "progress": null
++ }
++ ]
++ }
++ },
"events.delivery-policy": {
"version": "1.0.0",
"owner": "events",
ddiff --git a/generated/openapi/ai.json b/generated/openapi/ai.json
iindex 9af50a50..ae082da1 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.100.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 3c7955d0..99b61a6c 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.100.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 72f2401c..4030a573 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.100.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 569e590f..aa5eecf0 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.100.0",
-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 d0a067e9..f272b837 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.100.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 f627f212..98f24c54 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.100.0",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 53b75a57..4a22f712 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.100.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 7fd4890a..a5ed1565 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.100.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 9e8ed83d..946d196e 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.100.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 aaba4c30..e4336857 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.100.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 b5c19144..cd9abae8 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.100.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 74f2b7f2..ae541a21 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.100.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 40d0fe6a..ecc1b170 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.100.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 e35765a2..c83f55e1 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.100.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 ce3a18af..e528dbc4 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.100.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 77d70afe..040918c5 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.100.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 3ba89b66..0a3ee74d 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.100.0",
-- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 289f0e21..f9155017 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.100.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 379c4b5b..9514946a 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.100.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 16016788..f9374a35 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.100.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 f345266a..16c1f982 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.100.0",
-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 30f733fb..ff00b107 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.100.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 38a59b50..5fb90a91 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.100.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 0632fcf6..58b61d8d 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.100.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 b2c3b41b..d39a578a 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.100.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 968f819c..da9bf98d 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.100.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.100.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.101.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.101.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 c516f301..ca130f3a 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -52604,6 +52604,808 @@ export type JobFrame =
};
};
++/** [email protected] (owner: network) */
++/**
++ * platform.task@1 (PLANNED; plan T17, ADR-044): one task at OpenVibe.Actor's task router. POST /v1/tasks creates it from a plain-language `task` plus a `mode` and a `budget`; the router mints `id`, sets `state` and `created_at`, picks a backend from the registered adapters, runs the task, verifies the result, escalates or fails over, and answers with `result`, `cost` and `explanation`. GET /v1/tasks/{id} reads the same resource. Progress streams as server-sent events from `progress.stream_url` (a task's own counter in `progress.last_seq`, as platform.job-frame@1 counts on a link); `cancel` records that a cancel was requested (POST /v1/tasks/{id}/cancel), and `webhooks` are the registered endpoints the router delivers each state change to. No Actor service exists yet: OpenVibe.Actor is a product page (manifests/products/openvibe.actor.json, noRepo) with no service manifest, so the contract is owned by `network`, like every other platform.* contract, and stays PLANNED until T17 ships. This v1 contract may grow optional fields.
++ */
++export type Task = {
++ [k: string]: unknown | undefined;
++} & {
++ /**
++ * Minted by the router (tsk_<ULID>): the idempotency key of the task and the stem of its usage readings, as platform.job@1's id is for a job. Never reused.
++ */
++ id: string;
++ /**
++ * The project the caller's token names (ADR-034 §5): budgets, usage and the bill are the project's.
++ */
++ project_id: string;
++ /**
++ * The person or agent the task belongs to: a user subject for a person, an agent subject for an agent acting under the grants its owner delegated (roadmap WS-Z2).
++ */
++ requester: SubjectRef;
++ /**
++ * The instruction in plain words, exactly as the caller gave it. Not a backend command and not a model prompt: the planner turns it into one.
++ */
++ task: string;
++ /**
++ * The plan's five modes, in contract spelling: Cheapest, Balanced (the default), Best, Fastest and Private are `cheapest`, `balanced`, `best`, `fastest` and `private`. A mode is the planner objective, never a fixed backend list (plan T17). `cheapest`: the lowest expected cost that still meets the task class's quality bar. `balanced` (the default): cost, quality and speed weighed together. `best`: the highest chance of a right answer first time. `fastest`: the shortest time to a checked result. `private`: only backends that keep the data on OpenVibe (first-party, and a user-owned node only when the task names it), per ADR-046.
++ */
++ mode: "cheapest" | "balanced" | "best" | "fastest" | "private";
++ /**
++ * Hard limits the router never exceeds (plan T17: 'a hard limit per task and per day'). A task whose cheapest eligible backend would take it over `per_task_usd` ends failed (actor.budget.exceeded) rather than running; `per_day_usd` caps the project's tasks in one UTC day and a new task waits or fails, it never overruns.
++ */
++ budget: {
++ per_task_usd: number;
++ per_day_usd: number;
++ };
++ /**
++ * queued: stored, waiting for the planner. running: a backend is working, or a cascade is between attempts. verifying: a result exists and a check (the task's own tests, a citation check, a postcondition) runs before it is delivered. succeeded, failed and cancelled are the end states; an end state never changes.
++ */
++ state: "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled";
++ created_at: string;
++ /**
++ * When the task reached its end state; null before.
++ */
++ finished_at?: string | null;
++ /**
++ * The checked deliverable (any JSON, as a platform.job@1 `result` is): for a code task a diff or branch reference, for research a brief with citations, for an action a postcondition report. Never a credential and never raw sandbox output. Null (or absent) in every state but succeeded, which the allOf below enforces.
++ */
++ result?: {
++ [k: string]: unknown | undefined;
++ };
++ /**
++ * Present once the task has run an attempt (also when it ends failed or cancelled); null while queued. One key, one bill (plan T17), through Billing (ADR-012).
++ */
++ cost?: {
++ /**
++ * What this task was billed, to the cent, across every attempt it made.
++ */
++ usd: number;
++ /**
++ * The part of `usd` covered by the project's free allowance; metered as platform.usage-sample@1.
++ */
++ free_allowance_used?: number;
++ } | null;
++ /**
++ * One platform.placement-result@1 per backend the planner tried, oldest first, an escalation appending a new one: which backend won, why and what else was considered, from openvibe-sdk/placement (ADR-046 §1). Null while queued.
++ */
++ explanation?: [PlacementResult, ...PlacementResult[]] | null;
++ /**
++ * Streaming progress (plan T17). Null before the router opens a stream and after the end.
++ */
++ progress?: {
++ /**
++ * The text/event-stream endpoint that carries this task's progress. Each event mirrors platform.job-frame@1's job_stdout/state shape: a state change, an output chunk, or the end.
++ */
++ stream_url: string;
++ /**
++ * The task's own event counter, from 0 before the first event; a reconnecting reader resumes after it, as a job frame's seq counts on one link.
++ */
++ last_seq: number;
++ /**
++ * The event kinds the stream delivers, run.job-stream-event@1's own three.
++ */
++ events?: ("output" | "state" | "end")[];
++ } | null;
++ /**
++ * Cancellation (plan T17): null until a cancel is requested, then it records who asked and when; the task's outcome is `state`.
++ */
++ cancel?: {
++ /**
++ * When POST /v1/tasks/{id}/cancel was accepted. The running backend is stopped as its own contract allows; the state becomes cancelled and `finished_at` is set.
++ */
++ requested_at: string;
++ requested_by?: SubjectRef;
++ } | null;
++ /**
++ * Webhooks (plan T17), registered at creation. A delivery that fails is retried with backoff and never delays the task; the task's own record is the source of truth.
++ *
++ * @maxItems 8
++ */
++ webhooks?:
++ | []
++ | [
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ }
++ ]
++ | [
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ }
++ ]
++ | [
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ }
++ ]
++ | [
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ }
++ ]
++ | [
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ }
++ ]
++ | [
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ }
++ ]
++ | [
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ }
++ ]
++ | [
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ },
++ {
++ /**
++ * The HTTPS endpoint the router POSTs a state change to. A plain-HTTP URL is refused.
++ */
++ url: string;
++ /**
++ * The state changes delivered; the same names as `state`.
++ *
++ * @minItems 1
++ */
++ events: [
++ "queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled",
++ ...("queued" | "running" | "verifying" | "succeeded" | "failed" | "cancelled")[]
++ ];
++ /**
++ * The NAME of the project secret used to sign the delivery (an HMAC header), never the secret itself, as community.moderation-request@1 names a webhook_url_ref and ADR-043 keeps device credentials by reference.
++ */
++ secret_ref?: string;
++ }
++ ];
++};
++
/** [email protected] (owner: events) */
/**
* How an event type is carried (roadmap WS-Z3 task 2): its delivery class sets the minimum semantics, which a publisher's intent may raise and never lower; ordering is per key; the route planner picks the carrier.
ddiff --git a/lib/ids.js b/lib/ids.js
iindex 5f552ed3..c10b8e64 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', job: 'job', 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', task: 'tsk', 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/package-lock.json b/package-lock.json
iindex 42dfd031..9c4f7e21 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-contracts",
-- "version": "0.100.0",
++ "version": "0.101.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-contracts",
-- "version": "0.100.0",
++ "version": "0.101.0",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 4e33e7f7..14eae2fd 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.100.0",
++ "version": "0.101.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/run.js b/test/run.js
iindex 0d653aa3..51de2a72 100644
---- a/test/run.js
++++ b/test/run.js
@@@ -531,7 +531,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'], ['job', 'job']]) ok(ids.newId(kind).startsWith(`${prefix}_`), `newId(${kind}) is ${prefix}_<ULID>`);
++for (const [kind, prefix] of [['watch', 'wch'], ['observation', 'wco'], ['check', 'ckr'], ['job', 'job'], ['task', 'tsk']]) 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');