openvibers/openvibe.network Commits

Give Actor private Media access for sandbox skill code (#824)

OP OpenVibers committed 2 days ago · ea6ccd8f05 · 6 files, +18 −16

* Grant Actor private Media access for sandbox skill code

* Make receipt route test ignore background Mail probes

* Make AI config proxy test ignore background Run probes

Changes

ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex 5ef81399..b7ca5043 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -146,11 +146,11 @@ const DEFAULT_GRANTS = [
// Compatibility register C-71: Inventory (Workshop badge images) and MediaHub (its library) reach Media with their
// own tokens on their own tenants, not with app keys. Upload also covers the delete of an object.
['inventory', 'media.object.upload', 'openvibe.media', ['inventory']],
-- // Codes (C8 proof screenshots, the rich embeds of a task) keeps a person's pictures in Media as private objects on
-- // its own tenant, reached with its own token (10-30). A grant on a host's own tenant never reaches the agents it
-- // hosts: an agent's token names only its project's namespaces (agent-tokens.test.js), so it gets no `actor`.
-- // Actor holds no Media grant for now all the same: its artifacts are in its own database, owner-only (Actor V2-4).
++ // Codes keeps a person's task pictures in Media. Actor's sandbox skills keep scanned code resources there.
++ // Each service reaches only its own tenant with its own token. A grant on Actor's tenant never reaches the agents
++ // it hosts: an agent's token names only its project's namespaces (agent-tokens.test.js), so it gets no `actor`.
...['media.object.upload', 'media.object.read'].map((cap) => ['codes', cap, 'openvibe.media', ['codes']]),
++ ...['media.object.upload', 'media.object.read'].map((cap) => ['actor', cap, 'openvibe.media', ['actor']]),
['media-hub', 'media.object.upload', 'openvibe.media', ['media-hub']],
['media-hub', 'media.object.read', 'openvibe.media', ['media-hub']],
// OpenVibe.Video's AI edit (MediaHub server/video/ai-edit.js): media.analyze on finished uploads, local and unpaid;
ddiff --git a/test/agent-grants.test.js b/test/agent-grants.test.js
iindex 1494f7dd..9d8548e5 100644
---- a/test/agent-grants.test.js
++++ b/test/agent-grants.test.js
@@@ -24,6 +24,8 @@ await db.prepare(`INSERT INTO users (id, username, password_hash, role) VALUES
(10, 'owner', 'x', 'user'), (11, 'dev', 'x', 'user'), (12, 'viewer', 'x', 'user'), (13, 'stranger', 'x', 'user'), (14, 'staff', 'x', 'admin'), (15, 'admin', 'x', 'user')`).run();
// The seed creates Actor's client (server/db/database.js); this file sets the fields it relies on.
await db.prepare("INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris, is_first_party) VALUES ('actor', 'x', 'OpenVibe.Actor', '[]', 1) ON CONFLICT (client_id) DO UPDATE SET client_secret = EXCLUDED.client_secret, name = EXCLUDED.name, redirect_uris = EXCLUDED.redirect_uris, is_first_party = EXCLUDED.is_first_party").run();
++// Exercise a service host without a Media grant; the default Actor grant is checked in media-private-grants.test.js.
++await db.prepare("DELETE FROM principal_grants WHERE client_id = 'actor' AND audience = 'openvibe.media'").run();
const S = {};
for (const [id, name] of PEOPLE) S[name] = await subjects.ensureUserSubject(db, await db.prepare('SELECT * FROM users WHERE id = ?').get(id));
@@@ -121,7 +123,7 @@ for (const [id, name] of PEOPLE) T[name] = jwt.sign({ sub: id, id }, keys.privat
const chat = ok(await put('dev', svcBot, 'chat.message.send', { mode: 'confirm' }));
assert.deepStrictEqual([chat.audience, chat.effective_mode, chat.within_host], ['openvibe.chat', 'confirm', true]);
refused(await put('dev', svcBot, 'chat.moderation.read'), 403, 'grant.beyond_host'); // internal, though Actor holds it
-- refused(await put('dev', svcBot, 'media.object.read'), 403, 'grant.beyond_host'); // Actor does not hold it
++ refused(await put('dev', svcBot, 'media.object.read'), 403, 'grant.beyond_host'); // This service host has no Media grant
// ── Read: members and staff ──
assert.deepStrictEqual(ok(await api('viewer', 'GET', `/${P}/agents/${ownBot}/grants`)).grants.map((g) => g.capability), ['media.object.delete', 'media.object.upload']);
ddiff --git a/test/agent-tokens.test.js b/test/agent-tokens.test.js
iindex d874f60d..a5f352ab 100644
---- a/test/agent-tokens.test.js
++++ b/test/agent-tokens.test.js
@@@ -187,7 +187,7 @@ const U = { owner: jwt.sign({ sub: 10, id: 10 }, keys.privateKey, { algorithm: '
// ── Service host: ns bounded by the host's own principal_grants rows ──
SECRETS.actor = 'actor-secret';
-- const grantActor = (cap, ns) => db.prepare("INSERT INTO principal_grants (client_id, capability, audience, namespaces, granted_by) VALUES ('actor', ?, 'openvibe.media', ?, 'test')").run(cap, JSON.stringify(ns));
++ const grantActor = (cap, ns) => db.prepare("INSERT INTO principal_grants (client_id, capability, audience, namespaces, granted_by) VALUES ('actor', ?, 'openvibe.media', ?, 'test') ON CONFLICT (client_id, capability, audience) DO UPDATE SET namespaces = EXCLUDED.namespaces").run(cap, JSON.stringify(ns));
await grantActor('media.object.read', [P]);
await grantActor('media.object.delete', ['app.*']);
const S = ok(await api('owner', 'POST', `/${P}/agents`, { name: 'Actor agent', host: { type: 'service', id: 'actor' } }), 201).id;
ddiff --git a/test/ai-config-proxy.test.js b/test/ai-config-proxy.test.js
iindex 4f092e4c..e4239f7f 100644
---- a/test/ai-config-proxy.test.js
++++ b/test/ai-config-proxy.test.js
@@@ -113,7 +113,8 @@ function summary(service, namespace, extra = {}) {
const compute = await ownerCall('GET','/api/admin/ai/config/compute/status');
assert.strictEqual(compute.status,200);
assert.strictEqual(compute.body.compute.budgets[0].near_cap,true);
-- assert.ok(run.seen[0].headers['x-ov-subject']);
++ const computeRequest = run.seen.find((request) => request.url === '/internal/config/compute-status');
++ assert.ok(computeRequest && computeRequest.headers['x-ov-subject']);
// ── Only the owner: a user and a plain admin are refused, and nothing reaches a service ──
let r = await call('alice', 'GET', '/api/admin/ai/config/services');
ddiff --git a/test/media-private-grants.test.js b/test/media-private-grants.test.js
iindex 32a19fe7..ab99ebe6 100644
---- a/test/media-private-grants.test.js
++++ b/test/media-private-grants.test.js
@@@ -1,8 +1,7 @@
/**
-- * OpenVibe.Codes keeps a person's pictures in Media as private objects (task screenshots, 10-30). Media takes no app
-- * keys (C-71), so Codes reaches it with its own Network service token: upload and read, on its own tenant only, and
-- * nothing else of Media's. Actor holds no Media grant: its artifacts live in its own database, owner-only (Actor V2-4).
-- * Were it granted one, the agents it hosts would still not reach its tenant (agent-tokens.test.js).
++ * Codes keeps task pictures in Media; Actor keeps scanned sandbox skill code there. Each uses its own Network service
++ * token to upload and read private objects on its own tenant only. Actor's hosted agents do not inherit its tenant
++ * grant (agent-tokens.test.js).
*
* node test/media-private-grants.test.js
*/
@@@ -11,7 +10,7 @@ const assert = require('assert');
const contracts = require('openvibe-contracts');
const principals = require('../server/identity/principals');
--for (const svc of ['codes']) {
++for (const svc of ['codes', 'actor']) {
const rows = principals.DEFAULT_GRANTS.filter((g) => g[0] === svc && g[2] === 'openvibe.media');
const held = new Map(rows.map((g) => [g[1], g[3]]));
assert.deepStrictEqual([...held.keys()].sort(), ['media.object.read', 'media.object.upload'], `${svc} uploads and reads, nothing more`);
@@@ -21,5 +20,4 @@ for (const svc of ['codes']) {
assert.ok(c && c.status === 'active', `${cap} is an active capability`);
}
}
--assert.strictEqual(principals.DEFAULT_GRANTS.filter((g) => g[0] === 'actor' && g[2] === 'openvibe.media').length, 0, 'the agent host holds no Media grant');
--console.log('media private grants: codes on its own tenant; none for the agent host');
++console.log('media private grants: codes and actor on their own tenants only');
ddiff --git a/test/my-receipts-route.test.js b/test/my-receipts-route.test.js
iindex 896bcfdc..c51f547e 100644
---- a/test/my-receipts-route.test.js
++++ b/test/my-receipts-route.test.js
@@@ -38,8 +38,9 @@ const { signToken } = require('../server/auth/routes');
assert.strictEqual(d.enabled, true);
assert.strictEqual(d.receipts.length, 1);
assert.strictEqual(d.receipts[0].message_id, 'msg_1');
-- assert.strictEqual(asked[0].url, '/api/v1/me/mail/receipts', 'the real app reached Mail');
-- assert.strictEqual(asked[0].auth, `Bearer ${token}`, "with the person's own token");
++ const receiptRequest = asked.find((request) => request.url === '/api/v1/me/mail/receipts');
++ assert.ok(receiptRequest, 'the real app reached Mail');
++ assert.strictEqual(receiptRequest.auth, `Bearer ${token}`, "with the person's own token");
mode = 'off';
r = await get({ authorization: `Bearer ${token}` });