# AGENTS.md — OpenVibe.Vision ## Project overview OpenVibe.Vision (`https://openvibe.vision`) answers questions about pictures and pages: ask/describe/ identify/count/translate, OCR with a local Tesseract first, side-by-side model comparison, UI checks (HTML, public URL or screenshot), a consented face library, and the network's read-only MCP tools. - **Entry:** `server/index.js`; app `server/app.js`; port `5130`, service id `vision`. - **API/pages:** `server/http/` — `api.js` (`/api/v1`), `faces-api.js`, `mcp.js`, `principal.js`, `pages.js`, `discovery.js`; views `server/views/index.js`, `people.js`, `review.js`, `tools.js`; rendering `server/render/`; own browser script `public/js/upload-picker.js`, CSS/img in `public/`. - **Engine:** `server/vision/` — `service.js`, `images.js` (normalise/limits), `ocr.js` (Tesseract 5, `OMP_THREAD_LIMIT=1`), `ui-check.js` (static parser + WCAG maths + screenshot OCR/sharp), `ai.js` (`ai.run.create`), `network-tools.js`, `review.js`, `media-view.js`, `local-models.js`. - **Faces:** `server/vision/faces.js`, `faces-model.js` (YuNet + SFace + ONNX Runtime), `face-libraries.js`, `consent-registry.js`; consent and encrypted vectors in the `face_*` tables. - **Events:** `server/events/` (outbox, publisher) and `server/identity/account-data.js`. - **Docs:** `README.md` ("Faces with consent", "Unsafe-image review", "MCP and events"), `SECURITY.md`. ## Owns, does not own Owns its tables: `vision_answers` (`vis_`; task, question, source, image sha256/mime/bytes, cleaned text, answer jsonb, model/provider/route, cost, latency), `vision_compare_samples` (`vcm_`), `vision_ui_checks` (`vuc_`), `account_data_events`, `telemetry_samples`, and the face tables — `face_consents`, `face_templates`, `face_media`, `face_tags`, `face_clusters` — plus `event_outbox`. General answer tables store no image bytes, token or provider key; opt-in face media and vectors are AES-GCM encrypted. Does not own: models, routing, quotas and provider keys (AI — Vision makes first-party `ai.run.create` runs and AI decides/reports the model and provenance); identity/roles (Network); the local OCR binary and language data (the host: Tesseract under `/usr/bin`, `/usr/share`); Search (nothing public to index); Events/Host/database. **Vision holds no provider key and calls no paid provider directly.** ## Who calls it, what it calls People use the pages/API (session cookie or Network user Bearer; a session write must come from `https://openvibe.vision`, else `403 vision.cross_site`); services use a token with the route's capability (`vision.image.ask`, `vision.text.read`, `vision.model.compare`, `vision.ui.check`, `vision.face.cast.match`, `vision.alt.write`). Typical callers: Design's Arena for UI checks, Live for consented cast matching, Pics/Media/Chat for alt text. SDK `openvibe-sdk/vision` is the client surface and `/mcp` exposes the read-only tools. Vision calls Network (JWKS) and AI (`ai.run.create`); the only egress is AI plus an explicitly supplied public URL for a UI check, through the guard. ## Database and migrations PostgreSQL through `openvibe-sdk/db` (PgBouncer for the app, owner role for migrations): `0001` answers/compare/UI checks, `0003` the face tables, `0004` the event outbox; `0002`/later add the rest. Applied at boot; never edit a migration that has run. `face_templates`, `face_media` and `face_clusters` hold ciphertext, never a plaintext vector; `face_tags` disappear when a cast revocation lands. Account export/deletion covers every table with a person column; a redelivery changes nothing. ## Consent and privacy Faces are opt-in and **purpose- and scope-scoped**: a `face_consents` row names the feature (`verify|find|cast|library`), the channel/scope, the signed text version/name and an optional `purpose_ends_at`; revoked consent stops detection/matching/tagging. Identifying matches require separate consent; faces are detected, blurred and matched locally — no face image or vector goes to AI. An unsafe-image review exists (`server/vision/review.js`); do not bypass it for a faster answer. `/internal/events` is signed and loopback-only. ## Deploy `deploy/systemd/openvibe-vision.service` (`127.0.0.1:5130`, env `/etc/openvibe/vision.env`) and `deploy/nginx/openvibe.vision.conf` (TLS, www → apex, `/auth/me` on the API zone, a forms zone for `/ask`, `/ocr`, `/compare`, `/ui-check`, `/internal` and `/metrics` 404). `sudo ovhost deploy vision` is the production path; rollback with `sudo ovhost rollback vision --to `. ## Commands and tests ```bash npm ci npm run dev # http://127.0.0.1:5130 npm test # every test/*.test.js, PGlite + mock Network/AI npm test -- ocr ui # only matching files npm run test:pg # the same on PostgreSQL npm run proof # the v1 proof run npm run models:faces # download the YuNet/SFace models on the host npm run models:fetch # fetch the local model set ``` `test/run.js` uses `openvibe-shared/test-runner` (90 s, serial); temp PGlite, a mock Network and AI and local image/page fixtures, never the network. Coverage includes OCR/local-first and fallback, compare/benchmarks, UI checks, faces/consent, libraries, network tools/MCP, unsafe-image review, events/outbox, account data and the pages. Some model/OCR work skips without the binaries — a skipped file is never counted as passed. ## Conventions and pitfalls - CommonJS, 4-space indent, single quotes, semicolons; problem+json with a stable code; escaped HTML. - **Local first, AI second, never a direct provider:** Tesseract answers a read for free; AI's routed model is the only paid path and `requested_model` is a hint, not a promise. Report the model/provider/ route that AI says answered, with cost and latency. - **No image bytes in the answer tables**, and no token/provider key anywhere in the repo. The normalised image is what goes to AI, not the original. - **Consent is checked per feature and scope, on every use**, and revoked/expired means no match; an identifying match needs its own purpose. Never use a face vector outside the encrypted store. - **The unsafe-image review is a gate**, not a suggestion; a route that returns image text or alt text must run it. - **A UI check of a URL only fetches a public HTTP(S) address** through the guard; never loopback, private ranges or a redirect that lands there. - **Alt text is written as text**, never markup; a caller's content is untrusted.