openvibers/openvibe.ai Commits

Refresh AI service manifest and bump openvibe-contracts to v0.86.0 (#12)

OP OpenVibers committed 3 Oct 2026 · af2c49326b · 9 files, +131 −18

checks run by GitHub CI (test, test-pg) · CI ✓ 3/3 · review SHIP by DeepSeek harness (QUALITY=high)

https://github.com/OpenVibers/OpenVibe.AI/pull/12

Changes

ddiff --git a/.env.example b/.env.example
iindex 59e8330..6fa84a9 100644
---- a/.env.example
++++ b/.env.example
@@@ -1,5 +1,6 @@
# ═══════════════════════════════════════════════════════════════
# ai.openvibe.network — OpenVibe.AI Environment Configuration
++# Target host: ai.openvibe.services (the public home; BASE_URL still defaults to ai.openvibe.network here).
# Copy to .env and fill in values. In production this lives at /etc/openvibe/ai.env (0600).
# Secrets are read by NAME only (providers store 'env:NAME' references); nothing logs or returns them.
# ═══════════════════════════════════════════════════════════════
@@@ -32,6 +33,7 @@ OV_NETWORK_INTERNAL_URL=http://127.0.0.1:4000
# ── Operator console (/console; README "Operator console") ──
# Network SSO as OAuth client `ai` (the client AI's service tokens use). The Network must list
# ${BASE_URL}/auth/callback (https://ai.openvibe.network/auth/callback) as a redirect URI of client `ai`.
++# Target host: also list https://ai.openvibe.services/auth/callback when BASE_URL moves there.
# OV_OAUTH_CLIENT_SECRET is also what the events relay and the user modules authenticate with.
#OV_OAUTH_CLIENT_ID=ai
#OV_OAUTH_CLIENT_SECRET=
ddiff --git a/README.md b/README.md
iindex 8eed68e..d3652e4 100644
---- a/README.md
++++ b/README.md
@@@ -6,7 +6,7 @@
(loopback only) and in production use: OpenVibe.Live runs with `AI_SERVICE=remote` and Network's
footer copy runs `network.site_copy` here. It is not a public product: the domain keeps its placeholder
page until the launch rule below is met.
--**Domain:** `ai.openvibe.network` · **Port:** 4700 · **Unit:** `openvibe-ai.service`
++**Domain:** `ai.openvibe.network` (target host `ai.openvibe.services`) · **Port:** 4700 · **Unit:** `openvibe-ai.service`
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §12.1, §12.13, §15.14, §33, §34.
**License:** AGPL-3.0 (same as every OpenVibe service).
@@@ -43,7 +43,7 @@ owns publication truth: Wiki, Blog, News, Live, … decide what to publish.
- OpenVibe.Media (recordings `media.analyze` reads, over allow-listed https URLs or signed URLs)
- provider APIs configured by the operator (OpenAI-compatible, Anthropic, the HTTP seam), whisper.cpp
and the local model server `openvibe-llm.service` on this host
--- `openvibe-contracts` v0.84.0, `openvibe-sdk` v0.26.0 (service tokens, events outbox),
++- `openvibe-contracts` v0.86.0, `openvibe-sdk` v0.26.0 (service tokens, events outbox),
`openvibe-shared` v2.3.1, pinned by release tarball
## Capabilities
ddiff --git a/STATUS.json b/STATUS.json
iindex f00fb53..f71a760 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -7,7 +7,7 @@
"runtime": "node 22, express, postgresql",
"port": 4700,
"domain": "ai.openvibe.network still serves the OpenVibe.Sites placeholder; the service runs on openvibe-ovh (/opt/openvibe.ai, unit openvibe-ai) on 127.0.0.1:4700, loopback only, used by OpenVibe.Live and OpenVibe.Network",
-- "contracts": "openvibe-contracts v0.84.0",
++ "contracts": "openvibe-contracts v0.86.0",
"sdk": "openvibe-sdk v0.26.0",
"capabilities": [
"ai.run.create",
ddiff --git a/docs/capabilities-proposal/ai.credential.manage.json b/docs/capabilities-proposal/ai.credential.manage.json
nnew file mode 100644
iindex 0000000..fb04125
---- /dev/null
++++ b/docs/capabilities-proposal/ai.credential.manage.json
@@@ -0,0 +1,23 @@
++{
++ "id": "ai.credential.manage",
++ "version": "1.0",
++ "owner": "ai",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Store, read (never the key) or delete a person's own provider key, for runs made on their behalf with it (ai.run-request credential). Only the service that stored a credential reads, uses or changes it.",
++ "permissions": [
++ "ai:credentials"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "ai-credentials",
++ "events": [],
++ "implementedBy": [
++ "PUT /api/v1/credentials/:subject",
++ "GET /api/v1/credentials/:subject",
++ "DELETE /api/v1/credentials/:subject"
++ ],
++ "inputSchema": "ai.credential-put@1",
++ "outputSchema": "ai.credential@1"
++}
ddiff --git a/docs/capabilities-proposal/ai.quota.attribution.manage.json b/docs/capabilities-proposal/ai.quota.attribution.manage.json
nnew file mode 100644
iindex 0000000..1000310
---- /dev/null
++++ b/docs/capabilities-proposal/ai.quota.attribution.manage.json
@@@ -0,0 +1,23 @@
++{
++ "id": "ai.quota.attribution.manage",
++ "version": "1.0",
++ "owner": "ai",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Cap what runs attributed to one of the caller's own entities may cost or request per hour or day (a streamer's daily AI budget on Live). Only the service named in the attribution sets, reads or removes its caps; a run over a cap is refused before any provider is called.",
++ "permissions": [
++ "ai:quotas"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "ai-quotas",
++ "events": [],
++ "implementedBy": [
++ "PUT /api/v1/attribution-quotas/:attribution",
++ "GET /api/v1/attribution-quotas/:attribution",
++ "DELETE /api/v1/attribution-quotas/:attribution"
++ ],
++ "inputSchema": "ai.attribution-quota-put@1",
++ "outputSchema": "ai.attribution-quota@1"
++}
ddiff --git a/docs/capabilities-proposal/service-manifest.ai.json b/docs/capabilities-proposal/service-manifest.ai.json
iindex 8023402..9f4e3f6 100644
---- a/docs/capabilities-proposal/service-manifest.ai.json
++++ b/docs/capabilities-proposal/service-manifest.ai.json
@@@ -1,5 +1,5 @@
{
-- "version": "0.1.0",
++ "version": "0.2.0",
"contractRanges": {
"openvibe-contracts": ">=0.6.0 <1.0.0"
},
@@@ -13,15 +13,77 @@
"publicOrigin": "https://ai.openvibe.network",
"health": "/api/health",
"ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:4700",
++ "exposure": {
++ "state": "internal",
++ "publicSite": null,
++ "note": "called by other services at 127.0.0.1:4700; ai.openvibe.network serves the OpenVibe.Sites placeholder page"
++ },
"capabilities": [
++ "ai.credential.manage",
++ "ai.provider.manage",
++ "ai.quota.attribution.manage",
"ai.run.create",
"ai.run.read",
-- "ai.workflow.manage",
-- "ai.provider.manage",
-- "ai.usage.read"
++ "ai.usage.read",
++ "ai.workflow.manage"
++ ],
++ "eventsProduced": [
++ "ai.run.queued",
++ "ai.run.succeeded",
++ "ai.run.failed",
++ "ai.run.cached"
],
-- "eventsProduced": [],
"eventsConsumed": [],
-- "namespacesOwned": [],
-- "notes": "Providers, models, routing profiles, versioned prompt templates and workflows, runs, request logs, citations, scoped cache, quotas and audit. Workflow output is a draft/evidence package attributed to workflow + model + run. ai.run.queued|succeeded|failed|cached events follow once the Events client is wired in."
++ "namespacesOwned": [
++ "ai.usage_summary"
++ ],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP; it checks nothing else (the database and dependencies are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 10,
++ "drains": [
++ "housekeeping timer, user-module sync and Network key refresh stopped",
++ "open HTTP connections closed (closeAllConnections), then the server",
++ "runs in progress awaited (runs.drain)",
++ "ai.run.* outbox relay stopped after the runs (its send in progress awaited; unsent rows stay for the next start)",
++ "whisper child processes killed",
++ "database closed"
++ ],
++ "note": "the forced exit after 10 s exits 1"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "jobs",
++ "what": "runs left queued or running are marked failed with error_code run.interrupted (announced as ai.run.failed); the caller retries them"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent ai.run.* events in the outbox (server/events.js) go to OpenVibe.Events once the relay starts"
++ }
++ ]
++ },
++ "rollback": {
++ "conditions": [
++ "automatic: ovhost deploy puts the previous sha back and restarts again when the ready URL does not answer 2xx within ready.timeoutSeconds after the restart (exit 3)",
++ "manual: ovhost rollback <service> --to <sha>"
++ ],
++ "window": "automatic only while ovhost waits for readiness after the restart (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.6.0 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process owns ai.db (better-sqlite3, single writer); a run is taken with a compare-and-set on its status (queued → running)"
++ }
++ },
++ "notes": "Providers, models, routing profiles, versioned prompt templates and workflows, runs, request logs, citations, scoped cache, quotas and audit. Workflow output is a draft/evidence package attributed to workflow + model + run. ai.run.queued|succeeded|failed|cached have payload contracts (v0.30.0, status planned): AI publishes them once its Events client is wired in; it publishes none today. ai.run.create takes ai.run-request@1 and returns ai.run@1."
}
ddiff --git a/package-lock.json b/package-lock.json
iindex 3debf21..3dc9d93 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -12,7 +12,7 @@
"ajv": "^8.17.1",
"dotenv": "^16.4.7",
"express": "^4.21.2",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.84.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.5.0",
"pg": "^8.23.0"
@@@ -1448,9 +1448,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.84.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.84.0",
-- "integrity": "sha512-wnmQbzffd2xXSGk3Zr1JxZYIL4oSlnJTo8lGpUApBPoDKeQ3U4VCLJgt3cZpbtkbl5NH1qNjBDXkwx+uASwoyA==",
++ "version": "0.86.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",
++ "integrity": "sha512-3o3AILEBtQ2pK0KqK7GEAlN3HJjaKUl6vrbzEDiBbhK6Nr2hivgN/5MAmJ7WRkgaDkxNsZETs/fLZC2Q4oNCiA==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 3777fb8..c7e264c 100644
---- a/package.json
++++ b/package.json
@@@ -24,7 +24,7 @@
"ajv": "^8.17.1",
"dotenv": "^16.4.7",
"express": "^4.21.2",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.84.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.5.0",
"pg": "^8.23.0"
ddiff --git a/server/auth.js b/server/auth.js
iindex 8ad4e0e..ca5ebcd 100644
---- a/server/auth.js
++++ b/server/auth.js
@@@ -8,9 +8,10 @@
* PEM, which skips the fetch entirely.
*
* Capabilities ai.run.create / ai.run.read / ai.workflow.manage / ai.provider.manage / ai.usage.read
-- * are proposed in docs/capabilities-proposal/ and are not in openvibe-contracts yet. Until they
-- * ship, allows() decides with the contracts' own grant rule (the exact id, or a `family.*` grant);
-- * once contracts knows an id, contracts decides.
++ * are proposed in docs/capabilities-proposal/ and are not in openvibe-contracts yet; ai.credential.manage
++ * and ai.quota.attribution.manage ship in contracts (0.73.0 / 0.75.0). Until the five do, allows()
++ * decides with the contracts' own grant rule (the exact id, or a `family.*` grant); once contracts
++ * knows an id, contracts decides.
*
* Namespaces fail closed: a token may only run workflows inside the namespaces its `ns` claim holds
* (the same matching as contracts' namespaceAllowed: 'live.*' allows 'live.translate'). A token
@@@ -27,6 +28,8 @@ const CAPS = Object.freeze({
workflowManage: 'ai.workflow.manage',
providerManage: 'ai.provider.manage',
usageRead: 'ai.usage.read',
++ credentialManage: 'ai.credential.manage',
++ quotaAttributionManage: 'ai.quota.attribution.manage',
});
/**