openvibers/openvibe.network Commits
agent: Host owns its Media tenant again, and Blog can read the shots (plan L9) (#573)
Changes
@@@ -122,6 +122,10 @@ const DEFAULT_GRANTS = [// OpenVibe.Video's AI edit (MediaHub server/video/ai-edit.js): media.analyze on finished uploads, local and unpaid;// OpenVibe.Pics alt text (server/pics/describe.js): pics.describe_image on new pictures.['media-hub', 'ai.run.create', 'openvibe.ai', ['media.analyze', 'pics.describe_image']],++ // Plan L9 (2026-11-06): the screenshot pass. ovhost shots captures each public page at two viewports and++ // stores the PNG in OpenVibe.Media, in Host's own tenant (the "host" namespace): Host uploads, lists and++ // prunes its shots, and Blog reads them to put a before/after in the patch notes.++ ...['media.object.upload', 'media.object.read', 'media.object.list', 'media.object.delete'].map(cap => ['host', cap, 'openvibe.media', ['host']]),// Plan T3: Chat owns the six chat tables. Emote images go to Media under the chat namespace, and// the chat-AI summaries run in Chat with its own token.['chat', 'media.object.upload', 'openvibe.media', ['chat']],@@@ -221,7 +225,7 @@ const DEFAULT_GRANTS = [['blog', 'events.event.publish', 'openvibe.events', []],['blog', 'community.comment.write', 'openvibe.community', []],['blog', 'community.comment.moderate', 'openvibe.community', []],-- ['blog', 'media.object.read', 'openvibe.media', ['blog']],++ ['blog', 'media.object.read', 'openvibe.media', ['blog', 'host']],// Wave 10: VIP sells plans through Billing and projects its entitlements from Billing events.['vip', 'billing.intent.create', 'openvibe.billing', []],['vip', 'billing.subscription.manage', 'openvibe.billing', []],@@@ -775,12 +779,8 @@ const REVOKED_GRANTS = [['space', 'vip.resource.policy.evaluate', 'openvibe.vip'],['space', 'events.event.read', 'openvibe.events'],['space', 'network.blocks.read', SELF_AUDIENCE],-- // The static hosting engine left Host (T20, Host#84, contracts 0.150.0): its Media objects and Services' fan-out to-- // its resource index go. Space serves the engine.-- ['host', 'media.object.upload', 'openvibe.media'],-- ['host', 'media.object.read', 'openvibe.media'],-- ['host', 'media.object.list', 'openvibe.media'],-- ['host', 'media.object.delete', 'openvibe.media'],++ // Plan L9 (2026-11-06): Host's Media grants come back for the screenshot pass (they went with the static++ // hosting engine in T20). They are in RESTORED_GRANTS below, scoped to the "host" namespace.['services', 'host.resource.read', 'openvibe.host'],];@@@ -800,6 +800,12 @@ const RESTORED_GRANTS = [['space', 'network.account.deletion.confirm', SELF_AUDIENCE],// Codes publishes codes.task.* (contracts 0.232.0); revoked when the developer portal left it (0.113.0).['codes', 'events.event.publish', 'openvibe.events'],++ // Plan L9 (2026-11-06): Host's Media grants are back (they went when the static hosting engine left Host in++ // T20): ovhost shots stores the screenshots it captures in OpenVibe.Media, in Host's own tenant.++ ['host', 'media.object.upload', 'openvibe.media'],++ ['host', 'media.object.read', 'openvibe.media'],++ ['host', 'media.object.list', 'openvibe.media'],++ ['host', 'media.object.delete', 'openvibe.media'],];const CHANGED_DEFAULT_NAMESPACES = [@@@ -823,6 +829,13 @@ const CHANGED_DEFAULT_NAMESPACES = [['media-hub', 'ai.run.create', 'openvibe.ai', ['media.analyze'], ['media.analyze', 'pics.describe_image']],// Search's embedder runs AI workflow ai.embed (contracts 0.234.0; POST /api/v1/embed for hybrid ranking).['search', 'ai.run.create', 'openvibe.ai', ['research.*'], ['research.*', 'ai.embed']],++ // Plan L9 (2026-11-06): Host's Media rows move from the old host/host.* list to the host namespace alone, and++ // Blog's Media read reaches Host's tenant too, so the patch notes can read the shots Host captured.++ ['host', 'media.object.upload', 'openvibe.media', ['host', 'host.*'], ['host']],++ ['host', 'media.object.read', 'openvibe.media', ['host', 'host.*'], ['host']],++ ['host', 'media.object.list', 'openvibe.media', ['host', 'host.*'], ['host']],++ ['host', 'media.object.delete', 'openvibe.media', ['host', 'host.*'], ['host']],++ ['blog', 'media.object.read', 'openvibe.media', ['blog'], ['blog', 'host']],// Design's frames with a preference run ai.chat (Design #57, 10-31). Network #533 added it to the default; the// production row, seeded before, kept ['ai.generate'] (the seed never changes an existing row), so frames still// failed until this moved it.nnew file mode 100644@@@ -0,0 +1,43 @@++'use strict';++// Plan L9 (2026-11-06): the screenshot pass. Host stores the shots ovhost captures in OpenVibe.Media, in its own++// tenant, and Blog reads them to put a before/after in the patch notes. Both are default grants.++const assert = require('assert');++const { getDb } = require('../server/db/database');++const principals = require('../server/identity/principals');++++const HOST_CAPS = ['media.object.upload', 'media.object.read', 'media.object.list', 'media.object.delete'];++++(async () => {++ const db = await getDb();++ await principals.ensureSchema(db);++++ for (const cap of HOST_CAPS) {++ const def = principals.DEFAULT_GRANTS.find((g) => g[0] === 'host' && g[1] === cap && g[2] === 'openvibe.media');++ assert.ok(def, `host's ${cap} grant on openvibe.media is a default grant`);++ assert.deepStrictEqual(def[3], ['host'], `host's ${cap} grant is on the host namespace alone`);++ assert.ok(!principals.REVOKED_GRANTS.some((g) => g[0] === 'host' && g[1] === cap && g[2] === 'openvibe.media'),++ `host's ${cap} grant is not revoked any more`);++ assert.ok(principals.RESTORED_GRANTS.some((g) => g[0] === 'host' && g[1] === cap && g[2] === 'openvibe.media'),++ `host's ${cap} grant comes back at boot (it is named in RESTORED_GRANTS)`);++ }++++ // The rows are live after boot: not revoked, on the host namespace.++ for (const cap of HOST_CAPS) {++ const row = await db.prepare('SELECT revoked_at, namespaces FROM principal_grants WHERE client_id = ? AND capability = ? AND audience = ?')++ .get('host', cap, 'openvibe.media');++ assert.ok(row, `the ${cap} row exists`);++ assert.strictEqual(row.revoked_at, null, `${cap} is not revoked`);++ assert.deepStrictEqual(JSON.parse(row.namespaces || '[]'), ['host'], `${cap} is on the host namespace`);++ }++++ // Blog reads its own tenant and Host's, so the patch notes can read the shots.++ const blog = await db.prepare('SELECT revoked_at, namespaces FROM principal_grants WHERE client_id = ? AND capability = ? AND audience = ?')++ .get('blog', 'media.object.read', 'openvibe.media');++ assert.ok(blog, "blog's Media read grant exists");++ const ns = JSON.parse(blog.namespaces || '[]');++ assert.ok(ns.includes('blog') && ns.includes('host'), `blog reads its own tenant and Host's: ${JSON.stringify(ns)}`);++ assert.strictEqual(blog.revoked_at, null, "blog's Media read grant is live");++++ console.log('l9 shots grants: Host owns its Media tenant again; Blog can read the shots');++ process.exit(0);++})().catch((err) => { console.error(err); process.exit(1); });
@@ -122,6 +122,10 @@ const DEFAULT_GRANTS = [
@@ -122,6 +122,10 @@ const DEFAULT_GRANTS = [
// OpenVibe.Video's AI edit (MediaHub server/video/ai-edit.js): media.analyze on finished uploads, local and unpaid;
// OpenVibe.Video's AI edit (MediaHub server/video/ai-edit.js): media.analyze on finished uploads, local and unpaid;
// OpenVibe.Pics alt text (server/pics/describe.js): pics.describe_image on new pictures.
// OpenVibe.Pics alt text (server/pics/describe.js): pics.describe_image on new pictures.
['media-hub', 'ai.run.create', 'openvibe.ai', ['media.analyze', 'pics.describe_image']],
['media-hub', 'ai.run.create', 'openvibe.ai', ['media.analyze', 'pics.describe_image']],
+ // Plan L9 (2026-11-06): the screenshot pass. ovhost shots captures each public page at two viewports and
+ // stores the PNG in OpenVibe.Media, in Host's own tenant (the "host" namespace): Host uploads, lists and
+ // prunes its shots, and Blog reads them to put a before/after in the patch notes.
+ ...['media.object.upload', 'media.object.read', 'media.object.list', 'media.object.delete'].map(cap => ['host', cap, 'openvibe.media', ['host']]),
// Plan T3: Chat owns the six chat tables. Emote images go to Media under the chat namespace, and
// Plan T3: Chat owns the six chat tables. Emote images go to Media under the chat namespace, and
// the chat-AI summaries run in Chat with its own token.
// the chat-AI summaries run in Chat with its own token.
['chat', 'media.object.upload', 'openvibe.media', ['chat']],
['chat', 'media.object.upload', 'openvibe.media', ['chat']],
@@ -221,7 +225,7 @@ const DEFAULT_GRANTS = [
@@ -221,7 +225,7 @@ const DEFAULT_GRANTS = [
['blog', 'events.event.publish', 'openvibe.events', []],
['blog', 'events.event.publish', 'openvibe.events', []],
['blog', 'community.comment.write', 'openvibe.community', []],
['blog', 'community.comment.write', 'openvibe.community', []],
['blog', 'community.comment.moderate', 'openvibe.community', []],
['blog', 'community.comment.moderate', 'openvibe.community', []],
- ['blog', 'media.object.read', 'openvibe.media', ['blog']],
+ ['blog', 'media.object.read', 'openvibe.media', ['blog', 'host']],
// Wave 10: VIP sells plans through Billing and projects its entitlements from Billing events.
// Wave 10: VIP sells plans through Billing and projects its entitlements from Billing events.
['vip', 'billing.intent.create', 'openvibe.billing', []],
['vip', 'billing.intent.create', 'openvibe.billing', []],
['vip', 'billing.subscription.manage', 'openvibe.billing', []],
['vip', 'billing.subscription.manage', 'openvibe.billing', []],
@@ -775,12 +779,8 @@ const REVOKED_GRANTS = [
@@ -775,12 +779,8 @@ const REVOKED_GRANTS = [
['space', 'vip.resource.policy.evaluate', 'openvibe.vip'],
['space', 'vip.resource.policy.evaluate', 'openvibe.vip'],
['space', 'events.event.read', 'openvibe.events'],
['space', 'events.event.read', 'openvibe.events'],
['space', 'network.blocks.read', SELF_AUDIENCE],
['space', 'network.blocks.read', SELF_AUDIENCE],
- // The static hosting engine left Host (T20, Host#84, contracts 0.150.0): its Media objects and Services' fan-out to
- // its resource index go. Space serves the engine.
- ['host', 'media.object.upload', 'openvibe.media'],
- ['host', 'media.object.read', 'openvibe.media'],
- ['host', 'media.object.list', 'openvibe.media'],
- ['host', 'media.object.delete', 'openvibe.media'],
+ // Plan L9 (2026-11-06): Host's Media grants come back for the screenshot pass (they went with the static
+ // hosting engine in T20). They are in RESTORED_GRANTS below, scoped to the "host" namespace.
['services', 'host.resource.read', 'openvibe.host'],
['services', 'host.resource.read', 'openvibe.host'],
];
];
@@ -800,6 +800,12 @@ const RESTORED_GRANTS = [
@@ -800,6 +800,12 @@ const RESTORED_GRANTS = [
['space', 'network.account.deletion.confirm', SELF_AUDIENCE],
['space', 'network.account.deletion.confirm', SELF_AUDIENCE],
// Codes publishes codes.task.* (contracts 0.232.0); revoked when the developer portal left it (0.113.0).
// Codes publishes codes.task.* (contracts 0.232.0); revoked when the developer portal left it (0.113.0).
['codes', 'events.event.publish', 'openvibe.events'],
['codes', 'events.event.publish', 'openvibe.events'],
+ // Plan L9 (2026-11-06): Host's Media grants are back (they went when the static hosting engine left Host in
+ // T20): ovhost shots stores the screenshots it captures in OpenVibe.Media, in Host's own tenant.
+ ['host', 'media.object.upload', 'openvibe.media'],
+ ['host', 'media.object.read', 'openvibe.media'],
+ ['host', 'media.object.list', 'openvibe.media'],
+ ['host', 'media.object.delete', 'openvibe.media'],
];
];
const CHANGED_DEFAULT_NAMESPACES = [
const CHANGED_DEFAULT_NAMESPACES = [
@@ -823,6 +829,13 @@ const CHANGED_DEFAULT_NAMESPACES = [
@@ -823,6 +829,13 @@ const CHANGED_DEFAULT_NAMESPACES = [
['media-hub', 'ai.run.create', 'openvibe.ai', ['media.analyze'], ['media.analyze', 'pics.describe_image']],
['media-hub', 'ai.run.create', 'openvibe.ai', ['media.analyze'], ['media.analyze', 'pics.describe_image']],
// Search's embedder runs AI workflow ai.embed (contracts 0.234.0; POST /api/v1/embed for hybrid ranking).
// Search's embedder runs AI workflow ai.embed (contracts 0.234.0; POST /api/v1/embed for hybrid ranking).
['search', 'ai.run.create', 'openvibe.ai', ['research.*'], ['research.*', 'ai.embed']],
['search', 'ai.run.create', 'openvibe.ai', ['research.*'], ['research.*', 'ai.embed']],
+ // Plan L9 (2026-11-06): Host's Media rows move from the old host/host.* list to the host namespace alone, and
+ // Blog's Media read reaches Host's tenant too, so the patch notes can read the shots Host captured.
+ ['host', 'media.object.upload', 'openvibe.media', ['host', 'host.*'], ['host']],
+ ['host', 'media.object.read', 'openvibe.media', ['host', 'host.*'], ['host']],
+ ['host', 'media.object.list', 'openvibe.media', ['host', 'host.*'], ['host']],
+ ['host', 'media.object.delete', 'openvibe.media', ['host', 'host.*'], ['host']],
+ ['blog', 'media.object.read', 'openvibe.media', ['blog'], ['blog', 'host']],
// Design's frames with a preference run ai.chat (Design #57, 10-31). Network #533 added it to the default; the
// Design's frames with a preference run ai.chat (Design #57, 10-31). Network #533 added it to the default; the
// production row, seeded before, kept ['ai.generate'] (the seed never changes an existing row), so frames still
// production row, seeded before, kept ['ai.generate'] (the seed never changes an existing row), so frames still
// failed until this moved it.
// failed until this moved it.
new file mode 100644
new file mode 100644
@@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
+'use strict';
+// Plan L9 (2026-11-06): the screenshot pass. Host stores the shots ovhost captures in OpenVibe.Media, in its own
+// tenant, and Blog reads them to put a before/after in the patch notes. Both are default grants.
+const assert = require('assert');
+const { getDb } = require('../server/db/database');
+const principals = require('../server/identity/principals');
+
+const HOST_CAPS = ['media.object.upload', 'media.object.read', 'media.object.list', 'media.object.delete'];
+
+(async () => {
+ const db = await getDb();
+ await principals.ensureSchema(db);
+
+ for (const cap of HOST_CAPS) {
+ const def = principals.DEFAULT_GRANTS.find((g) => g[0] === 'host' && g[1] === cap && g[2] === 'openvibe.media');
+ assert.ok(def, `host's ${cap} grant on openvibe.media is a default grant`);
+ assert.deepStrictEqual(def[3], ['host'], `host's ${cap} grant is on the host namespace alone`);
+ assert.ok(!principals.REVOKED_GRANTS.some((g) => g[0] === 'host' && g[1] === cap && g[2] === 'openvibe.media'),
+ `host's ${cap} grant is not revoked any more`);
+ assert.ok(principals.RESTORED_GRANTS.some((g) => g[0] === 'host' && g[1] === cap && g[2] === 'openvibe.media'),
+ `host's ${cap} grant comes back at boot (it is named in RESTORED_GRANTS)`);
+ }
+
+ // The rows are live after boot: not revoked, on the host namespace.
+ for (const cap of HOST_CAPS) {
+ const row = await db.prepare('SELECT revoked_at, namespaces FROM principal_grants WHERE client_id = ? AND capability = ? AND audience = ?')
+ .get('host', cap, 'openvibe.media');
+ assert.ok(row, `the ${cap} row exists`);
+ assert.strictEqual(row.revoked_at, null, `${cap} is not revoked`);
+ assert.deepStrictEqual(JSON.parse(row.namespaces || '[]'), ['host'], `${cap} is on the host namespace`);
+ }
+
+ // Blog reads its own tenant and Host's, so the patch notes can read the shots.
+ const blog = await db.prepare('SELECT revoked_at, namespaces FROM principal_grants WHERE client_id = ? AND capability = ? AND audience = ?')
+ .get('blog', 'media.object.read', 'openvibe.media');
+ assert.ok(blog, "blog's Media read grant exists");
+ const ns = JSON.parse(blog.namespaces || '[]');
+ assert.ok(ns.includes('blog') && ns.includes('host'), `blog reads its own tenant and Host's: ${JSON.stringify(ns)}`);
+ assert.strictEqual(blog.revoked_at, null, "blog's Media read grant is live");
+
+ console.log('l9 shots grants: Host owns its Media tenant again; Blog can read the shots');
+ process.exit(0);
+})().catch((err) => { console.error(err); process.exit(1); });