openvibers/openvibe.network Commits

Grants for the unsafe-image review (#583)

OP OpenVibers committed 7 days ago · 40c05b05f5 · 6 files, +34 −8

* Grants for the unsafe-image review: Media, Community and Live ask Vision; Vision opens Media evidence

* Docs: name openvibe-contracts v0.294.0

* Docs: the dependency list names openvibe-contracts v0.294.0

Changes

ddiff --git a/README.md b/README.md
iindex 71a6d7f4..a40219f1 100644
---- a/README.md
++++ b/README.md
@@@ -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 (PostgreSQL
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`
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
MIT (`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 -->
ddiff --git a/STATUS.json b/STATUS.json
iindex 05ca7482..cae50e7a 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -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": [
ddiff --git a/package-lock.json b/package-lock.json
iindex 62fb815c..a67082d1 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -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",
ddiff --git a/package.json b/package.json
iindex f4c0b7e3..0100fb7f 100644
---- a/package.json
++++ b/package.json
@@@ -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",
ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex ce5e869e..c148f795 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -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', []],
ddiff --git a/test/principals.test.js b/test/principals.test.js
iindex a2f95b44..fb8d177e 100644
---- a/test/principals.test.js
++++ b/test/principals.test.js
@@@ -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' });