openvibers/openvibe.run README.md

README.md

main 25.6 KB · 310 lines · markdown

1# OpenVibe.Run
2
3The job service of the network (openvibe.run). Design: OpenVibe.Contracts docs/adr/ADR-034-platform-north-star.md;
4plan track T14.
5
6**Live site:** https://openvibe.run · **API docs:** https://openvibe.services · **For AI:** https://openvibe.run/llms.txt
7**Run it:** `npm ci && npm run dev` (Node 22 or newer) · **Tests:** `npm test`
8
9## Purpose
10
11OpenVibe.Run accepts jobs, places each one on a node that advertises the runtime class it needs, and follows it to
12its end. A job belongs to a **project**, is billed per wall-clock second it runs, and is visible only to the token
13that project presents. Run owns the API, the project scope and the placement; OpenVibe.Bot owns the node link and the
14per-second metering of the jobs it carries (plan T14 L1). The one exception is the external managed-browser provider
15(plan T14 R4a): Run meters its jobs itself and posts their readings with a Billing token of its own, used for nothing
16else.
17
18This repository is at plan **T14 steps 5 (R1b), 6 (R1c) and 7 (R3)**: the API is complete, and a queued job is
19placed from the Fabric's offers and actually runs — the poller in `server/jobs/poller.js` (started by
20`server/index.js`) sends it to OpenVibe.Bot, mirrors Bot's state and the node's stdout back, and settles it. Without
21`OV_OAUTH_CLIENT_SECRET` Run cannot mint the Network token Bot requires, so the bridge is off and every job stays
22`queued`.
23
24## How a job runs
25
261. **Submit** — `POST /api/v1/jobs` (capability `run.job.submit`). Run validates the request against
27 `run.job-create-request@1`, mints the `job_<ULID>` id, stores the row `queued` and writes `run.job.queued` in the
28 same transaction. Nothing leaves the process here: a submit never waits on Network or Bot.
292. **Place** — the poller (`RUN_POLL_MS`, default 1000 ms) reads the Fabric's offers for the job's class
30 (`GET {OV_NETWORK_URL}/api/v1/offers?kind=node`), and `openvibe-sdk/placement`'s `plan()` chooses the node the
31 job's own `requirements` allow. Run records the node, provider and region on its row (`placed_at` included).
32 No eligible offer: the job waits queued (a node may be briefly offline) and every tick tries again; if its
33 `ttl_ms` runs out first it ends `expired` with `run.job.unplaceable` (ADR-036 section 2).
343. **Send** — Run hands the `platform.job@1` body to Bot (`POST {RUN_BOT_URL}/api/v1/jobs`, capability
35 `bot.job.dispatch`, audience `openvibe.bot`) and Bot gives it to the node over its device link. Bot's dispatch is
36 idempotent by job id, so a retry after a crash or a timeout is safe.
374. **Mirror** — every tick mirrors each `placed` or `running` job from `GET {RUN_BOT_URL}/api/v1/jobs/{id}`: state,
38 timings, exit, result and the metered wall clock (Bot is the meter; Run never posts a usage record for a job Bot carries). The state
39 change and its `run.job.started|succeeded|failed|cancelled|expired` event are one transaction, and the job's
40 stdout is carried into its SSE stream as `output` events. A job Bot answers 404 for after Run sent it fails
41 `run.job.worker_failed` after `RUN_DISPATCH_UNKNOWN_POLLS` polls — it is never mirrored forever.
425. **End** — `succeeded`, `failed`, `cancelled` or `expired`, from Bot's `job_exit` (or from the cancel request and
43 the ttl sweep). An end state never changes. Bot's answer is never trusted over Run's own row: it can neither
44 rename the node nor the project of a job Run placed.
45
46## Owns
47
48- The public site at openvibe.run (`server/web/home.js`, `server/app.js`): the front page in the OpenVibe Frame (what a
49 job is and how it runs, the API in brief), the legal pages (`openvibe-shared/legal`), `robots.txt`, the sitemap,
50 `/llms.txt` and `/llms-full.txt`, IndexNow (only with `INDEXNOW_KEY`: the key file and one ping at boot), the icon and the web manifest. It says only what works today; `test/site.test.js` checks it.
51- The job API (`server/api/v1.js`, one capability per route — the table below) and the job's lifecycle record
52 (`run_jobs`, `migrations/0001_jobs.sql`): state, placement, timings, exit, result and the metered wall clock.
53- The project scope: a job's `project_id` and `requester` come from the caller's Network token
54 (`claims.project_id`, `claims.sub`) and never from a request body; another project's job is `404`, never `403`.
55- Project delegation (ADR-003 amendment 2026-10-12): a first-party service acting for a person (`X-OV-Subject`) may
56 name a project with `X-OV-Project`. Run asks Network whether the person is an admin or owner of the unarchived
57 project (`network.project.member.read`, a yes cached 60 s), then the person is the requester and the service is
58 stored as `via` (`migrations/0002_job_via.sql`). The service submits, reads, cancels and gets a stream ticket for only
59 the jobs it started for that person (another person's or another service's job is `404`); list and a delegated Bearer
60 stream are `403 project.not_delegated`, and so is any other caller naming a project. Network unreachable is
61 `503 project.check_unavailable`. Watch's `run-job` action and OpenVibe.Codes' coding jobs use it.
62- The `job_<ULID>` id (minted here, passed unchanged to Bot as `platform.job@1` `id`, so every metering reading of
63 the job is `run:<id>:<n>`).
64- Idempotent submit: one row per `(project_id, idempotency_key)`, a repeat answering the first job, a different body
65 with the same key refused `409 run.idempotency.conflict`.
66- The `run.job.*` events (`server/jobs/events.js`, `run_events_outbox`), written in the same transaction as the
67 state change they describe.
68- The job stream (`server/jobs/stream.js`): one ring buffer per open job, the SSE `id` being the job's own monotonic
69 `seq`, plus the two-minute single-use stream ticket (`server/jobs/ticket.js`).
70- The `platform.workload-requirements@1` default (`{ kind: run.<class>, mobility: job, latency_class: background,
71 objective: balanced }`, the contract's own default) and the project caps on a job's limits.
72- Placement (plan T14 R3, `server/dispatch/placement.js`): the job's requirements are stated by Run and the choice
73 among offers is `openvibe-sdk/placement` `plan()` — Run writes no placer of its own (ADR-036). The job's
74 `worker:<class>` capability is what a candidate must advertise. A user-owned offer is a person's own machine
75 (contracts 0.161.0): it is eligible only for that person's own job (requester `user:usr_…` equal to the offer's
76 `owner`) whose requirements name `user-owned`. Network keeps such offers off the public list, so Run reads them,
77 owners included, from `GET /internal/resources?kind=node&trust=user-owned` with its own client
78 (`network.registry.read`); a user-owned offer on the public list is never used.
79- The poller (`server/jobs/poller.js`): the ttl sweep, the send, the mirror, the cancel-on-Bot and the job's stdout
80 into the stream.
81
82## Does not own
83
84- The node link, the device credentials and the per-second metering: OpenVibe.Bot (`bot.job.dispatch`), which gives
85 the job to a node advertising `worker:<class>` and meters every wall-clock second into `platform.usage-sample@1`
86 readings keyed `run:<job id>:<n>`.
87- Identity, tokens and projects (OpenVibe.Network); Run only verifies the tokens it is the audience of.
88- The artifacts a job runs and the media its inputs come from (OpenVibe.Media; Run never fetches a caller-chosen
89 URL).
90- Money for the jobs Bot carries: Bot writes their usage records. Run writes only the external provider's
91 (`billing.usage.record`, clamped to Run's own observed duration + 5 s).
92
93## Depends on
94
95- PostgreSQL 18: `run_jobs` and the `run_events_outbox`; `migrations/` applies on boot.
96- Valkey (`VALKEY_URL`, optional): shared state between processes; unset, nothing is shared.
97- OpenVibe.Network: the RS256 signing key (JWKS) every token is verified against; the offers Run places from
98 (`GET /api/v1/offers`, public); Run's client `run` mints the events relay's service token, its Bot token and the
99 token for project memberships (`GET /internal/projects/:project/members/:subject`, project delegation).
100- OpenVibe.Events: `run.job.*` events go to `events.event.publish`; unset, they wait in `run_events_outbox`.
101- OpenVibe.Bench (`RUN_BENCH_URL`, default `https://bench.openvibe.services`): a race's judgement is recorded as a
102 `bench.match@1` on `POST /api/v1/matches` (`bench.judgement.cast`, audience `openvibe.bench`). Bench has no
103 matchmaker endpoint yet; `matchmaker: true` falls back to `RUN_RACE_ENTRANTS`.
104- OpenVibe.Bot (`RUN_BOT_URL`, `RUN_POLL_MS`): the node link and the meter. Run sends each job to it
105 (`bot.job.dispatch`, audience `openvibe.bot`, minted from `OV_OAUTH_CLIENT_ID`/`OV_OAUTH_CLIENT_SECRET` against
106 `OV_NETWORK_INTERNAL_URL` and cached until 60 s before expiry) and mirrors its answer. Unset the client secret and
107 the bridge is off: `/api/ready` skips the `bot` check and every job stays `queued`.
108- `openvibe-contracts` v0.106.0, `openvibe-sdk` v0.74.0 and `openvibe-shared` v4.179.0 (package.json).
109
110## Capabilities and routes
111
112Every route checks exactly the capability its manifest names (`contracts/manifests/capabilities/run.job.*.json`),
113from the caller's service token. A Network user token names no project and holds no capability here, so it is
114refused like any other token without the grant.
115
116| Method and path | Capability | Request → answer |
117|---------------------------------|------------------|--------------------------------------------------------|
118| `POST /api/v1/jobs` | `run.job.submit` | `run.job-create-request@1` → `run.job-create-result@1` (201 created; 200, `created:false` on an idempotent repeat) |
119| `GET /api/v1/jobs/:id` | `run.job.read` | `common.no-body@1` → `run.job-read-result@1` |
120| `GET /api/v1/jobs?state=&limit=&cursor=` | `run.job.list` | `run.job-list-query@1` → `run.job-list-result@1` |
121| `POST /api/v1/jobs/:id/cancel` | `run.job.cancel` | `common.no-body@1` → `run.job-read-result@1` |
122| `POST /api/v1/jobs/:id/stream/ticket` | `run.job.stream` | `common.no-body@1` → `run.job-stream-ticket-result@1` |
123| `GET /api/v1/jobs/:id/stream?ticket=` | `run.job.stream` | a ticket in the query, **or** a Bearer service token with the grant → `text/event-stream` of `run.job-stream-event@1` |
124| `GET /api/v1/admin/jobs` | `run.job.admin` | `run.job-list-query@1` (with `project_id`) → `run.job-list-result@1` |
125
126`run.job.admin` additionally reads and cancels any project's job and is the only way to pass `project_id` to a list
127(the manifest says so); it is internal, so only a first-party `svc:` token may hold it — an app, mod or agent token
128with the grant is refused. Every request body and query string is validated against its released contract, and so is
129every answer before it is sent; errors are RFC 9457 `application/problem+json`.
130
131## Checks
132
133The checks scheduler (`server/checks/`, [docs/checks.md](docs/checks.md)) turns a matching GitHub workflow job into a
134Run `code` job on a node and reports a GitHub check run. The owner creates the App once at `/admin/checks` (the
135manifest flow); GitHub calls `POST /webhooks/github`; `GET /api/v1/checks/agreement?repo=` gives the owner the node vs
136Actions agreement. Its secrets (the App key, webhook secret, client secret and installation tokens) are sealed with
137`RUN_SECRETS_KEY`; `RUN_CHECKS_PROJECT` is the project a check job belongs to.
138
139## Sessions
140
141A session (contracts 0.223.0, the Desks plan's D6) is a browser desk Run places for one person and OpenVibe.Bot leases
142to it (`bot.desk.session`, `server/sessions/`). Unlike jobs, a session is a person's own: their Network token, or a
143first-party `svc:` token holding the capability and naming them in `X-OV-Subject` (recorded as `via`).
144
145| Method and path | Capability | Request → answer |
146|-----------------------------------------|-----------------------|------------------|
147| `POST /api/v1/sessions` | `run.session.create` | `run.session-create-request@1` → `run.session@1` (201; 200 on an idempotent repeat) |
148| `GET /api/v1/sessions?state=&limit=&cursor=` | `run.session.read` | → `run.session-list-result@1` (running ones by default) |
149| `GET /api/v1/sessions/:id` | `run.session.read` | → `run.session@1` |
150| `POST /api/v1/sessions/:id/commands` | `run.session.control` | `run.session-control-request@1` (screen, input, browser) → `run.session-control-result@1` |
151| `POST /api/v1/sessions/:id/end` | `run.session.control` | → `run.session@1`, ended |
152
153- **Placement.** The request's own `machine`, or nowhere. Otherwise the hosts Bot lists for the person: online with
154 room, in the trust classes the requirements allow (first-party, then community, by default; user-owned only when
155 the requirements name it), most free desks first. A full host is skipped; with none, the session waits `placing`
156 until its `ttl_s`, then ends `unplaceable`.
157- **Its life.** `ttl_s` (default 3600) bounds it. `idle_s` (default 900) without a command ends it. Every
158 `RUN_SESSION_LEASE_CHECK_MS` (30 s) Run asks Bot whether the desk is still leased: a host that dropped it ends the
159 session `host_lost`. A person holds `RUN_SESSIONS_PER_PERSON` (3) at once.
160- **Metering.** One `platform.usage-sample@1` per minute of desk time (`run:<session>:<minute>`, operation
161 `browser.desk`, the last one the remainder) in `run_usage_outbox`, posted to Billing by the usage relay.
162- **Events.** `run.session.started` and `run.session.ended`, in the transaction that records each.
163
164## Jobs
165
166- **Submit.** `Idempotency-Key` (or `body.idempotency_key`) makes a retry safe; with neither, Run mints its own key
167 and returns it in the read result (`auto:<ulid>`). The row is unique per `(project_id, idempotency_key)` and that
168 index is permanent, so a key is spent for good: a repeat always answers the first job, and the same key with
169 different work is `409` — the contract promising that "within 24 h" is answered the same way, forever, rather than
170 reused. `limits` are capped by the project's (`RUN_LIMITS_*`, `422 run.limits.exceeded`) — the lifetime too, whether
171 named or defaulted — `args` over 512 KiB are refused `413 run.args_too_large`, and `ttl_ms` defaults to
172 `limits.wall_ms + 600000` exactly as the contract says.
173- **States.** `queued → placed → running → succeeded | failed | cancelled | expired`. Run stores `queued` and, once
174 the job is on a node, records `placed` itself; `running` and the end states arrive from Bot. An end state never
175 changes, so a replay of a cancel is a no-op rather than a second event.
176- **Events.** `run.job.queued|started|succeeded|failed|cancelled|expired`, payloads being the
177 `run.job-read-result@1` projection their contracts describe. The outbox row is keyed `run:<job id>:<state>` (one
178 row per job and state, so a replay is one row); the published envelope carries a contract-valid `evt_<ULID>`
179 document id, because `events.event-envelope@1` pins that pattern and OpenVibe.Events validates it on publish.
180- **Stream.** `POST /jobs/:id/stream/ticket` mints a two-minute single-use RS256 ticket for that one job (audience
181 `openvibe.run`, `typ run-stream`, its own key — never Network's signing key, so it is never a session anywhere).
182 `GET /jobs/:id/stream?ticket=…` replays what is kept (the last 1 MiB) and follows; a service that can send a header
183 opens the same stream with a Bearer token holding `run.job.stream` for the job's project (the manifest allows
184 both). `Last-Event-ID` resumes after the last `seq` seen; an ended job replays what is kept and closes with
185 `type: 'end'`. Reconnecting never re-runs a job. The `output` events are the node's `job_stdout` chunks as Bot
186 holds them (best effort: a chunk lost with the node's link is never resent, and a gap Bot dropped is dropped here
187 rather than streamed twice). The ring is this process's memory (reclaimed once it ended or went idle, and rebuilt from the job's row when
188 a stream opens again), so `seq` is this process's counter: a resume across a restart gets the current state rather
189 than a gap, and output is best effort either way.
190- **The `job` record.** `run_jobs.job` holds the `platform.job@1` body Run will send (id, class, artifact, args,
191 ttl_ms, limits, net, inputs) plus the fields `platform.job@1` has no place for and `0001_jobs.sql` has no column
192 for: `requirements` (required by `run.job-read-result@1` and by placement), `placed_at` (`timings.placed_at`) and
193 `cancel_requested` (a cancel of a placed or running job). `store.frameOf(record)` builds the frame body from the
194 `platform.job@1` keys alone, and the dispatcher bridge validates it before anything is sent.
195- **Cancelling.** A **queued** job was never sent anywhere: cancelled at once (`exit` stays null, usage stays 0). A
196 **placed or running** job may be on a node this instant, so it is marked `cancel_requested`, Bot is asked for its
197 `job_cancel` (once per job — Bot repeats it on every reconnect until the job ends — and the poller retries the
198 request until it lands), and the job reaches `cancelled` when Bot says so. Run never answers `cancelled` for work a
199 node may still be doing. Cancelling an ended job answers it as it is.
200- **Usage.** `usage.wall_ms` and `usage.seconds` are Bot's metered wall clock (Bot writes the
201 `run:<job id>:<n>` readings; Run never posts those). A running job therefore shows 0
202 until its `job_exit` carries the wall clock: Run does not estimate a number it cannot back. A job the managed-browser
203 provider ran is metered by Run: the provider's claimed `wall_ms`, never above Run's own observed duration (its send
204 to the answer that showed the end) + 5 s — a larger claim is clamped and logged — queued as `run:<job id>:<n>`
205 readings in `run_usage_outbox` with the end state and relayed to Billing (`RUN_BILLING_URL`; unset, they wait).
206- **Placement.** The node is chosen per tick from the offers Network publishes
207 (`GET {OV_NETWORK_URL}/api/v1/offers?kind=node`) by `openvibe-sdk/placement` `plan()`, on the job's own
208 `requirements`; `node_id`, `provider` and `region` are recorded on Run's row. A class no node offers keeps the job
209 queued until an offer appears or its `ttl_ms` ends it `expired` with `run.job.unplaceable`. Offers are cached for `RUN_OFFERS_TTL_MS`; a Network that does not answer
210 is retried, never a reason to fail a job.
211
212## Races (the Arena on the one engine)
213
214A race is the Arena's round (compare-arena-ecosystem.md §3): the same prompt, two to four entrants, one child job per
215entrant on the same machine class (`linux`), the same share of the parent budget each, and no entrant named until a
216person judges. The play is data — `agents.play@1`, id `race`, one role per side, one parallel phase, `blind: true`,
217`stop.when: person` — listed by `GET /api/v1/plays`. Contracts 0.422.0 has no `run.race@1`, so the request and answer
218are validated in `server/races/service.js`; the `bench.match@1` the judgement records is validated before it is sent.
219
220| Method and path | Capability | Request → answer |
221|-----------------------------------|------------------|------------------|
222| `GET /api/v1/plays` | `run.job.list` | → `{ plays: agents.play@1[] }` |
223| `POST /api/v1/races` | `run.job.submit` | `{ prompt, kind: chat\|code\|design, entrants: [2–4 bench.entrant ids] \| matchmaker: true, budget: { usd?, wall_ms?, cpu_ms?, mem_bytes? } }` → 201 race |
224| `GET /api/v1/races/:id` | `run.job.read` | each side's output so far (the job stream's ring), its state and, once ended, its receipts; names only after a judgement |
225| `POST /api/v1/races/:id/judgement` | `run.job.submit` | `{ winner: a\|b\|tie\|both_bad }` → 201 race with the names revealed |
226
227- **Who may start and judge.** A person, through a first-party service (`X-OV-Subject` + `X-OV-Project`): Run records
228 the person as the requester and the service as `via`. Only that person, through that service and project, may read
229 or judge it; anyone else gets the same `404 run.race_not_found` a missing race does.
230- **The split.** `budget` is the round's total and each side gets `floor(total / sides)` of wall, CPU and memory
231 (memory floored to bytes); `usd` is split and recorded, not metered by Run. A total too small for every side is
232 `422 run.limits.exceeded`; the per-side limits then pass the project caps like any job.
233- **The child.** `class: linux`, `args: { play, role, race, kind, prompt, entrant, budget }`, with
234 `job.parent = { type: 'race', id }` (dropped by `frameOf`, so Bot never receives it). Placement, metering, events
235 and the stream are the ordinary job path.
236- **The judgement.** One `bench.match@1`, source `arena`, the shown order (`a, b, …`, randomised when the race is
237 created), `blind: true`, each ended side's receipts, `play: { id: 'race', run_id }`, and one `human_pairwise`
238 judgement whose `by.id_hash` is an HMAC of the person (`RUN_RACE_HASH_KEY`, falling back to the Network client
239 secret). Bench records it before Run writes the judgement and reveals the names; a Bench that cannot answer leaves
240 the race blind and retryable with the same match id.
241- **The matchmaker.** Bench has no matchmaker endpoint yet (checked 2026-11-13). `matchmaker: true` picks two entrants
242 of different model families at random from `RUN_RACE_ENTRANTS`; unset, it answers `422
243 run.race_matchmaker_unavailable` and names the setting.
244
245## Tests
246
247```
248npm test # every file in test/, PGlite (in-process PostgreSQL)
249npm run test:pg # the same on the production-shaped containers (openvibe-sdk scripts/test-services.sh up)
250node test/jobs.test.js # the job API against the released contracts
251node test/dispatch.test.js # the dispatcher bridge: offers → placement → Bot → mirror → stream, cancel, ttl
252node test/app.test.js # the skeleton: health, readiness, release.json, /metrics, the bridge's off state
253```
254
255`test/helpers/app.js` boots Run against an in-process OpenVibe.Network stub (its JWKS, its tokens, the token
256endpoint Run mints from and the offers registry), so nothing needs the network, OpenVibe.Bot or a running service.
257`test/dispatch.test.js` runs a fake OpenVibe.Bot as a real HTTP server that verifies the Network service token Run
258mints for it (audience `openvibe.bot`, capability `bot.job.dispatch`), so the bridge is exercised over the wire
259rather than through a fetch stub.
260
261## Security
262
263- Every route checks one capability; the project and the requester come from the token, never the body.
264- Another project's job is `404 run.job_not_found`, never `403`: an id's existence is never confirmed.
265- The stream route accepts a valid, unexpired, unused ticket for that job, or a Bearer service token holding
266 `run.job.stream` for the job's project — a token in the query string is refused like any other non-ticket.
267- Bot is a trusted peer but never an authority: its answer cannot rename the node or the project of a job Run
268 placed, cannot move a job's state backwards, and cannot make Run carry out work the caller did not ask for. Run's
269 own row, its offers and the job's requirements decide.
270- Run's Bot token is minted from OV_OAUTH_CLIENT_ID/SECRET, cached until 60 s before expiry, never logged and never
271 put in an error; the job body is validated against `platform.job@1` before it is sent.
272- Limits are bounded by the contract and the project's caps; nothing is silently lowered.
273- No secret is logged (no `Authorization` header, no ticket, no job output), and nothing starts at module load.
274- See SECURITY.md to report a problem.
275
276## Deploy
277
278On openvibe-ovh since 2026-10-07: unit `openvibe-run` on loopback `:4920`, release layout under `/opt/openvibe.run`,
279env `/etc/openvibe/run.env`, database `ov_run`; deploy with `sudo ovhost deploy run`. First-party callers reach the job
280API on loopback, and jobs are placed on people's own Bot nodes. `openvibe.run` is parked (a 302 to openvibe.network)
281until Run has a product surface; the vhost below is installed then.
282
283- `deploy/nginx/openvibe.run.conf` — the vhost (TLS, www → apex, rate limits, client IP from `$remote_addr` only,
284 `/internal/` and `/metrics` never proxied).
285- `deploy/systemd/openvibe-run.service` — unit `openvibe-run`, `WorkingDirectory=/opt/openvibe.run`,
286 `EnvironmentFile=/etc/openvibe/run.env`, `PORT=4920`.
287- `/api/health` and `/api/ready` are for the loopback caller; `/metrics` refuses anything that arrived through a
288 proxy.
289
290## Deploy files
291
292| File | What it is |
293|------|------------|
294| `deploy/nginx/openvibe.run.conf` | The nginx vhost for openvibe.run (reference; deployed to `/etc/nginx/sites-available/`) |
295| `deploy/systemd/openvibe-run.service` | The systemd unit (release layout `/opt/openvibe.run`) |
296| `.github/workflows/ci.yml` | CI: the shared test job (syntax, tests, `openvibe-contracts-check --service run`) and the shared security job |
297| `migrations/0001_jobs.sql` | `run_jobs` and `run_events_outbox` |
298| `migrations/0004_telemetry.sql` | `telemetry_samples`: request telemetry (openvibe-sdk 0.46, `platform.telemetry-sample@1`) — each HTTP request aggregated by route template, method and status class, never the raw URL, with `/shared` files, static assets, health, readiness and `/metrics` not counted; once a minute one `http.request` sample per key, plus the active-requests, p95 latency and event-loop-lag gauges; rows older than 14 days deleted daily; wired in `server/app.js` (`telemetryMiddleware`, a no-op until init) and `server/index.js` (`telemetry.init()` at production start, retention, flush on stop) |
299| `STATUS.json` | What this repository is, what it does today and where it runs |
300
301<!-- versions:start -->
302- openvibe-contracts: v0.422.0
303- openvibe-sdk: v0.161.0
304- openvibe-shared: v4.241.0
305<!-- versions:end -->
306
307Compute placement, pool autoscaling, rental caps and the operator view are documented in
308[docs/compute-broker.md](docs/compute-broker.md). Rentals default to quote-only; keys and caps
309are never inferred from available credentials.
310