openvibers/openvibe.network Commits

Grants: ai.usage.report to Actor, Bot, Codes and Chat (contracts v0.421.0) (#898)

OP OpenVibers committed 10 h ago · 9f1b1a48f1 · 7 files, +30 −9

* Grants: ai.usage.report on OpenVibe.AI to Actor, Bot, Codes and Chat (contracts v0.421.0)

The products that make model calls without OpenVibe.AI report them to AI's POST /api/v1/usage/calls, and AI prices
and publishes each as ai.call.recorded@1. Actor and Bot send theirs from today; Codes and Chat are next.

* run-tokens test: Codes and Actor also hold ai.usage.report on AI

Changes

ddiff --git a/README.md b/README.md
iindex 66643bb3..f094a729 100644
---- a/README.md
++++ b/README.md
@@@ -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)).
<!-- versions:start -->
--- openvibe-contracts: v0.419.0
++- openvibe-contracts: v0.421.0
- openvibe-sdk: v0.161.0
- openvibe-shared: v4.232.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex 277484d4..e436be51 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.419.0",
++ "contracts": "openvibe-contracts v0.421.0",
"sdk": "openvibe-sdk v0.161.0",
"shared": "openvibe-shared v4.232.0",
"capabilities": [
ddiff --git a/package-lock.json b/package-lock.json
iindex 2ed16baa..9f1837ac 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -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",
ddiff --git a/package.json b/package.json
iindex e28ad0a4..31b6da24 100644
---- a/package.json
++++ b/package.json
@@@ -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",
ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex d79d2fee..dbd6b02d 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -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.
ddiff --git a/test/ai-usage-report-grant.test.js b/test/ai-usage-report-grant.test.js
nnew file mode 100644
iindex 00000000..8e1a3484
---- /dev/null
++++ b/test/ai-usage-report-grant.test.js
@@@ -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');
ddiff --git a/test/run-tokens.test.js b/test/run-tokens.test.js
iindex 3b019c7e..7528d13f 100644
---- a/test/run-tokens.test.js
++++ b/test/run-tokens.test.js
@@@ -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);