openvibers/openvibe.bot Commits

An ai command may carry tools (openvibe-contracts v0.277.0) (#283)

OP OpenVibers committed 11 days ago · 0297e06617 · 6 files, +24 −9

Bot validates commands against bot.command@1, so an agent's loop on a
person's own model (plan L55 C3) needs 0.277.0: tools, tool_choice, tool
messages and an assistant's tool_calls. The audit counts the tools offered
and keeps no names, arguments or results.

Changes

ddiff --git a/README.md b/README.md
iindex 03038c0..682d00d 100644
---- a/README.md
++++ b/README.md
@@@ -235,7 +235,7 @@ Set `BOT_ONVIF_CAMERAS` (JSON, unset by default) to the server-side cameras Bot
OpenVibe.Sites no longer generates `deploy/nginx/openvibe.bot.conf` (sites.json marks the vhost as owned by OpenVibe.Bot), and Bot serves nothing from a Sites checkout: the legal pages, `robots.txt`, `sitemap.xml`, `manifest.webmanifest` and the 404 page all come from the app. A Sites deploy leaves an installed vhost it no longer generates in place, so the current Sites vhost keeps serving openvibe.bot until Bot's first deploy installs this file over it; later Sites deploys never overwrite Bot's `/device` and `/control` routes.
<!-- versions:start -->
--- openvibe-contracts: v0.261.0
++- openvibe-contracts: v0.277.0
- openvibe-sdk: v0.81.0
- openvibe-shared: v4.105.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex 3d35ab0..55dbb7e 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -5,11 +5,11 @@
"runtime": "node 22, express 4, ws; PostgreSQL 18 via openvibe-sdk/db (pg, or embedded PGlite in tests); Valkey optional; no build step",
"port": 4630,
"domain": "openvibe.bot (www -> apex; /device and /control WebSockets; /install)",
-- "contracts": "openvibe-contracts v0.261.0",
++ "contracts": "openvibe-contracts v0.277.0",
"sdk": "openvibe-sdk v0.81.0",
"shared": "openvibe-shared v4.105.0",
"packages": [
-- "openvibe-contracts v0.261.0",
++ "openvibe-contracts v0.277.0",
"openvibe-sdk v0.81.0",
"openvibe-shared v4.105.0"
],
ddiff --git a/package-lock.json b/package-lock.json
iindex d9ef1bf..22ef163 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -13,7 +13,7 @@
"dotenv": "^16.4.7",
"express": "^4.21.2",
"iovalkey": "^0.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.261.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.277.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.105.0",
"pg": "^8.23.0",
@@@ -1120,9 +1120,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.261.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.261.0",
-- "integrity": "sha512-SiXOcXRL9+WivMkKM1W6sGNJMD4uCBR9o7KYZvs99hT2xgYxDvP4rM7tLBudGu1boPH+coMdUXVQAAWbS/3JDw==",
++ "version": "0.277.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.277.0",
++ "integrity": "sha512-GMODJIu4HUC5qWU2Ju8wft3aquLDBBN2VXLC34RjWw4NOVf/c3XndmN5wrv9yite0zv51ESwwmUFrpPnpcVs+w==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex bb97712..1dbebf5 100644
---- a/package.json
++++ b/package.json
@@@ -25,7 +25,7 @@
"dotenv": "^16.4.7",
"express": "^4.21.2",
"iovalkey": "^0.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.261.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.277.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.105.0",
"pg": "^8.23.0",
ddiff --git a/server/realtime.js b/server/realtime.js
iindex 33ca68e..01dd4df 100644
---- a/server/realtime.js
++++ b/server/realtime.js
@@@ -77,7 +77,8 @@ function computerSummary(kind, value) {
if (kind === 'desk') return { action: value.action, ...(typeof value.desk === 'string' ? { desk: value.desk } : {}), ...(typeof value.kind === 'string' ? { kind: value.kind } : {}), ...(typeof value.profile === 'string' ? { profile: value.profile } : {}), ...(Number.isInteger(value.width) && Number.isInteger(value.height) ? { width: value.width, height: value.height } : {}) };
if (kind === 'exec') return { program: String(value.argv[0]).slice(0, 120), args: value.argv.length - 1, ...where, ...(value.detach === true ? { detach: true } : {}) };
// A model's prompt is the person's own words: the audit keeps the model, how many messages and how long, never the text.
-- if (kind === 'ai') return { model: String(value.model).slice(0, 120), messages: value.messages.length, chars: value.messages.reduce((n, m) => n + String(m.content || '').length, 0) };
++ // With tools (contracts 0.277.0): how many were offered, never their names' arguments or the results sent back.
++ if (kind === 'ai') return { model: String(value.model).slice(0, 120), messages: value.messages.length, chars: value.messages.reduce((n, m) => n + String(m.content || '').length, 0), ...(Array.isArray(value.tools) ? { tools: value.tools.length } : {}) };
if (kind === 'fs') return { op: value.op, path: String(value.path).slice(0, 300), ...(typeof value.content_b64 === 'string' ? { bytes: Math.floor((value.content_b64.length * 3) / 4) } : {}) };
if (kind === 'probe') { try { const u = new URL(value.target); return { scheme: u.protocol.replace(':', ''), host: u.host }; } catch { return { scheme: null }; } }
// Typed text can be anything (a password the person asked for): the audit keeps its length, never the text.
ddiff --git a/test/machine-computer.test.js b/test/machine-computer.test.js
iindex 1426916..6d66ea4 100644
---- a/test/machine-computer.test.js
++++ b/test/machine-computer.test.js
@@@ -432,6 +432,20 @@ const { boot, check, done } = require('./helpers/app');
const rows = await t.db.many('SELECT value FROM command_audit WHERE robot_id = $1 AND kind = $2 AND result = $3', [gpu.deviceId, 'ai', 'ack']);
assert.deepStrictEqual(rows[0].value, { model: 'llama3.1:8b', messages: 1, chars: 29 });
assert.ok(!JSON.stringify(rows).includes('secret words'), 'never the text');
++ // Tools (contracts 0.277.0): an agent's loop on this machine's model. The command carries the tools and the
++ // calls already made; the audit counts the tools and keeps no names, arguments or results.
++ const withTools = { kind: 'ai', value: { model: 'llama3.1:8b', tools: [{ type: 'function', function: { name: 'web_search', parameters: { type: 'object' } } }], tool_choice: 'auto', messages: [
++ { role: 'user', content: 'Weather in Lisbon?' },
++ { role: 'assistant', content: '', tool_calls: [{ id: 'call_1', type: 'function', function: { name: 'web_search', arguments: '{"q":"secret query"}' } }] },
++ { role: 'tool', tool_call_id: 'call_1', content: 'secret result' },
++ ] } };
++ const toolAsk = await cmd(gpu.deviceId, withTools, asAi(alex.subject));
++ assert.strictEqual(toolAsk.json.result, 'ack', toolAsk.text);
++ const toolRow = (await t.db.many('SELECT value FROM command_audit WHERE robot_id = $1 AND kind = $2 ORDER BY id DESC LIMIT 1', [gpu.deviceId, 'ai']))[0];
++ assert.deepStrictEqual(toolRow.value, { model: 'llama3.1:8b', messages: 3, chars: 31, tools: 1 });
++ assert.ok(!/secret (query|result)|web_search/.test(JSON.stringify(toolRow)), 'no tool names, arguments or results in the audit');
++ const orphan = await cmd(gpu.deviceId, { kind: 'ai', value: { model: 'llama3.1:8b', messages: [{ role: 'tool', content: 'x' }] } }, asAi(alex.subject));
++ assert.strictEqual(orphan.status, 422, 'a tool message needs the call it answers');
// A real Node reports the ai plugin under its own name, and a machine may run it with no computer plugin.
const own = await machine(alex, null);
own.c.send(frame('status', { capabilities: { ai: { ai: { models: ['qwen2.5-1.5b-instruct'], ready: true, max_tokens: 256 } } }, drivers: [], faults: [] }));