openvibers/openvibe.network Commits
Grants: ai.usage.report to Actor, Bot, Codes and Chat (contracts v0.421.0) (#898)
Changes
@@@ -1036,7 +1036,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.419.0++- openvibe-contracts: v0.421.0- openvibe-sdk: v0.161.0- openvibe-shared: v4.232.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.419.0",++ "contracts": "openvibe-contracts v0.421.0","sdk": "openvibe-sdk v0.161.0","shared": "openvibe-shared v4.232.0","capabilities": [@@@ -22,7 +22,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.419.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.421.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.161.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.232.0","otpauth": "9.6.0",@@@ -2679,9 +2679,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.419.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.419.0",-- "integrity": "sha512-VxrTdeHchT8gvl2wWC9ASaId92en7ZKfJ8F/0hKzCAD0f2Y00rPrqgBWF9BNPoLGARoyQvsMbhBEhSkhxGNnjA==",++ "version": "0.421.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.421.0",++ "integrity": "sha512-iRSbGGqTS6Lr4XYrAvN5yhoCTkPkxrU/l3dufJCsc5BhbGU9F4ZSNt0FrgpqDseNxRM/8zx8Zu3t/w7vryAfBw==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -26,7 +26,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.419.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.421.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.161.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.232.0","otpauth": "9.6.0",@@@ -402,6 +402,10 @@ const DEFAULT_GRANTS = [['network', 'ai.layout.suggest', 'openvibe.ai', []],// Host's demand planner reads AI's usage feed (Host #283): ai.usage.read on openvibe.ai.['host', 'ai.usage.read', 'openvibe.ai', []],++ // Every AI call measured (ai-metrics.md, contracts 0.421.0): a product reports the model calls it made without++ // OpenVibe.AI (Actor's own provider calls, a person's local model through Bot, a Codes harness turn, Chat) to AI's++ // POST /api/v1/usage/calls, and AI prices and publishes each as ai.call.recorded@1.++ ...['actor', 'bot', 'codes', 'chat'].map((svc) => [svc, 'ai.usage.report', 'openvibe.ai', []]),// Plan T2 N4b: Bot pairs its users' machines with Network and reads or revokes only the principals it paired.['bot', 'network.link.manage', SELF_AUDIENCE, []],// Plan T15: an owner adds an operator by @username on the robot's panel; Bot resolves it to the subject here.nnew file mode 100644@@@ -0,0 +1,17 @@++/**++ * Every AI call measured (contracts 0.421.0, ai-metrics.md): the products that make model calls without OpenVibe.AI++ * report them to AI's POST /api/v1/usage/calls with ai.usage.report on audience openvibe.ai. Only those four hold it,++ * and AI itself does not (it publishes its own calls).++ *++ * node test/ai-usage-report-grant.test.js++ */++'use strict';++const assert = require('assert');++const contracts = require('openvibe-contracts');++const principals = require('../server/identity/principals');++++const holders = principals.DEFAULT_GRANTS.filter((g) => g[1] === 'ai.usage.report').map((g) => [g[0], g[2]]);++assert.deepStrictEqual(holders, [['actor', 'openvibe.ai'], ['bot', 'openvibe.ai'], ['codes', 'openvibe.ai'], ['chat', 'openvibe.ai']]);++const cap = contracts.capabilities.get('ai.usage.report');++assert.ok(cap && cap.status === 'active' && cap.owner === 'ai', 'ai.usage.report is an active AI capability in the pinned contracts');++console.log('ai usage report grant: ok');@@@ -74,7 +74,7 @@ const runTokens = require('../server/identity/run-tokens');assert.ok(has('codes', 'ai.credential.manage', 'openvibe.ai'), 'Codes holds the consent grant for AI');assert.ok(has('codes', 'ai.run.create', 'openvibe.ai'), 'Codes runs AI workflows for its harnesses');const codesGrants = await principals.grantsFor(db, 'codes', 'openvibe.ai');-- assert.deepStrictEqual(codesGrants.map((g) => g.capability).sort(), ['ai.credential.manage', 'ai.run.create']);++ assert.deepStrictEqual(codesGrants.map((g) => g.capability).sort(), ['ai.credential.manage', 'ai.run.create', 'ai.usage.report'], 'and it reports its harness turns to AI\'s meter');// ── The grants Actor holds for OpenVibe.AI (plan L55 C3) ──// A person chooses the model their tasks run on, with their own key (sealed in AI) or OpenVibe's. Actor runs@@@ -85,7 +85,7 @@ const runTokens = require('../server/identity/run-tokens');const actorGrant = principals.DEFAULT_GRANTS.find((g) => g[0] === 'actor' && g[1] === 'ai.run.create' && g[2] === 'openvibe.ai');assert.deepStrictEqual(actorGrant[3], ['ai.complete'], 'AI\'s OpenAI-compatible workflow only');const actorGrants = await principals.grantsFor(db, 'actor', 'openvibe.ai');-- assert.deepStrictEqual(actorGrants.map((g) => g.capability).sort(), ['ai.run.create']);++ assert.deepStrictEqual(actorGrants.map((g) => g.capability).sort(), ['ai.run.create', 'ai.usage.report'], 'and it reports its own provider calls to AI\'s meter');// ── A run token: the person it acts for, and how long it lives ──let t = await runToken(OWNER);
@@ -1036,7 +1036,7 @@ Every change also sends the person a Network notification, so a swap nobody aske
@@ -1036,7 +1036,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.419.0
+- openvibe-contracts: v0.421.0
- openvibe-sdk: v0.161.0
- openvibe-sdk: v0.161.0
- openvibe-shared: v4.232.0
- openvibe-shared: v4.232.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.419.0",
+ "contracts": "openvibe-contracts v0.421.0",
"sdk": "openvibe-sdk v0.161.0",
"sdk": "openvibe-sdk v0.161.0",
"shared": "openvibe-shared v4.232.0",
"shared": "openvibe-shared v4.232.0",
"capabilities": [
"capabilities": [
@@ -22,7 +22,7 @@
@@ -22,7 +22,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.419.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.421.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.161.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.161.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.232.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.232.0",
"otpauth": "9.6.0",
"otpauth": "9.6.0",
@@ -2679,9 +2679,9 @@
@@ -2679,9 +2679,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.419.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.419.0",
- "integrity": "sha512-VxrTdeHchT8gvl2wWC9ASaId92en7ZKfJ8F/0hKzCAD0f2Y00rPrqgBWF9BNPoLGARoyQvsMbhBEhSkhxGNnjA==",
+ "version": "0.421.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.421.0",
+ "integrity": "sha512-iRSbGGqTS6Lr4XYrAvN5yhoCTkPkxrU/l3dufJCsc5BhbGU9F4ZSNt0FrgpqDseNxRM/8zx8Zu3t/w7vryAfBw==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -26,7 +26,7 @@
@@ -26,7 +26,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.419.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.421.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.161.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.161.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.232.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.232.0",
"otpauth": "9.6.0",
"otpauth": "9.6.0",
@@ -402,6 +402,10 @@ const DEFAULT_GRANTS = [
@@ -402,6 +402,10 @@ const DEFAULT_GRANTS = [
['network', 'ai.layout.suggest', 'openvibe.ai', []],
['network', 'ai.layout.suggest', 'openvibe.ai', []],
// Host's demand planner reads AI's usage feed (Host #283): ai.usage.read on openvibe.ai.
// Host's demand planner reads AI's usage feed (Host #283): ai.usage.read on openvibe.ai.
['host', 'ai.usage.read', 'openvibe.ai', []],
['host', 'ai.usage.read', 'openvibe.ai', []],
+ // Every AI call measured (ai-metrics.md, contracts 0.421.0): a product reports the model calls it made without
+ // OpenVibe.AI (Actor's own provider calls, a person's local model through Bot, a Codes harness turn, Chat) to AI's
+ // POST /api/v1/usage/calls, and AI prices and publishes each as ai.call.recorded@1.
+ ...['actor', 'bot', 'codes', 'chat'].map((svc) => [svc, 'ai.usage.report', 'openvibe.ai', []]),
// Plan T2 N4b: Bot pairs its users' machines with Network and reads or revokes only the principals it paired.
// Plan T2 N4b: Bot pairs its users' machines with Network and reads or revokes only the principals it paired.
['bot', 'network.link.manage', SELF_AUDIENCE, []],
['bot', 'network.link.manage', SELF_AUDIENCE, []],
// Plan T15: an owner adds an operator by @username on the robot's panel; Bot resolves it to the subject here.
// Plan T15: an owner adds an operator by @username on the robot's panel; Bot resolves it to the subject here.
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+/**
+ * Every AI call measured (contracts 0.421.0, ai-metrics.md): the products that make model calls without OpenVibe.AI
+ * report them to AI's POST /api/v1/usage/calls with ai.usage.report on audience openvibe.ai. Only those four hold it,
+ * and AI itself does not (it publishes its own calls).
+ *
+ * node test/ai-usage-report-grant.test.js
+ */
+'use strict';
+const assert = require('assert');
+const contracts = require('openvibe-contracts');
+const principals = require('../server/identity/principals');
+
+const holders = principals.DEFAULT_GRANTS.filter((g) => g[1] === 'ai.usage.report').map((g) => [g[0], g[2]]);
+assert.deepStrictEqual(holders, [['actor', 'openvibe.ai'], ['bot', 'openvibe.ai'], ['codes', 'openvibe.ai'], ['chat', 'openvibe.ai']]);
+const cap = contracts.capabilities.get('ai.usage.report');
+assert.ok(cap && cap.status === 'active' && cap.owner === 'ai', 'ai.usage.report is an active AI capability in the pinned contracts');
+console.log('ai usage report grant: ok');
@@ -74,7 +74,7 @@ const runTokens = require('../server/identity/run-tokens');
@@ -74,7 +74,7 @@ const runTokens = require('../server/identity/run-tokens');
assert.ok(has('codes', 'ai.credential.manage', 'openvibe.ai'), 'Codes holds the consent grant for AI');
assert.ok(has('codes', 'ai.credential.manage', 'openvibe.ai'), 'Codes holds the consent grant for AI');
assert.ok(has('codes', 'ai.run.create', 'openvibe.ai'), 'Codes runs AI workflows for its harnesses');
assert.ok(has('codes', 'ai.run.create', 'openvibe.ai'), 'Codes runs AI workflows for its harnesses');
const codesGrants = await principals.grantsFor(db, 'codes', 'openvibe.ai');
const codesGrants = await principals.grantsFor(db, 'codes', 'openvibe.ai');
- assert.deepStrictEqual(codesGrants.map((g) => g.capability).sort(), ['ai.credential.manage', 'ai.run.create']);
+ assert.deepStrictEqual(codesGrants.map((g) => g.capability).sort(), ['ai.credential.manage', 'ai.run.create', 'ai.usage.report'], 'and it reports its harness turns to AI\'s meter');
// ── The grants Actor holds for OpenVibe.AI (plan L55 C3) ──
// ── The grants Actor holds for OpenVibe.AI (plan L55 C3) ──
// A person chooses the model their tasks run on, with their own key (sealed in AI) or OpenVibe's. Actor runs
// A person chooses the model their tasks run on, with their own key (sealed in AI) or OpenVibe's. Actor runs
@@ -85,7 +85,7 @@ const runTokens = require('../server/identity/run-tokens');
@@ -85,7 +85,7 @@ const runTokens = require('../server/identity/run-tokens');
const actorGrant = principals.DEFAULT_GRANTS.find((g) => g[0] === 'actor' && g[1] === 'ai.run.create' && g[2] === 'openvibe.ai');
const actorGrant = principals.DEFAULT_GRANTS.find((g) => g[0] === 'actor' && g[1] === 'ai.run.create' && g[2] === 'openvibe.ai');
assert.deepStrictEqual(actorGrant[3], ['ai.complete'], 'AI\'s OpenAI-compatible workflow only');
assert.deepStrictEqual(actorGrant[3], ['ai.complete'], 'AI\'s OpenAI-compatible workflow only');
const actorGrants = await principals.grantsFor(db, 'actor', 'openvibe.ai');
const actorGrants = await principals.grantsFor(db, 'actor', 'openvibe.ai');
- assert.deepStrictEqual(actorGrants.map((g) => g.capability).sort(), ['ai.run.create']);
+ assert.deepStrictEqual(actorGrants.map((g) => g.capability).sort(), ['ai.run.create', 'ai.usage.report'], 'and it reports its own provider calls to AI\'s meter');
// ── A run token: the person it acts for, and how long it lives ──
// ── A run token: the person it acts for, and how long it lives ──
let t = await runToken(OWNER);
let t = await runToken(OWNER);