openvibers/openvibe.watch AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Watch |
| 2 | |
| 3 | ## Project overview |
| 4 | |
| 5 | OpenVibe.Watch (`https://openvibe.watch`) is the network's place for a **person or an agent** to define |
| 6 | persistent observations and conditions over pages, feeds, APIs, webhooks and OpenVibe events, and an |
| 7 | action when they change or fire — cheaply, because a Watch prefers an event or the site's own ETag |
| 8 | over polling. The API is loopback-only; the public site is server-rendered. |
| 9 | |
| 10 | - **Entry:** `server/index.js`; app `server/app.js`; port `4730`, service id `watch`. |
| 11 | - **API:** `server/api/` — `watches.js`, `checks.js`, `provider-rates.js`, `resource-index.js`, |
| 12 | `resource-control.js`, `actor-limits.js` (all `/api/v1`); `server/http/` — `pages.js`, |
| 13 | `discovery.js`, `site-limits.js`, `network-reads.js`. |
| 14 | - **Carriers (binding order):** `server/carriers/` — `event.js` (OpenVibe.Events), `feed.js`, `api.js`, |
| 15 | `node.js` (Link probes from the owner's machine), `http.js`, `webhook.js`; the preference logic is in |
| 16 | `index.js`. |
| 17 | - **Checks:** scheduler, conditional fetch and backoff in `server/check*`/`server/net/fetcher.js`; |
| 18 | the outbound guard is `server/net/guard.js`; checks and observations live in `check_runs`/ |
| 19 | `observations` with `watch_endpoint_state`. |
| 20 | - **Events and actions:** `server/events/` (`envelope.js`, `agent-step.js`), actions in the check path |
| 21 | (notification, event, webhook, Actor task/agent run, Run job, Codes task). |
| 22 | - **Pages:** `server/render/` (`watch.js`, `layout.js`, `html.js`, `changelog.js`, `short.js`); no own |
| 23 | browser script layer, CSS in `public/css`; every page complete without JavaScript. |
| 24 | |
| 25 | ## Owns, does not own |
| 26 | |
| 27 | Owns the watch registry (`watches`, `watch_endpoint_state`) and the `watch.watch@1` / |
| 28 | `watch.watch-request@1` / `watch.watch-result@1` shapes; checks (`check_runs`, `observations`, the |
| 29 | scheduler, conditional-fetch state, backoff); the public site's sign-in sessions (`web_sessions`) and |
| 30 | pages; `watch.*` events; provider-rate evidence (`WATCH_PROVIDER_RATES`, off) — the DeepInfra and |
| 31 | Featherless official price documents as immutable observations, the pending change and a person's dated |
| 32 | approval. |
| 33 | |
| 34 | Does not own: structured source ingestion for publication (Sources — Watch observes what one person or |
| 35 | agent asked for, not what a product publishes); expensive browser/code execution (Run, step 6) and |
| 36 | local/LAN probes (Link, step 7) — Watch asks them, they run; interpreting a trigger and acting (Actor, |
| 37 | Codes); delivery of a notification (Network consumes `watch.watch.triggered`); the supply rate card |
| 38 | (AI's catalog — an approved rate change is evidence AI's operator applies). |
| 39 | |
| 40 | ## Who calls it, what it calls |
| 41 | |
| 42 | Every `/api/v1` route needs a Network token for audience `openvibe.watch` with that route's **one** |
| 43 | capability: a service token is judged by its `cap` claim, a person/agent token by identity |
| 44 | (`usr_…`/`agt_…`), and a first-party `svc:…` may send `X-OV-Subject` to act for a person (an app or |
| 45 | module may not). Watch calls Events (sources and |
| 46 | outbox), Live/Community/anything through the generic pull carriers, Link for node probes, Run for |
| 47 | browser/code checks (not yet), Actor/Codes/Run for actions, Network (JWKS, sessions, notifications), |
| 48 | and a webhook target only through the guarded POST. The vhost keeps `/api/v1/*` and `/internal/*` |
| 49 | loopback-only. |
| 50 | |
| 51 | ## Database and migrations |
| 52 | |
| 53 | PostgreSQL `ov_watch` through `openvibe-sdk/db`: `DATABASE_URL` through PgBouncer, owner role for |
| 54 | migrations; `migrations/` are applied at boot and only add. Tests use PGlite (slow to boot — 180 s per |
| 55 | file) or `WATCH_TEST_STORE=pg`; never edit a migration that has run, and rollback must work. A check |
| 56 | writes a `check_runs` row and observations; a conditional fetch stores its validators only after a |
| 57 | good answer. |
| 58 | |
| 59 | ## Auth, ownership and exposure |
| 60 | |
| 61 | Tokens are Network RS256 JWTs verified offline against JWKS. Reads and writes are scoped to the acting |
| 62 | subject; another owner's watch answers 404, not 403. `X-OV-Subject` is honoured only from a first-party |
| 63 | service. `/auth/*`, the pages and discovery files are public; the API and `/internal/*` are |
| 64 | loopback-only; `/metrics` answers direct loopback callers only. `/internal/events` is signed and |
| 65 | loopback-only. |
| 66 | |
| 67 | ## Deploy |
| 68 | |
| 69 | `sudo ovhost deploy watch`: checkout `/opt/openvibe.watch`, unit |
| 70 | `deploy/systemd/openvibe-watch.service` as `openvibe-watch.service` on `127.0.0.1:4730`, env |
| 71 | `/etc/openvibe/watch.env` (0600), database `ov_watch`, nginx |
| 72 | `deploy/nginx/openvibe.watch.conf`. Rollback with `sudo ovhost rollback watch --to <sha>`; readiness |
| 73 | reports `degraded` for an unloaded Network key or a check worker behind by 15 minutes. |
| 74 | |
| 75 | ## Commands and tests |
| 76 | |
| 77 | ```bash |
| 78 | npm install |
| 79 | npm run dev # http://127.0.0.1:4730 |
| 80 | npm test # every test/*.test.js against local stub sites |
| 81 | npm test -- carriers check # only matching files |
| 82 | npm run test:pg # the same through PgBouncer (CI path) |
| 83 | ``` |
| 84 | |
| 85 | `test/run.js` uses `openvibe-shared/test-runner` (180 s, serial): temp PGlite, generated RSA keys and |
| 86 | stub subscribers/sites, never the network. Tests cover the carriers and their preference, event and |
| 87 | webhook sources, conditional fetch/ETag, extraction, conditions and observations, actions, budgets, |
| 88 | account export/deletion, provider rates, the pages and discovery. |
| 89 | |
| 90 | ## Conventions and pitfalls |
| 91 | |
| 92 | - CommonJS, 4-space indent, single quotes, semicolons; problem+json errors; escaped server HTML. |
| 93 | - **Carrier preference is binding:** an event or ETag wins over polling; a Watch must not poll when the |
| 94 | source can push or answer 304. A new carrier is added to `server/carriers/index.js` and the README |
| 95 | table, not bolted onto the http path. |
| 96 | - **Watch asks, others run:** no browser/code execution or LAN probing in this repo — call Run/Link |
| 97 | with the person's authority. Expensive work needs a budget. |
| 98 | - **Every check records its observation state and backoff**; a failure is recorded, not retried in a |
| 99 | tight loop, and never invents a change. |
| 100 | - **Actions are explicit and owner-scoped:** a notification, event, webhook, Actor/Codes/Run task are |
| 101 | the person's; never let a watch trigger another person's process. |
| 102 | - **The public site is complete without JavaScript**: create/edit/pause/resume/delete/check-now are |
| 103 | forms; the query picks a template, not a script. |
| 104 | - **Provider rates are evidence, not an applied price:** while `WATCH_PROVIDER_RATES` is off, store |
| 105 | the official document, show the pending change and require the dated approval; AI's operator applies |
| 106 | it. |
| 107 |