openvibers/openvibe.contracts Commits

v0.33.0: ADR-027 Tools platform API; CHANGELOG, README and version 0.33.0

OP OpenVibers committed 23 Sept 2026 · 18175b1aef · 6 files, +322 −7

ADR-027 records the decisions of the Tools platform program:
- One descriptor per tool and one run API. Inline answers, or a job with 202 + Location. File results always come from a job. Inputs can reference Media objects or the caller's own job results.
- The gateway fronts the jobs API.
- Browser-only pure transforms may get an opt-in server engine, and their pages stay browser-only.
- yt is page-only (api false; YouTube's terms and legal exposure).
- Caller tiers: anonymous (IP, IPv6 /64) < session < user < app/service. Anonymous use is open; heavy job types need a session or a token; sandbox tokens keep their small allowance.
- Weighted quotas per class. Class names describe the work, never a tier or a price, so paid tiers can come later.
- Only one proxy hop is trusted for the client IP; a client-sent X-Forwarded-For is never trusted.
- A no-op challenge hook for a later Turnstile.
- Network probes need tools.net.probe (partner: staff-set allowances only). People keep the probe pages, which have a per-target throttle.
- The abuse log holds HMAC-SHA256(IP, daily salt, discarded after its day) plus the subject, tool and reason, kept 30 days and never raw IPs: pseudonymous security data under ADR-021, never joined into analytics.
- Legacy /api/process, /api/net/* and /api/dev/* keep working with Deprecation: true, Sunset: Thu, 31 Dec 2026 23:59:59 GMT and Link rel=successor-version.
- Honest statuses (unavailable until qpdf and poppler-utils are deployed).

The ADR also lists alternatives, the migration order (slices), rollback and acceptance tests.

README: counts (95 schemas, 176 capabilities), a Tools platform API section, and tools.job.* no longer described as planned. ADR index row. package.json and package-lock.json are 0.33.0.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 1cd76456..369863ba 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,95 @@ 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.33.0 — 2026-09-23
++
++Additive: `compat.js` reports no breaking change against v0.32.0.
++
++**Tools platform API** (new ADR-027). The tools on openvibe.tools become one registry and one run API,
++open to the SDK, with tiered, weighted quotas.
++
++- `tools.tool@1`: the **tool descriptor**, one per catalogue tool.
++ - Identity: `id`, `family`, `name`, `summary`.
++ - `status`: `stable` | `beta` | `preview` | `unavailable`, with `statusReason`.
++ - How it runs:
++ - `execution`: `client` | `sync` | `job`.
++ - `api`: whether the run API exposes the tool.
++ - `run`: `{ method: POST, path: /api/v1/tools/{id}/run, job: { type, operation, preset? } | null, legacy? }`.
++ - Its data:
++ - `input`: a JSON Schema, embedded or `{ $ref }` to `GET /api/v1/tools/:id/schema#/$defs/input`.
++ - `files`: `{ min, max, accept, maxBytes }` or null.
++ - `output`: `{ kind: json|file|files|text, schema?, mime? }`.
++ - `limits`: `timeoutMs`, and optionally `maxDurationSec`, `maxPixels`, `maxPages`, `maxInputBytes`,
++ `perTargetPerMinute`.
++ - Access and cost:
++ - `auth`: `{ anonymous, capability: tools.tool.run | tools.net.probe }`.
++ - `quotaClass` and `cost` (the relative weight quotas count).
++ - `egress`: the server fetches a host the caller chose.
++ - `hosts` and `docs`.
++
++ The schema enforces these rules:
++ - `api: false` has `run: null`, and `api: true` has a run and an input schema.
++ - Only job tools name a job, and an API job tool always does.
++ - File output through the API comes from a job.
++ - A `tools.net.probe` tool fetches and is never anonymous.
++ - An anonymous egress tool declares `limits.perTargetPerMinute`.
++ - A client tool never fetches.
++ - `unavailable` needs `statusReason`.
++ - JSON output has a schema.
++- `tools.tool-list@1`: the `GET /api/v1/tools` answer (`tools`, `count`, `updated_at`, optional `families`).
++- `tools.run-request@1`: the body of `POST /api/v1/tools/{id}/run`, `{ input, files?, wait_ms?, idempotency_key? }`.
++ It can be JSON or multipart, like the jobs API. `files` references a Media object (`{ media_id }`)
++ or a result file of the caller's own job (`{ job_id, index }`), so tools can chain. The refusal
++ codes are listed in the description.
++- `tools.run@1`: the answer. Either `{ state: succeeded, tool, result: { data?, text?, files? }, took_ms, job? }`,
++ or `{ state: failed|cancelled, tool, error, took_ms, job? }`, or `{ state: queued|running, tool, job, location }`
++ (202).
++- `tools.job@1`: the jobs API's view of a job, read from `apps/_shared/jobs` `system.view()` today.
++ It carries state, progress, attempts, times, `expires_at`, result files (`storage`, `media`, `url`),
++ error, retry links and references. It also has an optional `tool`, set when the job came from a
++ run. `tools.job-request@1` is the body of `POST /api/v1/jobs`.
++- `contracts.tools` (`lib/tools.js`) adds three helpers:
++ - `checkDescriptor(d)` applies the schema plus the rules that depend on the id: `run.path` is the
++ tool's own, `$ref`s point at its own schema, `files.min <= files.max`, `legacy` never lists the
++ run API, and the quota class has no tier word.
++ - `checkList(list)` checks every descriptor, that ids and hosts are unique, and that counts are right.
++ - `jobInput(d, input)` returns `{ ...input, ...preset, tool: operation }`.
++
++**Capabilities**
++
++- These are `planned` until Tools serves their routes:
++ - `tools.tool.read`: public, quota class `tools-read`. `GET /api/v1/tools`, `GET /api/v1/tools/:id`
++ and `GET /api/v1/tools/:id/schema`.
++ - `tools.tool.run`: public, quota class `tools-run`, `tools.run-request@1` → `tools.run@1`.
++ `POST /api/v1/tools/:id/run`.
++ - `tools.net.probe`: **partner**, quota class `tools-probe`. Network grants it only through a
++ staff-set project allowance, never a default one. It covers port, ping, traceroute, mtr, latency
++ and bulk header and TLS checks through the API.
++- `tools.job.create` adds `POST /api/v1/jobs/:id/retry` and `PUT|DELETE /api/v1/jobs/:id/references/:ref`,
++ and declares `tools.job-request@1` → `tools.job@1`. `tools.job.read` and `tools.job.cancel` declare
++ `tools.job@1`.
++
++**Manifests and statuses**
++
++- Tools 0.4.0:
++ - lists the three new capabilities;
++ - gains `ready: /api/ready`;
++ - its notes now say that job results are in Media and job events reach OpenVibe.Events in
++ production, that the gateway has no jobs routes yet, and what ADR-027 plans.
++- `tools.job.created|started|succeeded|failed` are `active` (they were `planned`), because Tools
++ publishes them. Their descriptions no longer say planned.
++
++**Tests.** `test/run.js` checks:
++- every descriptor fixture with `checkDescriptor`;
++- that a run endpoint exists exactly when `api` is true;
++- that only job tools name a job;
++- the egress, probe and client rules, and tier-free quota classes;
++- that the run capabilities and the descriptor's `auth.capability` agree;
++- that a run's result files are exactly the job's, and that runs and jobs share the Idempotency-Key
++ rule and the job type pattern;
++- that the `tools.job.succeeded` event carries a subset of the job view;
++- the list's counts, ids and hosts.
++
## 0.32.0 — 2026-09-23
Additive: `compat.js` reports no breaking change against v0.31.0.
ddiff --git a/README.md b/README.md
iindex ba89ae17..6b607b42 100644
---- a/README.md
++++ b/README.md
@@@ -2,7 +2,7 @@
> Machine-readable contracts for the whole OpenVibe network.
--**Status:** alpha, v0.30.0 (first released in Wave 1). 74 schemas (53 of them event payloads), 30 service manifests, 170 capability manifests and 6 namespace manifests. 23 repositories run `openvibe-contracts-check` in CI, and deployed services verify service and app tokens with `serviceAuth` in production. Deployed consumers pin different tags (Games v0.8.0, the rest v0.11.0 to v0.29.0); Network, which serves the registry, pins v0.28.0. No N/N-1 compatibility fixtures exist yet.
++**Status:** alpha, v0.33.0 (first released in Wave 1). 95 schemas (66 of them event payloads), 30 service manifests, 176 capability manifests and 6 namespace manifests. 23 repositories run `openvibe-contracts-check` in CI, and deployed services verify service and app tokens with `serviceAuth` in production. Deployed consumers pin different tags (Games v0.8.0, the rest v0.11.0 to v0.29.0); Network, which serves the registry, pins v0.28.0. No N/N-1 compatibility fixtures exist yet.
**Plan:** OpenVibe Development Roadmap, Wave 1 (implementation plan rev 3, §3.1 and §18.11).
**License:** AGPL-3.0 (same as every OpenVibe service).
@@@ -60,7 +60,7 @@ TypeScript types: `generated/typescript/index.d.ts` (`"types"` in package.json).
| `events.event-envelope` | events | durable event shape for OpenVibe.Events (Wave 3) |
| `media.media-ref` | media | `med_<ULID>` (Wave 4) or transitional `legacy:<app>:<kind>:<id>` |
--Manifests: `manifests/services/` (30 services as of v0.30.0: 23 `alpha`, 1 `beta`, 5 `stable` and 1 `placeholder`, realtime; SDK and Shared are released libraries and Examples a repository with CI, none of them a runtime), `manifests/capabilities/` (173 capabilities as of v0.32.0; the v0.1 set was Media upload/read, chat send, paste create, coins credit/debit, notifications push, subject resolve and Community post). Each active capability names the route that implements it today. Where a service is reachable today (public, loopback only, library) is Network's observed overlay (`server/registry/exposure.js` there); manifest notes say whether a service is deployed and launched publicly.
++Manifests: `manifests/services/` (30 services as of v0.30.0: 23 `alpha`, 1 `beta`, 5 `stable` and 1 `placeholder`, realtime; SDK and Shared are released libraries and Examples a repository with CI, none of them a runtime), `manifests/capabilities/` (176 capabilities as of v0.33.0; the v0.1 set was Media upload/read, chat send, paste create, coins credit/debit, notifications push, subject resolve and Community post). Each active capability names the route that implements it today. Where a service is reachable today (public, loopback only, library) is Network's observed overlay (`server/registry/exposure.js` there); manifest notes say whether a service is deployed and launched publicly.
**Ids.** Subjects use prefixed ULIDs: `usr_`, `gst_`, `app_`, `mod_`. Services and system actors use slugs (`live`, `media`). Events use `evt_` and Media objects `med_`.
@@@ -84,7 +84,7 @@ Three `public` capabilities let a developer app (ADR-014) use OpenVibe.Events wi
## Event payload contracts (v0.30)
--Every event type has a payload contract, named after the type. The envelope's `version` is the contract's major, and the file is `contracts/events/payloads/<event_type>.v<version>.json`. The owner is the producing service, whose manifest lists the type in `eventsProduced`. Contracts with status `planned` describe events the owner does not emit yet (`ai.run.*`, `tools.job.*`).
++Every event type has a payload contract, named after the type. The envelope's `version` is the contract's major, and the file is `contracts/events/payloads/<event_type>.v<version>.json`. The owner is the producing service, whose manifest lists the type in `eventsProduced`. Contracts with status `planned` describe events the owner does not emit yet (`ai.run.*`). `tools.job.*` are `active` since v0.33.0, because Tools publishes them.
```js
if (env.payload.redacted === true) return; // a tombstone (events.tombstone-payload@1)
@@@ -124,6 +124,31 @@ One resolver answers "which channel, and whose, is this?" for channel, stream, V
The first input that resolves decides. Every other input that resolves must name the same channel, or the answer is `unresolved` with reason `conflict`. A clip belongs to the clipped channel, never to the clipper. A display name never resolves anything: a request holding only one is answered `unresolved`, `display_name_only`.
++## Tools platform API (v0.33)
++
++Every tool on openvibe.tools gets a descriptor, `tools.tool@1` (ADR-027). The page, the run API,
++openvibe-sdk and the docs all read it. It says:
++
++- where the tool runs (`execution`: `client` | `sync` | `job`);
++- whether the API exposes it (`api`), and how to call it (`run`);
++- its input schema, files, output, limits and auth;
++- its quota class and cost;
++- whether it fetches hosts the caller chose (`egress`).
++
++| Route (openvibe.tools) | Capability | Contract |
++|---|---|---|
++| `GET /api/v1/tools`, `/api/v1/tools/:id`, `/api/v1/tools/:id/schema` | `tools.tool.read` (public) | `tools.tool-list@1`, `tools.tool@1` |
++| `POST /api/v1/tools/:id/run` | `tools.tool.run` (public); probes need `tools.net.probe` (partner) | `tools.run-request@1` → `tools.run@1` |
++| `/api/v1/jobs…` (img, audio, docs; the gateway later) | `tools.job.create`, `.read`, `.cancel` | `tools.job-request@1` → `tools.job@1` |
++
++A run is answered inline, or with its job (202 + `Location`) while a job tool is still working.
++Callers are tiered: anonymous < session < user < app/service. Quotas count each tool's `cost` within
++its `quotaClass`. yt is page-only (`api: false`). The registry routes and the run route are `planned`
++until Tools serves them.
++
++`contracts.tools.checkDescriptor(d)` and `checkList(list)` apply the rules that JSON Schema cannot
++express, such as `run.path` being the tool's own. `jobInput(d, input)` builds a job tool's job input.
++
## Versioning and compatibility
- A contract id is permanent. Minor versions only add optional fields.
ddiff --git a/docs/adr/ADR-027-tools-platform-api.md b/docs/adr/ADR-027-tools-platform-api.md
nnew file mode 100644
iindex 00000000..e2c9b30f
---- /dev/null
++++ b/docs/adr/ADR-027-tools-platform-api.md
@@@ -0,0 +1,201 @@
++# ADR-027: Tools platform API
++
++**Status:** Accepted 2026-09-23. Contracts in openvibe-contracts v0.33.0; the registry and run routes
++are `planned` until OpenVibe.Tools serves them.
++
++## Context and current evidence
++
++The owner asked on 2026-09-23 for every tool to be open, modular, reachable through an API and the
++SDK, efficient, protected against abuse, and used across the other repositories. The survey of
++OpenVibe.Tools that day found:
++
++- **169 public tools in 9 families**, listed by the gateway's catalogue (`GET /api/catalog.json`,
++ re-served by Network). Each tool is described in three places that nobody reconciles: the gateway
++ copy, the satellite's `domain-map.js` and the satellite's `tools/index.js`.
++- **Only the jobs API is uniform.** `POST /api/v1/jobs` and the routes under it run only on the img,
++ audio and docs satellites (`apps/_shared/jobs/http.js`). Everything else has its own shape:
++ `POST /api/process` and `/api/process/direct` answer `{ success, download }` or `{ error }`,
++ `GET /api/net/<endpoint>/:target` answers `{ ok, … }`, and the `/api/dev/*`, yt and maps routes
++ each do something different. No endpoint states its input schema, its limits or how it runs.
++- **Browser-only tools have no API.** About 70 dev and text tools exist only as page JavaScript.
++- **Abuse controls are uneven.** Rate limits are in memory and per IP only. The limiter runs before
++ sign-in is read, so the signed-in tier never applies. Every first-party server caller shares
++ 127.0.0.1. The port tool accepts 20 ports a request at 60 requests a minute with no per-target
++ limit. myip trusts the first `X-Forwarded-For` entry the client sent. Sync endpoints skip the job
++ caps. Nothing records abuse.
++- **Some tools promise more than they do.** Six net tools have no implementation. traceroute, mtr
++ and latency are a TCP ping. Protect PDF does not encrypt, and PDF to image returns a placeholder.
++- **In the contracts,** `tools.job.*` did not list retry or references and had no schemas, and
++ nothing enforced a quota class.
++
++## Decision
++
++### One descriptor per tool, one run API
++
++- **`tools.tool@1` describes each tool.** It gives `id` (the catalogue id), `family`, `name`,
++ `summary`, `status` (`stable` | `beta` | `preview` | `unavailable`, and `statusReason` when a tool
++ is unavailable), `execution` (`client` | `sync` | `job`), `api`, `run`, `input` (JSON Schema),
++ `files`, `output`, `limits`, `auth`, `quotaClass`, `cost`, `egress`, `hosts` and `docs`. Tools
++ builds the descriptors from each tool's own code, which retires the three copies. The page, the
++ run API, `openvibe-sdk/tools`, the OpenAPI document and the docs all read them.
++- **Registry routes** (capability `tools.tool.read`, public). `GET /api/v1/tools` answers
++ `tools.tool-list@1`, with schemas as `$ref`. `GET /api/v1/tools/:id` answers the descriptor with
++ its schemas embedded. `GET /api/v1/tools/:id/schema` answers
++ `{ $schema, $id, $defs: { input, output } }`, which the `$ref`s point at.
++- **Run route** (capability `tools.tool.run`, public). `POST /api/v1/tools/:id/run` takes
++ `tools.run-request@1`, as JSON or as multipart with the same parts the jobs API takes. It answers
++ `tools.run@1`:
++ - `200` when the run finished: `succeeded` with `result`, or `failed` or `cancelled` with the
++ tool's own `error`;
++ - `202` with `Location` while a job is still `queued` or `running`, after waiting up to
++ `wait_ms`.
++
++ Refusals before the tool runs are problem+json. `run-request.v1.json` lists the codes.
++- **Execution modes.** A `sync` tool is answered inline. A `job` tool's run submits a job of
++ `run.job.type` whose input is `{ ...input, ...run.job.preset, tool: run.job.operation }`. The
++ job, its events, its files, retry and references are the existing jobs API (`tools.job@1`).
++ Output that is a file always comes from a job, so a result always has a download URL, an owner
++ and an expiry. Inputs can reference files that already exist: a Media object the caller may
++ read, or a result file of the caller's own job. That lets one tool's output feed the next
++ without a download.
++- **The gateway fronts every job route** under `/api/v1/jobs` (a facade over the satellites), so
++ callers and the SDK need one base URL.
++
++### Browser-only tools may get server engines
++
++A pure transform whose engine also runs in Node gets an opt-in server engine for the API: JSON,
++minify and beautify, case, slug, hash, base64 and the like. Its descriptor then says
++`execution: client` and `api: true`. The page stays browser-only, so "nothing leaves your browser"
++stays true for people using the page. A tool without such an engine says `api: false`.
++
++### YouTube downloader is page-only
++
++yt stays a page for people. It is not in the run API or the SDK, because of YouTube's terms and the
++legal exposure. Its descriptor says `api: false` and `run: null`.
++
++### Callers, tiers and quotas
++
++- **Tiers, lowest first:** `anonymous < session < user < app/service`.
++ - **anonymous:** no token and no session. Keyed by IP, IPv6 by its /64.
++ - **session:** the browser's `ov_tools_jobs` cookie.
++ - **user:** a Network user token or the `ov_token` cookie.
++ - **app/service:** a Network client-credentials token for audience `openvibe.tools`.
++- **Anonymous API use is allowed:** the tools are open. It gets the lowest allowance. A tool with
++ `auth.anonymous: false` (the heavy job types) needs at least a browser session or a token.
++ Sandbox app tokens keep their small allowance (ADR-014).
++- **Quotas count weight, not requests.** Each run counts its descriptor's `cost` against its
++ `quotaClass` (`tools-run`, `tools-job`, `tools-fetch`, `tools-probe`…), with an allowance per
++ tier. A 429 carries `Retry-After`, which openvibe-sdk honours.
++- **A class name describes the work, never a tier or a price.** Paid tiers are not built now, and
++ when they come they add allowances, not class names. `contracts.tools.checkDescriptor` refuses
++ a class name containing a tier word.
++- **Only one proxy hop is trusted.** Host nginx derives the client IP (`real_ip_header
++ CF-Connecting-IP` over the Cloudflare ranges), and every proxied location sets `X-Real-IP`,
++ `X-Forwarded-For` and `CF-Connecting-IP` from `$remote_addr`. Express `trust proxy` matches that
++ one hop. A client-sent `X-Forwarded-For` entry is never trusted.
++- **Challenge hook.** The guard has a no-op challenge hook, so a challenge such as Turnstile can be
++ added later without changing the API. None is used now.
++
++### Network probes need `tools.net.probe`
++
++Port checks, ping, traceroute, mtr, latency and bulk header or TLS checks send traffic to a host the
++caller chose. Through the API and the SDK they need the capability `tools.net.probe`, whose
++visibility is **partner**. Network grants it to a developer project only when staff add it to that
++project's allowance by hand, never through a default or sandbox allowance. First-party services
++hold it through their own grants. People keep the probe pages, which have a per-target throttle.
++
++A descriptor with `egress: true` either refuses anonymous callers or declares
++`limits.perTargetPerMinute`: runs per minute against one target, across all callers. A probe tool
++always has `egress: true` and `auth.anonymous: false`. A client tool never fetches.
++
++### Abuse log
++
++Tools records refused and throttled runs, and runs that trip an abuse rule. Each row holds:
++
++- the caller as `HMAC-SHA256(IP, daily salt)`;
++- the principal or user subject when there is one;
++- the tool, the reason and the time.
++
++**No raw IP is stored anywhere.** The salt rotates daily and is never kept after its day, so an
++old hash cannot be tied back to an address, not even by trying every IPv4 address. Rows are kept
++**30 days**. Under ADR-021 this is pseudonymous security data, not analytics. Its only use is
++acting on abuse: throttling, blocking a token or an app, reporting to the owner. It is never
++joined into analytics. Subject ids may appear because acting on a token or an app needs them,
++which ADR-021's analytics rule does not cover.
++
++### Legacy endpoints get deprecation headers
++
++`/api/process` (and its `/direct`, `/multi` and `/info` variants), `/api/net/*` and `/api/dev/*`
++keep working. Each answer carries:
++
++- `Deprecation: true`
++- `Sunset: Thu, 31 Dec 2026 23:59:59 GMT`
++- `Link: </api/v1/tools/{id}/run>; rel="successor-version"`
++
++A descriptor lists these routes in `run.legacy`, which gives the mapping. Removing them after the
++sunset is a later decision, informed by how much they are still used.
++
++### Honest status
++
++A tool whose engine is missing on the host has status `unavailable`: it is listed, its runs are
++refused with `503 tools.tool.unavailable`, and its page says so. PDF protection becomes real
++(qpdf, AES-256) and PDF to image uses pdftoppm. They stay `unavailable` until `qpdf` and
++`poppler-utils` are deployed. Probe tools that only do a TCP ping describe themselves as that.
++
++## Alternatives considered
++
++- **Keep one API per satellite and document it.** Rejected: the shapes, error bodies and auth
++ differ per satellite, and the SDK would need one client per tool family.
++- **Jobs only, with no inline runs.** Rejected: a DNS lookup or a JSON minify would cost a job row,
++ a poll and a file, for a millisecond of work.
++- **Probes open to anonymous API callers.** Rejected: the port tool can already be used to scan
++ third parties, and a public API makes that cheap to automate. People keep the pages.
++- **The downloader in the API.** Rejected on YouTube's terms and legal exposure.
++- **Raw IPs in the abuse log, deleted later.** Rejected: a daily-salted HMAC is enough to link one
++ day's abuse, and it never becomes an address list.
++- **A challenge (Turnstile) now.** Deferred: it needs owner setup. The hook keeps the option open.
++
++## Migration consequences
++
++In order:
++
++1. Contracts v0.33.0 (this ADR).
++2. The Tools registry and descriptors.
++3. The shared guard (tiers, quotas, semaphores, upload sniffing, ffmpeg hardening, egress
++ throttles, the abuse log), first in report mode.
++4. Worker isolation, so pdf-lib and sharp stop blocking the event loop.
++5. The run API, the gateway jobs facade and the legacy headers.
++6. Network grants and the sandbox allowance.
++7. openvibe-sdk `tools` and the jobs client's retry and references.
++8. Callers move over: Live kiosk page titles, Chat MP3 conversion, Community "save as paste",
++ Codes docs, the Search index, the Network launcher's recent tools.
++
++`tools.tool.read`, `tools.tool.run` and `tools.net.probe` become `active` in the contracts release
++after Tools serves their routes.
++
++## Rollback
++
++Every new route is additive. Turning the run API off leaves the pages and the legacy routes as they
++were. The guard starts in report mode, and enforcement is a switch. Deprecation headers are
++informational. Deleting the abuse log's table removes it completely, and it holds no raw data to
++leak.
++
++## Acceptance tests
++
++- openvibe-contracts `test/run.js`:
++ - every descriptor fixture passes `tools.tool@1` and `checkDescriptor`;
++ - api false never has a run endpoint, and api true has its own path and an input schema;
++ - only job tools name a job, and file output through the API comes from one;
++ - an anonymous egress tool has a per-target throttle, and a probe is never anonymous;
++ - a client tool never fetches;
++ - quota classes carry no tier word;
++ - a run's result files are exactly the job's;
++ - the list's ids, hosts and counts are consistent.
++- In Tools, a registry test runs `checkDescriptor` and `checkList` over every tool, and every
++ catalogue id has a descriptor.
++- Run API tests cover each refusal code, anonymous versus session versus token tiers, and a 429
++ with `Retry-After`.
++- A probe run without `tools.net.probe` gets 403.
++- yt's run answers `404 tools.tool.not_runnable`.
++- Legacy routes carry `Deprecation`, `Sunset` and `Link`.
++- No raw IP appears in the abuse log, and nothing in it is older than 30 days.
ddiff --git a/docs/adr/README.md b/docs/adr/README.md
iindex a30c399c..66c7ae1f 100644
---- a/docs/adr/README.md
++++ b/docs/adr/README.md
@@@ -30,4 +30,4 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each
| [ADR-024](ADR-024-theme-authority.md) | Theme authority between Network and Shared | Accepted 2026-09-23 |
| [ADR-025](ADR-025-marketplace-scope.md) | Marketplace and commerce scope | Accepted 2026-09-23: Trade stays informational |
| [ADR-026](ADR-026-event-redaction.md) | Producer redaction of its own events (`payload.redacts`) and event payload contracts | Accepted and implemented 2026-09-23 (OpenVibe.Events, Chat) |
--
++| [ADR-027](ADR-027-tools-platform-api.md) | Tools platform API: descriptors, one run API, caller tiers, probes, abuse log, legacy sunset | Accepted 2026-09-23; contracts v0.33.0, routes planned |
ddiff --git a/package-lock.json b/package-lock.json
iindex bdaec06e..c32a41dd 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-contracts",
-- "version": "0.32.0",
++ "version": "0.33.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-contracts",
-- "version": "0.32.0",
++ "version": "0.33.0",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 445e6ec2..0d4c22ce 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.32.0",
++ "version": "0.33.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",