openvibers/openvibe.network Commits

openvibe-contracts v0.28.0 -> v0.30.1: realtime is the only placeholder manifest left (AI, SDK, Shared and Examples are alpha), so the exposure overlay's comment says how each exists instead of correcting a placeholder label, the contracts library row reads v0.30.1, and AI is polled on its manifest's own /api/health and /api/ready (the READY_PATHS/HEALTH_PATHS stand-ins are gone). Footer site copy reads the answering model from run.provenance.model (ai.run@1; run.route is { key, version }, there is no run.model), and test/chrome.test.js answers with the contract's shape and checks the stored model

OP OpenVibers committed 23 Sept 2026 · 692c775157 · 10 files, +27 −18


Changes

ddiff --git a/docs/developer-projects.md b/docs/developer-projects.md
iindex d23649d4..4cc6b36c 100644
---- a/docs/developer-projects.md
++++ b/docs/developer-projects.md
@@@ -44,7 +44,7 @@ A new project is `sandbox` only. Staff switch it to `sandbox+production`, which
- The only grants that exist are capabilities in the openvibe-contracts catalog, at the version Network has installed.
- **The grantability rule.** The contracts `visibility` enum is `public | first-party | internal`. Only `active` capabilities with visibility `public` may be granted to apps. `first-party` and `internal` capabilities are never grantable: staff cannot add them to an allowance, members cannot request them, and token issuance filters them out again. The code also accepts a proposed `partner` visibility. Staff may put a `partner` capability in one project's allowance by hand, but it never comes from the default allowance. `partner` is not in the enum yet; see `docs/contracts-proposal/`.
- **The allowance** is the set of grantable capabilities one project's apps may hold. Only staff set it. `DEV_DEFAULT_ALLOWANCE` seeds new projects, with public capabilities only. It is empty by default, so staff decide each project's **production** apps.
--- **The sandbox allowance** is added to the allowance for **sandbox apps only**, in every project, without a staff decision. It comes from `DEV_SANDBOX_ALLOWANCE`. When that is unset, the code default is `media.object.upload`, `media.object.read`, `events.app.publish`, `events.app.read`, `events.app.subscribe`, `tools.job.create`, `tools.job.read` and `tools.job.cancel`. Only `public` + `active` capabilities of the installed openvibe-contracts catalog count. `partner`, `first-party`, `internal` and unknown ids are dropped. The three `events.app.*` ids are defined from openvibe-contracts v0.28.0, which Network pins; with an older release they would be left out. The project view and `GET /catalog` show it as `sandbox_allowance`. Production apps never use it.
++- **The sandbox allowance** is added to the allowance for **sandbox apps only**, in every project, without a staff decision. It comes from `DEV_SANDBOX_ALLOWANCE`. When that is unset, the code default is `media.object.upload`, `media.object.read`, `events.app.publish`, `events.app.read`, `events.app.subscribe`, `tools.job.create`, `tools.job.read` and `tools.job.cancel`. Only `public` + `active` capabilities of the installed openvibe-contracts catalog count. `partner`, `first-party`, `internal` and unknown ids are dropped. The three `events.app.*` ids are defined from openvibe-contracts v0.28.0 (Network pins v0.30.1); with an older release they would be left out. The project view and `GET /catalog` show it as `sandbox_allowance`. Production apps never use it.
- **A grant never exceeds the allowance** (for a sandbox app: allowance ∪ sandbox allowance). It is checked in three places. Approval refuses with `403 grant.beyond_allowance`. Shrinking the allowance revokes approved grants, and denies requested ones, that fall outside it. A sandbox app's grant that is still inside the sandbox allowance is kept. Token issuance intersects approved grants with the current allowance and with the current grantability.
- A developer's grant request waits in `requested`. When an owner or admin makes the request and the capability is inside the allowance, it is approved at once.
- `GET /api/v1/projects/catalog` lists what could ever be granted.
ddiff --git a/package-lock.json b/package-lock.json
iindex 72b545dd..50a6a8bc 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -20,7 +20,7 @@
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.28.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",
"uuid": "^11.1.0",
@@@ -1465,9 +1465,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.28.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.28.0",
-- "integrity": "sha512-cOOEmIhcjDmnWbo4jsz6kYOKT+Kc+aitYoc4w/EJOKg5uvJDt10vctjKB3IIl53UXeuDOhbckP53yBdBeewC1A==",
++ "version": "0.30.1",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.1",
++ "integrity": "sha512-JAOna8MA/+4iABEZ5qnzGjW27z9UgAHDF1Pj4VE+R3tb1UVyRUVZX06yuE+BggTdhZPGZRq5mFFIyPNHp8N3GA==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex ca201cf8..7c902267 100644
---- a/package.json
++++ b/package.json
@@@ -23,7 +23,7 @@
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.28.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",
"uuid": "^11.1.0",
ddiff --git a/server/chrome/service.js b/server/chrome/service.js
iindex abc481d8..a2e899db 100644
---- a/server/chrome/service.js
++++ b/server/chrome/service.js
@@@ -50,7 +50,11 @@ function aiSiteCopy({ privateKey, issuer, aiUrl }) {
const j = await r.json().catch(() => null);
const run = j && j.run;
if (!run || run.status !== 'succeeded' && run.status !== 'cached' || !run.output || !Array.isArray(run.output.sites)) return null;
-- return { sites: run.output.sites, model: (run.model && (run.model.key || run.model.id)) || (run.route && run.route.key) || 'openvibe.ai' };
++ // ai.run@1: the model that answered is provenance.model (null for a cached or unresolved run);
++ // run.route is the provider route { key, version }. There is no top-level run.model.
++ const prov = run.provenance && typeof run.provenance === 'object' ? run.provenance : {};
++ const route = run.route && typeof run.route === 'object' ? run.route.key : prov.route;
++ return { sites: run.output.sites, model: prov.model || route || 'openvibe.ai' };
};
}
ddiff --git a/server/registry/ecosystem.js b/server/registry/ecosystem.js
iindex 2583296b..2b744466 100644
---- a/server/registry/ecosystem.js
++++ b/server/registry/ecosystem.js
@@@ -39,10 +39,10 @@ const POLL_MS = 60 * 1000;
// Readiness paths of services whose manifest (openvibe-contracts) does not carry `ready` yet. Each
// answers with openvibe-shared/ready's shape; until a service deploys it, its health path is used
// and the row says so (basis: 'health').
--const READY_PATHS = { network: '/api/ready', media: '/api/ready', tools: '/api/ready', ai: '/api/ready' };
--// Liveness paths of services whose manifest in the pinned openvibe-contracts has none (AI is a
--// placeholder there but runs on loopback; see ./exposure.js).
--const HEALTH_PATHS = { ai: '/api/health' };
++const READY_PATHS = { network: '/api/ready', media: '/api/ready', tools: '/api/ready' };
++// Liveness paths of services whose manifest in the pinned openvibe-contracts has none (none since
++// 0.30, where AI's manifest carries /api/health and /api/ready).
++const HEALTH_PATHS = {};
/** Row status from a readiness body (openvibe-shared/ready shape, or an older ad-hoc one). */
function statusFromReady(res, body) {
ddiff --git a/server/registry/exposure.js b/server/registry/exposure.js
iindex 4f8b6de4..8e4fe811 100644
---- a/server/registry/exposure.js
++++ b/server/registry/exposure.js
@@@ -7,8 +7,10 @@
* the service is meant to answer. Neither says whether that public domain serves the service today:
* on 2026-09-23 openvibe.news, .reviews, .deals, .coupons, .trade, .host, .tips, .vip and openre.stream
* served the OpenVibe.Sites "coming soon" placeholder, search. and sources.openvibe.network served the
-- * admin placeholder, while each of those services ran on this host's loopback only. And AI, SDK,
-- * Shared and Examples existed although their manifests (openvibe-contracts 0.28.0) said placeholder.
++ * admin placeholder, while each of those services ran on this host's loopback only. (Until
++ * openvibe-contracts 0.30 the AI, SDK, Shared and Examples manifests said placeholder although all four
++ * existed; since 0.30 they are alpha, and this overlay says how each one exists: AI runs on loopback,
++ * SDK and Shared are released libraries, Examples is a repository.)
*
* Every manifest id has exactly one state here (test/registry-exposure.test.js pins them):
*
@@@ -66,7 +68,7 @@ const EXPOSURE = {
sdk: { state: 'library', public_site: null, release: 'v0.4.0', distribution: 'https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0' },
shared: { state: 'library', public_site: null, release: 'v1.3.0', distribution: 'https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0', note: 'also served at /shared/* by each site' },
-- contracts: { state: 'library', public_site: null, release: 'v0.29.0', distribution: 'https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.29.0', note: 'schemas are also served by this registry at their $id URLs' },
++ contracts: { state: 'library', public_site: null, release: 'v0.30.1', distribution: 'https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.30.1', note: 'schemas are also served by this registry at their $id URLs' },
examples: { state: 'repository', public_site: null, note: 'example apps with CI in OpenVibers/OpenVibe.Examples; not published as a package' },
realtime: { state: 'placeholder', public_site: null },
ddiff --git a/test/chrome.test.js b/test/chrome.test.js
iindex 7283460d..696e26ab 100644
---- a/test/chrome.test.js
++++ b/test/chrome.test.js
@@@ -40,7 +40,7 @@ for (let i = 0; i < 6; i++) ins.run(1, 'tools', 'dns');
aiAuth = req.headers.authorization;
if (aiMode === 'down') { res.statusCode = 503; return res.end('{}'); }
res.setHeader('content-type', 'application/json');
-- res.end(JSON.stringify({ run: { status: 'succeeded', model: { key: 'test-model' }, output: { sites: [{ id: 'games', blurb: 'Play Scraplandia and the shared pixel canvas in a tab.', picks: ['site:live'] }] } } }));
++ res.end(JSON.stringify({ run: { status: 'succeeded', route: { key: 'site-copy', version: 1 }, provenance: { origin: 'ai', workflow: 'network.site_copy', model: 'test-model', route: 'site-copy' }, output: { sites: [{ id: 'games', blurb: 'Play Scraplandia and the shared pixel canvas in a tab.', picks: ['site:live'] }] } } }));
});
await Promise.all([live, ai].map(s => new Promise(r => s.listen(0, '127.0.0.1', r))));
const { privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, publicKeyEncoding: { type: 'spki', format: 'pem' } });
@@@ -63,6 +63,7 @@ for (let i = 0; i < 6; i++) ins.run(1, 'tools', 'dns');
p = s2.payloadFor('openvibe.games');
assert.equal(p.footer.ai, true);
assert.equal(p.footer.blurb, 'Play Scraplandia and the shared pixel canvas in a tab.');
++ assert.equal(JSON.parse(db2.prepare("SELECT value FROM chrome_cache WHERE key = 'copy'").get().value).model, 'test-model', 'the model is read from run.provenance.model (ai.run@1)');
aiMode = 'down';
await s2.refreshCopy();
ddiff --git a/test/registry-ecosystem.test.js b/test/registry-ecosystem.test.js
iindex b7e2e2ab..dd7bb331 100644
---- a/test/registry-ecosystem.test.js
++++ b/test/registry-ecosystem.test.js
@@@ -33,7 +33,9 @@ const { createEcosystemRegistry } = require('../server/registry/ecosystem');
assert.strictEqual(byId.realtime.runtime.status, 'not-running', 'placeholders are never shown as running');
assert.ok(r.body.services.every(s => contracts.validate('registry.service-manifest@1', Object.fromEntries(Object.entries(s).filter(([k]) => k !== 'runtime' && k !== 'exposure'))).valid));
r = await get('/api/v1/registry/services?status=placeholder');
-- assert.ok(r.body.services.length >= 2 && r.body.services.every(s => s.status === 'placeholder'));
++ // openvibe-contracts 0.30: realtime is the only placeholder manifest left (AI, SDK, Shared, Examples are alpha).
++ assert.ok(r.body.services.length >= 1 && r.body.services.every(s => s.status === 'placeholder'));
++ assert.ok(r.body.services.some(s => s.id === 'realtime'));
r = await get('/api/v1/registry/services/network');
assert.ok(r.body.capability_details.some(c => c.id === 'network.coins.credit'));
ddiff --git a/test/registry-exposure.test.js b/test/registry-exposure.test.js
iindex df3e5cb1..2848d852 100644
---- a/test/registry-exposure.test.js
++++ b/test/registry-exposure.test.js
@@@ -51,7 +51,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
// 2. Registry and status over fake loopback services.
const up = await fakeService({ '/api/ready': ready, '/ready': ready, '/api/health': [200, { status: 'ok' }], '/health': [200, {}], '/healthz': [200, {}] });
-- const ai = await fakeService({ '/api/ready': ready, '/api/health': [200, { status: 'ok' }] }); // AI's manifest (contracts 0.28.0) has no paths
++ const ai = await fakeService({ '/api/ready': ready, '/api/health': [200, { status: 'ok' }] }); // AI answers its manifest's /api/ready (contracts 0.30)
const overrides = {};
for (const id of Object.keys(exposure.EXPOSURE)) overrides[id] = up.url;
overrides.ai = ai.url;
ddiff --git a/test/status.test.js b/test/status.test.js
iindex d8b58a2f..b16182b8 100644
---- a/test/status.test.js
++++ b/test/status.test.js
@@@ -75,7 +75,7 @@ const rel = (release) => [200, { service: 'x', release, released_at: '2026-09-22
assert.strictEqual(by.realtime.status, 'not-running');
assert.strictEqual(by.realtime.label, 'not running (placeholder)');
assert.strictEqual(by.contracts.status, 'not-running');
-- assert.strictEqual(by.contracts.label, 'not running (library, released v0.29.0)');
++ assert.strictEqual(by.contracts.label, 'not running (library, released v0.30.1)');
for (const s of r.body.services) assert.ok(s.checked_at, `${s.id} says when it was checked`);
assert.strictEqual(r.body.summary.up + r.body.summary.degraded + r.body.summary.down + r.body.summary['not-running'] + r.body.summary.unknown, contracts.services.manifests.length);