openvibers/openvibe.network Commits
Grants for the unsafe-image review (#583)
Changes
@@@ -95,7 +95,7 @@ person's username and picture (a service token, `identity.subject.resolve`).## Depends on--- `openvibe-contracts` v0.289.1 (registry manifests, capability checks), `openvibe-sdk` v0.82.0 (PostgreSQL++- `openvibe-contracts` v0.294.0 (registry manifests, capability checks), `openvibe-sdk` v0.82.0 (PostgreSQLand Valkey adapters, events, per-actor limits, universal telemetry), `openvibe-shared` v4.30.0, pinned by release tarball- OpenVibe.Events (Network's outbox relay; the events it consumes), OpenVibe.AI (the `network.site_copy`workflow), OpenVibe.Blog (the changelog feed it proxies)@@@ -1025,7 +1025,7 @@ Every change also sends the person a Network notification, so a swap nobody askeMIT (`license` in [package.json](package.json)).<!-- versions:start -->--- openvibe-contracts: v0.289.1++- openvibe-contracts: v0.294.0- openvibe-sdk: v0.82.0- openvibe-shared: v4.114.0<!-- versions:end -->@@@ -6,7 +6,7 @@"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9","port": 4000,"domain": "openvibe.network (with auth., api., admin., my. and themes.openvibe.network in its manifest)",-- "contracts": "openvibe-contracts v0.289.1",++ "contracts": "openvibe-contracts v0.294.0","sdk": "openvibe-sdk v0.82.0","shared": "openvibe-shared v4.114.0","capabilities": [@@@ -20,7 +20,7 @@"iovalkey": "^0.4.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.289.1",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.294.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.86.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.114.0","pg": "^8.23.0",@@@ -1722,9 +1722,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.289.1",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.289.1",-- "integrity": "sha512-LrKTwe8VPCWn+b0MTNmfjAYEJ0rtgLxeM6EHONBDl0ynt/Igndz+ruDEpwuz5SZ9HYCIMkCqSV5ujhedh6zmzQ==",++ "version": "0.294.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.294.0",++ "integrity": "sha512-CBDymrE4VrgslAAgCTCvecNrVcaevhBcIrb4nU4ApkJasst+6xt3RdR9lktsdHuhBeipdlKAfaxLAFmRadkKqA==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -24,7 +24,7 @@"iovalkey": "^0.4.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.289.1",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.294.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.86.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.114.0","pg": "^8.23.0",@@@ -96,6 +96,8 @@ const DEFAULT_GRANTS = [// pastes into Community on behalf of its users and its AI jobs.['community', 'identity.subject.resolve', SELF_AUDIENCE, []],['community', 'media.object.upload', 'openvibe.media', ['community']],++ // VIS-6: Community asks Vision to sort an image paste before it can be public.++ ['community', 'vision.image.review', 'openvibe.vision', []],// The sharing pilot (2026-11-06, Community #261): a screenshot is a private Media object served only through// Community's checked proxy (read), the old unlisted copy is deleted after the move (delete), and share notices go// through Network's inbox (push). New defaults, never revoked: boot inserts them.@@@ -113,6 +115,8 @@ const DEFAULT_GRANTS = [// its objects under the live namespace (VOD/clip/thumbnail uploads, reads, lists, deletes); OpenRestream uploads the// recordings and thumbnails it produces there too....['media.object.read', 'media.object.list', 'media.object.upload', 'media.object.delete'].map(cap => ['live', cap, 'openvibe.media', ['live']]),++ // VIS-6: Live's auto-clip text guard reports a category-3 AI text to Vision.++ ['live', 'vision.image.review', 'openvibe.vision', []],// media-hub: a record-only stream records into OpenVibe.Video's tenant as its owner's private video (plan 2026-10-22 §3 D1).['openre', 'media.object.upload', 'openvibe.media', ['live', 'media-hub']],// Compatibility register C-71: Inventory (Workshop badge images) and MediaHub (its library) reach Media with their@@@ -161,6 +165,8 @@ const DEFAULT_GRANTS = [['games', 'events.subscription.manage', 'openvibe.events', []],// Media and Tools subscribe to network.user.token_valid_after too (their sign-in refuses older tokens).['media', 'events.subscription.manage', 'openvibe.events', []],++ // VIS-6: Media asks Vision to review a visible image object before serving it.++ ['media', 'vision.image.review', 'openvibe.vision', []],['tools', 'events.subscription.manage', 'openvibe.events', []],// Mod principals (ADR-013, WS-M task 3): Games registers its mod installs and changes their grants.['games', 'mods.grant.manage', SELF_AUDIENCE, []],@@@ -665,6 +671,12 @@ const DEFAULT_GRANTS = [// its account-event subscriptions, and the export/deletion pair above. It has no public records, so no Search grant.['vision', 'ai.run.create', 'openvibe.ai', ['ai.complete']],['vision', 'events.subscription.manage', 'openvibe.events', []],++ // VIS-6: the operator review, its evidence view and the decision that releases Media's evidence hold.++ // The media namespaces are the first-party tenants whose objects can be reviewed.++ ['vision', 'media.object.review.view', 'openvibe.media', ['live', 'community', 'media']],++ ['vision', 'media.object.review.decide', 'openvibe.media', ['live', 'community', 'media']],++ ['vision', 'vision.review.read', 'openvibe.vision', []],++ ['vision', 'vision.review.decide', 'openvibe.vision', []],// Vision publishes vision.result.ready and vision.alert (VIS-5, contracts manifests/services/vision.json): without this// its outbox retried every event (found 11-02 with Events' sources check).['vision', 'events.event.publish', 'openvibe.events', []],@@@ -133,6 +133,20 @@ const server = http.createServer(app);assert.deepStrictEqual([visionAiClaims.claims.cap, visionAiClaims.claims.ns], [['ai.run.create'], ['ai.complete']], 'Vision runs AI workflow ai.complete through the OpenAI-compatible endpoint');t = await token({ client_id: 'vision', client_secret: 'vision-secret', audience: 'openvibe.search' });assert.strictEqual(t.status, 400, 'Vision has no public records, so no Search grant');++ // VIS-6: the unsafe-image review. Media, Community and Live ask Vision; Vision alone can open Media's++ // 60-second operator evidence view and apply the operator's overruling category.++ for (const [client, secret] of [['media', 'media-secret'], ['community', 'community-secret'], ['live', 'live-secret']]) {++ await db.prepare('UPDATE oauth_clients SET client_secret = ? WHERE client_id = ?').run(secret, client);++ t = await token({ client_id: client, client_secret: secret, audience: 'openvibe.vision' });++ assert.strictEqual(t.status, 200, `${client}: ${JSON.stringify(t.body)}`);++ assert.strictEqual(t.body.scope, 'vision.image.review', `${client} sorts an image through Vision`);++ }++ t = await token({ client_id: 'vision', client_secret: 'vision-secret', audience: 'openvibe.media' });++ assert.strictEqual(t.status, 200, JSON.stringify(t.body));++ assert.deepStrictEqual(t.body.scope.split(' ').sort(), ['media.object.review.decide', 'media.object.review.view'], 'Vision opens and decides Media evidence views');++ t = await token({ client_id: 'vision', client_secret: 'vision-secret', audience: 'openvibe.vision' });++ assert.strictEqual(t.status, 200, JSON.stringify(t.body));++ assert.deepStrictEqual(t.body.scope.split(' ').sort(), ['vision.review.decide', 'vision.review.read'], 'Vision staff tools read and decide reviews');t = await token({ client_id: 'exchange', client_secret: 'exchange-secret', audience: 'openvibe.search' });assert.strictEqual(t.body.scope, 'search.document.write', 'Exchange indexes its public listings in Search');t = await token({ client_id: 'exchange', client_secret: 'exchange-secret', audience: 'openvibe.ai' });
@@ -95,7 +95,7 @@ person's username and picture (a service token, `identity.subject.resolve`).
@@ -95,7 +95,7 @@ person's username and picture (a service token, `identity.subject.resolve`).
## Depends on
## Depends on
-- `openvibe-contracts` v0.289.1 (registry manifests, capability checks), `openvibe-sdk` v0.82.0 (PostgreSQL
+- `openvibe-contracts` v0.294.0 (registry manifests, capability checks), `openvibe-sdk` v0.82.0 (PostgreSQL
and Valkey adapters, events, per-actor limits, universal telemetry), `openvibe-shared` v4.30.0, pinned by release tarball
and Valkey adapters, events, per-actor limits, universal telemetry), `openvibe-shared` v4.30.0, pinned by release tarball
- OpenVibe.Events (Network's outbox relay; the events it consumes), OpenVibe.AI (the `network.site_copy`
- OpenVibe.Events (Network's outbox relay; the events it consumes), OpenVibe.AI (the `network.site_copy`
workflow), OpenVibe.Blog (the changelog feed it proxies)
workflow), OpenVibe.Blog (the changelog feed it proxies)
@@ -1025,7 +1025,7 @@ Every change also sends the person a Network notification, so a swap nobody aske
@@ -1025,7 +1025,7 @@ Every change also sends the person a Network notification, so a swap nobody aske
MIT (`license` in [package.json](package.json)).
MIT (`license` in [package.json](package.json)).
<!-- versions:start -->
<!-- versions:start -->
-- openvibe-contracts: v0.289.1
+- openvibe-contracts: v0.294.0
- openvibe-sdk: v0.82.0
- openvibe-sdk: v0.82.0
- openvibe-shared: v4.114.0
- openvibe-shared: v4.114.0
<!-- versions:end -->
<!-- versions:end -->
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": 4000,
"port": 4000,
"domain": "openvibe.network (with auth., api., admin., my. and themes.openvibe.network in its manifest)",
"domain": "openvibe.network (with auth., api., admin., my. and themes.openvibe.network in its manifest)",
- "contracts": "openvibe-contracts v0.289.1",
+ "contracts": "openvibe-contracts v0.294.0",
"sdk": "openvibe-sdk v0.82.0",
"sdk": "openvibe-sdk v0.82.0",
"shared": "openvibe-shared v4.114.0",
"shared": "openvibe-shared v4.114.0",
"capabilities": [
"capabilities": [
@@ -20,7 +20,7 @@
@@ -20,7 +20,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.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.289.1",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.294.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.86.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.86.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.114.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.114.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
@@ -1722,9 +1722,9 @@
@@ -1722,9 +1722,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.289.1",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.289.1",
- "integrity": "sha512-LrKTwe8VPCWn+b0MTNmfjAYEJ0rtgLxeM6EHONBDl0ynt/Igndz+ruDEpwuz5SZ9HYCIMkCqSV5ujhedh6zmzQ==",
+ "version": "0.294.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.294.0",
+ "integrity": "sha512-CBDymrE4VrgslAAgCTCvecNrVcaevhBcIrb4nU4ApkJasst+6xt3RdR9lktsdHuhBeipdlKAfaxLAFmRadkKqA==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -24,7 +24,7 @@
@@ -24,7 +24,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.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.289.1",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.294.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.86.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.86.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.114.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.114.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
@@ -96,6 +96,8 @@ const DEFAULT_GRANTS = [
@@ -96,6 +96,8 @@ const DEFAULT_GRANTS = [
// pastes into Community on behalf of its users and its AI jobs.
// pastes into Community on behalf of its users and its AI jobs.
['community', 'identity.subject.resolve', SELF_AUDIENCE, []],
['community', 'identity.subject.resolve', SELF_AUDIENCE, []],
['community', 'media.object.upload', 'openvibe.media', ['community']],
['community', 'media.object.upload', 'openvibe.media', ['community']],
+ // VIS-6: Community asks Vision to sort an image paste before it can be public.
+ ['community', 'vision.image.review', 'openvibe.vision', []],
// The sharing pilot (2026-11-06, Community #261): a screenshot is a private Media object served only through
// The sharing pilot (2026-11-06, Community #261): a screenshot is a private Media object served only through
// Community's checked proxy (read), the old unlisted copy is deleted after the move (delete), and share notices go
// Community's checked proxy (read), the old unlisted copy is deleted after the move (delete), and share notices go
// through Network's inbox (push). New defaults, never revoked: boot inserts them.
// through Network's inbox (push). New defaults, never revoked: boot inserts them.
@@ -113,6 +115,8 @@ const DEFAULT_GRANTS = [
@@ -113,6 +115,8 @@ const DEFAULT_GRANTS = [
// its objects under the live namespace (VOD/clip/thumbnail uploads, reads, lists, deletes); OpenRestream uploads the
// its objects under the live namespace (VOD/clip/thumbnail uploads, reads, lists, deletes); OpenRestream uploads the
// recordings and thumbnails it produces there too.
// recordings and thumbnails it produces there too.
...['media.object.read', 'media.object.list', 'media.object.upload', 'media.object.delete'].map(cap => ['live', cap, 'openvibe.media', ['live']]),
...['media.object.read', 'media.object.list', 'media.object.upload', 'media.object.delete'].map(cap => ['live', cap, 'openvibe.media', ['live']]),
+ // VIS-6: Live's auto-clip text guard reports a category-3 AI text to Vision.
+ ['live', 'vision.image.review', 'openvibe.vision', []],
// media-hub: a record-only stream records into OpenVibe.Video's tenant as its owner's private video (plan 2026-10-22 §3 D1).
// media-hub: a record-only stream records into OpenVibe.Video's tenant as its owner's private video (plan 2026-10-22 §3 D1).
['openre', 'media.object.upload', 'openvibe.media', ['live', 'media-hub']],
['openre', 'media.object.upload', 'openvibe.media', ['live', 'media-hub']],
// Compatibility register C-71: Inventory (Workshop badge images) and MediaHub (its library) reach Media with their
// Compatibility register C-71: Inventory (Workshop badge images) and MediaHub (its library) reach Media with their
@@ -161,6 +165,8 @@ const DEFAULT_GRANTS = [
@@ -161,6 +165,8 @@ const DEFAULT_GRANTS = [
['games', 'events.subscription.manage', 'openvibe.events', []],
['games', 'events.subscription.manage', 'openvibe.events', []],
// Media and Tools subscribe to network.user.token_valid_after too (their sign-in refuses older tokens).
// Media and Tools subscribe to network.user.token_valid_after too (their sign-in refuses older tokens).
['media', 'events.subscription.manage', 'openvibe.events', []],
['media', 'events.subscription.manage', 'openvibe.events', []],
+ // VIS-6: Media asks Vision to review a visible image object before serving it.
+ ['media', 'vision.image.review', 'openvibe.vision', []],
['tools', 'events.subscription.manage', 'openvibe.events', []],
['tools', 'events.subscription.manage', 'openvibe.events', []],
// Mod principals (ADR-013, WS-M task 3): Games registers its mod installs and changes their grants.
// Mod principals (ADR-013, WS-M task 3): Games registers its mod installs and changes their grants.
['games', 'mods.grant.manage', SELF_AUDIENCE, []],
['games', 'mods.grant.manage', SELF_AUDIENCE, []],
@@ -665,6 +671,12 @@ const DEFAULT_GRANTS = [
@@ -665,6 +671,12 @@ const DEFAULT_GRANTS = [
// its account-event subscriptions, and the export/deletion pair above. It has no public records, so no Search grant.
// its account-event subscriptions, and the export/deletion pair above. It has no public records, so no Search grant.
['vision', 'ai.run.create', 'openvibe.ai', ['ai.complete']],
['vision', 'ai.run.create', 'openvibe.ai', ['ai.complete']],
['vision', 'events.subscription.manage', 'openvibe.events', []],
['vision', 'events.subscription.manage', 'openvibe.events', []],
+ // VIS-6: the operator review, its evidence view and the decision that releases Media's evidence hold.
+ // The media namespaces are the first-party tenants whose objects can be reviewed.
+ ['vision', 'media.object.review.view', 'openvibe.media', ['live', 'community', 'media']],
+ ['vision', 'media.object.review.decide', 'openvibe.media', ['live', 'community', 'media']],
+ ['vision', 'vision.review.read', 'openvibe.vision', []],
+ ['vision', 'vision.review.decide', 'openvibe.vision', []],
// Vision publishes vision.result.ready and vision.alert (VIS-5, contracts manifests/services/vision.json): without this
// Vision publishes vision.result.ready and vision.alert (VIS-5, contracts manifests/services/vision.json): without this
// its outbox retried every event (found 11-02 with Events' sources check).
// its outbox retried every event (found 11-02 with Events' sources check).
['vision', 'events.event.publish', 'openvibe.events', []],
['vision', 'events.event.publish', 'openvibe.events', []],
@@ -133,6 +133,20 @@ const server = http.createServer(app);
@@ -133,6 +133,20 @@ const server = http.createServer(app);
assert.deepStrictEqual([visionAiClaims.claims.cap, visionAiClaims.claims.ns], [['ai.run.create'], ['ai.complete']], 'Vision runs AI workflow ai.complete through the OpenAI-compatible endpoint');
assert.deepStrictEqual([visionAiClaims.claims.cap, visionAiClaims.claims.ns], [['ai.run.create'], ['ai.complete']], 'Vision runs AI workflow ai.complete through the OpenAI-compatible endpoint');
t = await token({ client_id: 'vision', client_secret: 'vision-secret', audience: 'openvibe.search' });
t = await token({ client_id: 'vision', client_secret: 'vision-secret', audience: 'openvibe.search' });
assert.strictEqual(t.status, 400, 'Vision has no public records, so no Search grant');
assert.strictEqual(t.status, 400, 'Vision has no public records, so no Search grant');
+ // VIS-6: the unsafe-image review. Media, Community and Live ask Vision; Vision alone can open Media's
+ // 60-second operator evidence view and apply the operator's overruling category.
+ for (const [client, secret] of [['media', 'media-secret'], ['community', 'community-secret'], ['live', 'live-secret']]) {
+ await db.prepare('UPDATE oauth_clients SET client_secret = ? WHERE client_id = ?').run(secret, client);
+ t = await token({ client_id: client, client_secret: secret, audience: 'openvibe.vision' });
+ assert.strictEqual(t.status, 200, `${client}: ${JSON.stringify(t.body)}`);
+ assert.strictEqual(t.body.scope, 'vision.image.review', `${client} sorts an image through Vision`);
+ }
+ t = await token({ client_id: 'vision', client_secret: 'vision-secret', audience: 'openvibe.media' });
+ assert.strictEqual(t.status, 200, JSON.stringify(t.body));
+ assert.deepStrictEqual(t.body.scope.split(' ').sort(), ['media.object.review.decide', 'media.object.review.view'], 'Vision opens and decides Media evidence views');
+ t = await token({ client_id: 'vision', client_secret: 'vision-secret', audience: 'openvibe.vision' });
+ assert.strictEqual(t.status, 200, JSON.stringify(t.body));
+ assert.deepStrictEqual(t.body.scope.split(' ').sort(), ['vision.review.decide', 'vision.review.read'], 'Vision staff tools read and decide reviews');
t = await token({ client_id: 'exchange', client_secret: 'exchange-secret', audience: 'openvibe.search' });
t = await token({ client_id: 'exchange', client_secret: 'exchange-secret', audience: 'openvibe.search' });
assert.strictEqual(t.body.scope, 'search.document.write', 'Exchange indexes its public listings in Search');
assert.strictEqual(t.body.scope, 'search.document.write', 'Exchange indexes its public listings in Search');
t = await token({ client_id: 'exchange', client_secret: 'exchange-secret', audience: 'openvibe.ai' });
t = await token({ client_id: 'exchange', client_secret: 'exchange-secret', audience: 'openvibe.ai' });