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
Changes
@@@ -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.@@@ -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",@@@ -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",@@@ -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' };};}@@@ -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) {@@@ -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 },@@@ -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();@@@ -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'));@@@ -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;@@@ -75,7 +75,7 @@ const rel = (release) => [200, { service: 'x', release, released_at: '2026-09-22assert.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);
@@ -44,7 +44,7 @@ A new project is `sandbox` only. Staff switch it to `sandbox+production`, which
@@ -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 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 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 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 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.
- 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.
- `GET /api/v1/projects/catalog` lists what could ever be granted.
@@ -20,7 +20,7 @@
@@ -20,7 +20,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
"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-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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",
"uuid": "^11.1.0",
"uuid": "^11.1.0",
@@ -1465,9 +1465,9 @@
@@ -1465,9 +1465,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"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",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -23,7 +23,7 @@
@@ -23,7 +23,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
"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-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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",
"uuid": "^11.1.0",
"uuid": "^11.1.0",
@@ -50,7 +50,11 @@ function aiSiteCopy({ privateKey, issuer, aiUrl }) {
@@ -50,7 +50,11 @@ function aiSiteCopy({ privateKey, issuer, aiUrl }) {
const j = await r.json().catch(() => null);
const j = await r.json().catch(() => null);
const run = j && j.run;
const run = j && j.run;
if (!run || run.status !== 'succeeded' && run.status !== 'cached' || !run.output || !Array.isArray(run.output.sites)) return null;
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' };
};
};
}
}
@@ -39,10 +39,10 @@ const POLL_MS = 60 * 1000;
@@ -39,10 +39,10 @@ const POLL_MS = 60 * 1000;
// Readiness paths of services whose manifest (openvibe-contracts) does not carry `ready` yet. Each
// 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
// answers with openvibe-shared/ready's shape; until a service deploys it, its health path is used
// and the row says so (basis: 'health').
// 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). */
/** Row status from a readiness body (openvibe-shared/ready shape, or an older ad-hoc one). */
function statusFromReady(res, body) {
function statusFromReady(res, body) {
@@ -7,8 +7,10 @@
@@ -7,8 +7,10 @@
* the service is meant to answer. Neither says whether that public domain serves the service today:
* 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
* 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
* 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):
* Every manifest id has exactly one state here (test/registry-exposure.test.js pins them):
*
*
@@ -66,7 +68,7 @@ const EXPOSURE = {
@@ -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' },
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' },
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' },
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 },
realtime: { state: 'placeholder', public_site: null },
@@ -40,7 +40,7 @@ for (let i = 0; i < 6; i++) ins.run(1, 'tools', 'dns');
@@ -40,7 +40,7 @@ for (let i = 0; i < 6; i++) ins.run(1, 'tools', 'dns');
aiAuth = req.headers.authorization;
aiAuth = req.headers.authorization;
if (aiMode === 'down') { res.statusCode = 503; return res.end('{}'); }
if (aiMode === 'down') { res.statusCode = 503; return res.end('{}'); }
res.setHeader('content-type', 'application/json');
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))));
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' } });
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');
@@ -63,6 +63,7 @@ for (let i = 0; i < 6; i++) ins.run(1, 'tools', 'dns');
p = s2.payloadFor('openvibe.games');
p = s2.payloadFor('openvibe.games');
assert.equal(p.footer.ai, true);
assert.equal(p.footer.ai, true);
assert.equal(p.footer.blurb, 'Play Scraplandia and the shared pixel canvas in a tab.');
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';
aiMode = 'down';
await s2.refreshCopy();
await s2.refreshCopy();
@@ -33,7 +33,9 @@ const { createEcosystemRegistry } = require('../server/registry/ecosystem');
@@ -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.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));
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');
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');
r = await get('/api/v1/registry/services/network');
assert.ok(r.body.capability_details.some(c => c.id === 'network.coins.credit'));
assert.ok(r.body.capability_details.some(c => c.id === 'network.coins.credit'));
@@ -51,7 +51,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -51,7 +51,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
// 2. Registry and status over fake loopback services.
// 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 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 = {};
const overrides = {};
for (const id of Object.keys(exposure.EXPOSURE)) overrides[id] = up.url;
for (const id of Object.keys(exposure.EXPOSURE)) overrides[id] = up.url;
overrides.ai = ai.url;
overrides.ai = ai.url;
@@ -75,7 +75,7 @@ const rel = (release) => [200, { service: 'x', release, released_at: '2026-09-22
@@ -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.status, 'not-running');
assert.strictEqual(by.realtime.label, 'not running (placeholder)');
assert.strictEqual(by.realtime.label, 'not running (placeholder)');
assert.strictEqual(by.contracts.status, 'not-running');
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`);
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);
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);