openvibers/openvibe.actor Commits
Hand it to me reaches the person wherever they are: hand_to_person sends one Network notification (Your agent needs you, the step, the link to take the desk over), best effort (#146)
Changes
@@@ -2,6 +2,7 @@const crypto = require('crypto');const looks = require('./looks');++const { createNotifier } = require('../notify/client');/*** What the OpenVibe runtime can do, as model tools — every one of them an OpenVibe service's public API, called the@@@ -53,8 +54,10 @@ const json = (v) => clip(JSON.stringify(v));/** The person a task is for, when it is a signed-in person ('user:usr_…' → 'usr_…'), else null. */const personOf = (requester) => (typeof requester === 'string' && requester.startsWith('user:') ? requester.slice(5) : null);--function createOpenVibeTools({ config, fetchImpl = globalThis.fetch, now = () => Date.now(), log = console, robots = null, computers = null, vision = null }) {++function createOpenVibeTools({ config, fetchImpl = globalThis.fetch, now = () => Date.now(), log = console, robots = null, computers = null, vision = null, notifier = null }) {const { searchUrl, toolsUrl } = config.services;++ // Desks plan D4: "hand it to me" reaches the person through Network's notifications (server/notify/client.js).++ const notify = notifier || createNotifier({ config, fetchImpl, log });const bot = robots || createRobots({ config, fetchImpl });const pcs = computers || createComputers({ config, fetchImpl });let catalog = null;@@@ -237,6 +240,9 @@ function createOpenVibeTools({ config, fetchImpl = globalThis.fetch, now = () =>const granted = waitForPerson ? waitForPerson(PERSON_WAIT_MS) : 0;if (!granted) return { done: false, note: 'This task has already waited for the person as long as it may: tell them in your answer what is left for them to do, with the link.', watch };if (emit) await emit({ step: 'text', text: `Needs you: ${what} Open your agent's desk, take it over, do it, then let go: ${watch}` });++ // Wherever the person is (Network's bell, and a push when they allowed it), with the link to the desk. Best++ // effort, never awaited by the wait below.++ if (notify.enabled) notify.needsYou({ subject: person, what, url: watch }).catch(() => {});const started = Date.now();const end = started + granted;let took = false;nnew file mode 100644@@@ -0,0 +1,66 @@++'use strict';++/**++ * OpenVibe.Network notifications for the person a task is for (Desks plan D4, "hand it to me"): when the agent needs++ * them for a step on its desk, they hear it wherever they are (the bell on every OpenVibe site, and a push when they++ * allowed it), with the link to take the desk over. The task's owner (a usr_ subject) becomes Network's user id++ * (GET /internal/identity/resolve, capability identity.subject.resolve), then one notification is pushed as app actor++ * (POST /internal/notifications/push, capability network.notifications.push). Each with a token of its own, audience++ * openvibe.network.++ *++ * Best effort: off without a client secret, never retried (Network keeps no idempotency record for a push, and a person++ * told twice is worse than one told once), and a failure never stops a task. → { sent, reason? }++ */++const { serviceAuth } = require('openvibe-contracts');++++const SUBJECT = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;++++function createNotifier({ config, fetchImpl = globalThis.fetch, tokens: tokensOverride = null, log = console }) {++ const base = config.networkInternalUrl;++ const enabled = !!(base && (tokensOverride || (config.oauth && config.oauth.clientSecret)));++ const tokenFor = (scope) => tokensOverride || serviceAuth.createTokenClient({++ tokenUrl: `${base}/oauth/token`, clientId: config.oauth.clientId, clientSecret: config.oauth.clientSecret,++ audience: 'openvibe.network', scope, fetchImpl,++ });++ const resolveTokens = enabled ? tokenFor('identity.subject.resolve') : null;++ const pushTokens = enabled ? tokenFor('network.notifications.push') : null;++++ async function networkUserId(subject) {++ const res = await fetchImpl(`${base}/internal/identity/resolve?subject_id=${encodeURIComponent(subject)}`, {++ headers: { Accept: 'application/json', ...(await resolveTokens.authHeaders()) }, signal: AbortSignal.timeout(10_000),++ });++ if (res.status === 404) return null;++ if (!res.ok) throw new Error(`Network answered ${res.status} to resolve`);++ const who = await res.json();++ return who && !who.deleted && who.network_user_id != null ? who.network_user_id : null;++ }++++ /** The agent needs its person for one step on its desk: `what`, the task's `title` and the desk's `url`. */++ async function needsYou({ subject, what, title = null, url }) {++ if (!enabled) return { sent: false, reason: 'off' };++ if (!SUBJECT.test(String(subject || '')) || !/^https:\/\//.test(String(url || ''))) return { sent: false, reason: 'bad_input' };++ try {++ const userId = await networkUserId(subject);++ if (userId == null) return { sent: false, reason: 'unknown_subject' };++ const message = `${String(what || 'One step only you can do.').slice(0, 300)}${title ? ` (${String(title).slice(0, 120)})` : ''}`;++ const res = await fetchImpl(`${base}/internal/notifications/push`, {++ method: 'POST',++ headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(await pushTokens.authHeaders()) },++ body: JSON.stringify({++ user_id: userId, type: 'AGENT_NEEDS_YOU', category: 'service', priority: 'high', icon: '🙋',++ title: 'Your agent needs you', message, url, service: 'actor',++ }),++ signal: AbortSignal.timeout(10_000),++ });++ if (!res.ok) return { sent: false, reason: `network_${res.status}` };++ const out = await res.json().catch(() => null);++ return { sent: true, ...(out && out.skipped ? { skipped: true } : {}) };++ } catch (err) {++ log.warn && log.warn(`[notify] could not tell the person: ${(err && err.message) || err}`);++ return { sent: false, reason: 'error' };++ }++ }++++ return { enabled, needsYou };++}++++module.exports = { createNotifier };@@@ -40,7 +40,10 @@ function botWith(turns) {};return { asked, fetchImpl };}--const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl, robots: createRobots({ config, fetchImpl: bot.fetchImpl, tokens }), computers: createComputers({ config, fetchImpl: bot.fetchImpl, tokens }) });++// Network notifications (Desks plan D4): recorded, never sent from these tests.++const told = [];++const notifier = { enabled: true, needsYou: async (n) => { told.push(n); return { sent: true }; } };++const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl, robots: createRobots({ config, fetchImpl: bot.fetchImpl, tokens }), computers: createComputers({ config, fetchImpl: bot.fetchImpl, tokens }), notifier });(async () => {await check('hand_to_person: the person takes the desk and lets go, and the agent goes on (looking again first)', async () => {@@@ -66,6 +69,7 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImplassert.strictEqual(said[0].step, 'text');assert.match(said[0].text, new RegExp(`^Needs you: Sign in to your Google account in the browser\\. .*https://openvibe\\.bot/machines/${PC}/screen\\?desk=${run.name}$`));assert.ok(bot.asked.every((d) => d === run.name), 'it watched the task\'s own desk');++ assert.deepStrictEqual(told.at(-1), { subject: ME, what: 'Sign in to your Google account in the browser.', url: out.watch }, 'the person is told wherever they are, with the link');});await check('hand_to_person: nobody takes it, the clock may not wait, or there is nothing to do', async () => {nnew file mode 100644@@@ -0,0 +1,39 @@++'use strict';++// Network notifications for "hand it to me" (Desks plan D4, server/notify/client.js): the task's owner is resolved to++// Network's user id, then one notification is pushed as app actor with the link to the desk; never retried, never thrown.++const assert = require('assert');++const { createNotifier } = require('../server/notify/client');++const { check, done } = require('./helpers/boot');++++const ME = 'usr_01M4W5JBFEG3QG9ER8W1A2K0RX';++const config = { networkInternalUrl: 'http://network.test', oauth: { clientId: 'actor', clientSecret: 'actor-secret' } };++const tokens = { authHeaders: async () => ({ Authorization: 'Bearer t' }) };++const reply = (status, body) => new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });++++(async () => {++ await check('resolve, then one push as app actor with the desk link; a person Network does not know is not told', async () => {++ const calls = [];++ const fetchImpl = async (url, init = {}) => {++ calls.push({ url: String(url), method: init.method || 'GET', body: init.body ? JSON.parse(init.body) : null });++ if (String(url).includes('/internal/identity/resolve')) return String(url).includes(ME) ? reply(200, { network_user_id: 42 }) : reply(404, { code: 'identity.subject_not_found' });++ return reply(200, { ok: true });++ };++ const n = createNotifier({ config, fetchImpl, tokens, log: { warn() {} } });++ const url = 'https://openvibe.bot/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-0123456789';++ assert.deepStrictEqual(await n.needsYou({ subject: ME, what: 'Sign in to the airline.', url }), { sent: true });++ assert.strictEqual(calls[0].url, `http://network.test/internal/identity/resolve?subject_id=${ME}`);++ assert.deepStrictEqual(calls[1].body, { user_id: 42, type: 'AGENT_NEEDS_YOU', category: 'service', priority: 'high', icon: '🙋', title: 'Your agent needs you', message: 'Sign in to the airline.', url, service: 'actor' });++ assert.deepStrictEqual(await n.needsYou({ subject: 'usr_01M4W5JBFEG3QG9ER8W1A2K0RY', what: 'x', url }), { sent: false, reason: 'unknown_subject' });++ assert.strictEqual(calls.filter((c) => c.method === 'POST').length, 1, 'only the known person was told');++ assert.deepStrictEqual(await n.needsYou({ subject: ME, what: 'x', url: 'http://evil.example/' }), { sent: false, reason: 'bad_input' }, 'only an https link');++ });++++ await check('a failure is an answer, never a throw; off without a client secret', async () => {++ const n = createNotifier({ config, fetchImpl: async () => { throw new Error('down'); }, tokens, log: { warn() {} } });++ assert.deepStrictEqual(await n.needsYou({ subject: ME, what: 'x', url: 'https://openvibe.bot/x' }), { sent: false, reason: 'error' });++ const off = createNotifier({ config: { networkInternalUrl: 'http://network.test', oauth: { clientId: 'actor', clientSecret: '' } } });++ assert.strictEqual(off.enabled, false);++ assert.deepStrictEqual(await off.needsYou({ subject: ME, what: 'x', url: 'https://openvibe.bot/x' }), { sent: false, reason: 'off' });++ });++ done();++})();@@@ -109,6 +109,7 @@ const SAME = { 'sec-fetch-site': 'same-origin' };'server/agents/codes.js': 'the configured OpenVibe.Codes base (ACTOR_CODES_URL) with Actor\'s own service token; the subject must be usr_<ULID> and every task id tsk_<ULID> before it goes in a path; the repository named in the task is data for the person\'s own machine, never fetched here (test/codes.test.js)','server/agents/computers.js': 'the configured OpenVibe.Bot base (ACTOR_BOT_URL) with Actor\'s own service token (bot.computer.control); the machine id must be dev_<ULID> and the subject usr_<ULID> before anything is sent; a path or program is data for the person\'s own machine, never fetched here (test/computers.test.js)','server/chat/client.js': 'the configured OpenVibe.Chat base (ACTOR_CHAT_URL, loopback) with Actor\'s own service token; the room and the message it answers come from a signed chat.message.created, never from the task (test/chat-asks.test.js)',++ 'server/notify/client.js': 'the configured OpenVibe.Network internal base (OV_NETWORK_INTERNAL_URL, loopback) with Actor\'s own service tokens: identity resolve and one notification push; the subject is the task\'s owner and the link is the desk page Actor built, never a URL from the task (test/notify-client.test.js)','server/agents/robots.js': 'the configured OpenVibe.Bot base (ACTOR_BOT_URL) with Actor\'s own service token; the robot id must be rob_<ULID> and the subject usr_<ULID> before anything is sent (test/robots.test.js)','server/identity/projects.js': 'Network\'s internal membership read (configured networkInternalUrl) with Actor\'s own service token (network.project.member.read); the project and subject ids are path-encoded and come from a parsed OVRN and a checked usr_ id (test/resource-control.test.js)','server/agents/tools.js': 'the configured OpenVibe.Tools and OpenVibe.Search bases; ids come from Tools\' catalog, a page URL goes to Tools as input',
@@ -2,6 +2,7 @@
@@ -2,6 +2,7 @@
const crypto = require('crypto');
const crypto = require('crypto');
const looks = require('./looks');
const looks = require('./looks');
+const { createNotifier } = require('../notify/client');
/**
/**
* What the OpenVibe runtime can do, as model tools — every one of them an OpenVibe service's public API, called the
* What the OpenVibe runtime can do, as model tools — every one of them an OpenVibe service's public API, called the
@@ -53,8 +54,10 @@ const json = (v) => clip(JSON.stringify(v));
@@ -53,8 +54,10 @@ const json = (v) => clip(JSON.stringify(v));
/** The person a task is for, when it is a signed-in person ('user:usr_…' → 'usr_…'), else null. */
/** The person a task is for, when it is a signed-in person ('user:usr_…' → 'usr_…'), else null. */
const personOf = (requester) => (typeof requester === 'string' && requester.startsWith('user:') ? requester.slice(5) : null);
const personOf = (requester) => (typeof requester === 'string' && requester.startsWith('user:') ? requester.slice(5) : null);
-function createOpenVibeTools({ config, fetchImpl = globalThis.fetch, now = () => Date.now(), log = console, robots = null, computers = null, vision = null }) {
+function createOpenVibeTools({ config, fetchImpl = globalThis.fetch, now = () => Date.now(), log = console, robots = null, computers = null, vision = null, notifier = null }) {
const { searchUrl, toolsUrl } = config.services;
const { searchUrl, toolsUrl } = config.services;
+ // Desks plan D4: "hand it to me" reaches the person through Network's notifications (server/notify/client.js).
+ const notify = notifier || createNotifier({ config, fetchImpl, log });
const bot = robots || createRobots({ config, fetchImpl });
const bot = robots || createRobots({ config, fetchImpl });
const pcs = computers || createComputers({ config, fetchImpl });
const pcs = computers || createComputers({ config, fetchImpl });
let catalog = null;
let catalog = null;
@@ -237,6 +240,9 @@ function createOpenVibeTools({ config, fetchImpl = globalThis.fetch, now = () =>
@@ -237,6 +240,9 @@ function createOpenVibeTools({ config, fetchImpl = globalThis.fetch, now = () =>
const granted = waitForPerson ? waitForPerson(PERSON_WAIT_MS) : 0;
const granted = waitForPerson ? waitForPerson(PERSON_WAIT_MS) : 0;
if (!granted) return { done: false, note: 'This task has already waited for the person as long as it may: tell them in your answer what is left for them to do, with the link.', watch };
if (!granted) return { done: false, note: 'This task has already waited for the person as long as it may: tell them in your answer what is left for them to do, with the link.', watch };
if (emit) await emit({ step: 'text', text: `Needs you: ${what} Open your agent's desk, take it over, do it, then let go: ${watch}` });
if (emit) await emit({ step: 'text', text: `Needs you: ${what} Open your agent's desk, take it over, do it, then let go: ${watch}` });
+ // Wherever the person is (Network's bell, and a push when they allowed it), with the link to the desk. Best
+ // effort, never awaited by the wait below.
+ if (notify.enabled) notify.needsYou({ subject: person, what, url: watch }).catch(() => {});
const started = Date.now();
const started = Date.now();
const end = started + granted;
const end = started + granted;
let took = false;
let took = false;
new file mode 100644
new file mode 100644
@@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
+'use strict';
+/**
+ * OpenVibe.Network notifications for the person a task is for (Desks plan D4, "hand it to me"): when the agent needs
+ * them for a step on its desk, they hear it wherever they are (the bell on every OpenVibe site, and a push when they
+ * allowed it), with the link to take the desk over. The task's owner (a usr_ subject) becomes Network's user id
+ * (GET /internal/identity/resolve, capability identity.subject.resolve), then one notification is pushed as app actor
+ * (POST /internal/notifications/push, capability network.notifications.push). Each with a token of its own, audience
+ * openvibe.network.
+ *
+ * Best effort: off without a client secret, never retried (Network keeps no idempotency record for a push, and a person
+ * told twice is worse than one told once), and a failure never stops a task. → { sent, reason? }
+ */
+const { serviceAuth } = require('openvibe-contracts');
+
+const SUBJECT = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
+
+function createNotifier({ config, fetchImpl = globalThis.fetch, tokens: tokensOverride = null, log = console }) {
+ const base = config.networkInternalUrl;
+ const enabled = !!(base && (tokensOverride || (config.oauth && config.oauth.clientSecret)));
+ const tokenFor = (scope) => tokensOverride || serviceAuth.createTokenClient({
+ tokenUrl: `${base}/oauth/token`, clientId: config.oauth.clientId, clientSecret: config.oauth.clientSecret,
+ audience: 'openvibe.network', scope, fetchImpl,
+ });
+ const resolveTokens = enabled ? tokenFor('identity.subject.resolve') : null;
+ const pushTokens = enabled ? tokenFor('network.notifications.push') : null;
+
+ async function networkUserId(subject) {
+ const res = await fetchImpl(`${base}/internal/identity/resolve?subject_id=${encodeURIComponent(subject)}`, {
+ headers: { Accept: 'application/json', ...(await resolveTokens.authHeaders()) }, signal: AbortSignal.timeout(10_000),
+ });
+ if (res.status === 404) return null;
+ if (!res.ok) throw new Error(`Network answered ${res.status} to resolve`);
+ const who = await res.json();
+ return who && !who.deleted && who.network_user_id != null ? who.network_user_id : null;
+ }
+
+ /** The agent needs its person for one step on its desk: `what`, the task's `title` and the desk's `url`. */
+ async function needsYou({ subject, what, title = null, url }) {
+ if (!enabled) return { sent: false, reason: 'off' };
+ if (!SUBJECT.test(String(subject || '')) || !/^https:\/\//.test(String(url || ''))) return { sent: false, reason: 'bad_input' };
+ try {
+ const userId = await networkUserId(subject);
+ if (userId == null) return { sent: false, reason: 'unknown_subject' };
+ const message = `${String(what || 'One step only you can do.').slice(0, 300)}${title ? ` (${String(title).slice(0, 120)})` : ''}`;
+ const res = await fetchImpl(`${base}/internal/notifications/push`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(await pushTokens.authHeaders()) },
+ body: JSON.stringify({
+ user_id: userId, type: 'AGENT_NEEDS_YOU', category: 'service', priority: 'high', icon: '🙋',
+ title: 'Your agent needs you', message, url, service: 'actor',
+ }),
+ signal: AbortSignal.timeout(10_000),
+ });
+ if (!res.ok) return { sent: false, reason: `network_${res.status}` };
+ const out = await res.json().catch(() => null);
+ return { sent: true, ...(out && out.skipped ? { skipped: true } : {}) };
+ } catch (err) {
+ log.warn && log.warn(`[notify] could not tell the person: ${(err && err.message) || err}`);
+ return { sent: false, reason: 'error' };
+ }
+ }
+
+ return { enabled, needsYou };
+}
+
+module.exports = { createNotifier };
@@ -40,7 +40,10 @@ function botWith(turns) {
@@ -40,7 +40,10 @@ function botWith(turns) {
};
};
return { asked, fetchImpl };
return { asked, fetchImpl };
}
}
-const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl, robots: createRobots({ config, fetchImpl: bot.fetchImpl, tokens }), computers: createComputers({ config, fetchImpl: bot.fetchImpl, tokens }) });
+// Network notifications (Desks plan D4): recorded, never sent from these tests.
+const told = [];
+const notifier = { enabled: true, needsYou: async (n) => { told.push(n); return { sent: true }; } };
+const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl, robots: createRobots({ config, fetchImpl: bot.fetchImpl, tokens }), computers: createComputers({ config, fetchImpl: bot.fetchImpl, tokens }), notifier });
(async () => {
(async () => {
await check('hand_to_person: the person takes the desk and lets go, and the agent goes on (looking again first)', async () => {
await check('hand_to_person: the person takes the desk and lets go, and the agent goes on (looking again first)', async () => {
@@ -66,6 +69,7 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
@@ -66,6 +69,7 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
assert.strictEqual(said[0].step, 'text');
assert.strictEqual(said[0].step, 'text');
assert.match(said[0].text, new RegExp(`^Needs you: Sign in to your Google account in the browser\\. .*https://openvibe\\.bot/machines/${PC}/screen\\?desk=${run.name}$`));
assert.match(said[0].text, new RegExp(`^Needs you: Sign in to your Google account in the browser\\. .*https://openvibe\\.bot/machines/${PC}/screen\\?desk=${run.name}$`));
assert.ok(bot.asked.every((d) => d === run.name), 'it watched the task\'s own desk');
assert.ok(bot.asked.every((d) => d === run.name), 'it watched the task\'s own desk');
+ assert.deepStrictEqual(told.at(-1), { subject: ME, what: 'Sign in to your Google account in the browser.', url: out.watch }, 'the person is told wherever they are, with the link');
});
});
await check('hand_to_person: nobody takes it, the clock may not wait, or there is nothing to do', async () => {
await check('hand_to_person: nobody takes it, the clock may not wait, or there is nothing to do', async () => {
new file mode 100644
new file mode 100644
@@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
+'use strict';
+// Network notifications for "hand it to me" (Desks plan D4, server/notify/client.js): the task's owner is resolved to
+// Network's user id, then one notification is pushed as app actor with the link to the desk; never retried, never thrown.
+const assert = require('assert');
+const { createNotifier } = require('../server/notify/client');
+const { check, done } = require('./helpers/boot');
+
+const ME = 'usr_01M4W5JBFEG3QG9ER8W1A2K0RX';
+const config = { networkInternalUrl: 'http://network.test', oauth: { clientId: 'actor', clientSecret: 'actor-secret' } };
+const tokens = { authHeaders: async () => ({ Authorization: 'Bearer t' }) };
+const reply = (status, body) => new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
+
+(async () => {
+ await check('resolve, then one push as app actor with the desk link; a person Network does not know is not told', async () => {
+ const calls = [];
+ const fetchImpl = async (url, init = {}) => {
+ calls.push({ url: String(url), method: init.method || 'GET', body: init.body ? JSON.parse(init.body) : null });
+ if (String(url).includes('/internal/identity/resolve')) return String(url).includes(ME) ? reply(200, { network_user_id: 42 }) : reply(404, { code: 'identity.subject_not_found' });
+ return reply(200, { ok: true });
+ };
+ const n = createNotifier({ config, fetchImpl, tokens, log: { warn() {} } });
+ const url = 'https://openvibe.bot/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-0123456789';
+ assert.deepStrictEqual(await n.needsYou({ subject: ME, what: 'Sign in to the airline.', url }), { sent: true });
+ assert.strictEqual(calls[0].url, `http://network.test/internal/identity/resolve?subject_id=${ME}`);
+ assert.deepStrictEqual(calls[1].body, { user_id: 42, type: 'AGENT_NEEDS_YOU', category: 'service', priority: 'high', icon: '🙋', title: 'Your agent needs you', message: 'Sign in to the airline.', url, service: 'actor' });
+ assert.deepStrictEqual(await n.needsYou({ subject: 'usr_01M4W5JBFEG3QG9ER8W1A2K0RY', what: 'x', url }), { sent: false, reason: 'unknown_subject' });
+ assert.strictEqual(calls.filter((c) => c.method === 'POST').length, 1, 'only the known person was told');
+ assert.deepStrictEqual(await n.needsYou({ subject: ME, what: 'x', url: 'http://evil.example/' }), { sent: false, reason: 'bad_input' }, 'only an https link');
+ });
+
+ await check('a failure is an answer, never a throw; off without a client secret', async () => {
+ const n = createNotifier({ config, fetchImpl: async () => { throw new Error('down'); }, tokens, log: { warn() {} } });
+ assert.deepStrictEqual(await n.needsYou({ subject: ME, what: 'x', url: 'https://openvibe.bot/x' }), { sent: false, reason: 'error' });
+ const off = createNotifier({ config: { networkInternalUrl: 'http://network.test', oauth: { clientId: 'actor', clientSecret: '' } } });
+ assert.strictEqual(off.enabled, false);
+ assert.deepStrictEqual(await off.needsYou({ subject: ME, what: 'x', url: 'https://openvibe.bot/x' }), { sent: false, reason: 'off' });
+ });
+ done();
+})();
@@ -109,6 +109,7 @@ const SAME = { 'sec-fetch-site': 'same-origin' };
@@ -109,6 +109,7 @@ const SAME = { 'sec-fetch-site': 'same-origin' };
'server/agents/codes.js': 'the configured OpenVibe.Codes base (ACTOR_CODES_URL) with Actor\'s own service token; the subject must be usr_<ULID> and every task id tsk_<ULID> before it goes in a path; the repository named in the task is data for the person\'s own machine, never fetched here (test/codes.test.js)',
'server/agents/codes.js': 'the configured OpenVibe.Codes base (ACTOR_CODES_URL) with Actor\'s own service token; the subject must be usr_<ULID> and every task id tsk_<ULID> before it goes in a path; the repository named in the task is data for the person\'s own machine, never fetched here (test/codes.test.js)',
'server/agents/computers.js': 'the configured OpenVibe.Bot base (ACTOR_BOT_URL) with Actor\'s own service token (bot.computer.control); the machine id must be dev_<ULID> and the subject usr_<ULID> before anything is sent; a path or program is data for the person\'s own machine, never fetched here (test/computers.test.js)',
'server/agents/computers.js': 'the configured OpenVibe.Bot base (ACTOR_BOT_URL) with Actor\'s own service token (bot.computer.control); the machine id must be dev_<ULID> and the subject usr_<ULID> before anything is sent; a path or program is data for the person\'s own machine, never fetched here (test/computers.test.js)',
'server/chat/client.js': 'the configured OpenVibe.Chat base (ACTOR_CHAT_URL, loopback) with Actor\'s own service token; the room and the message it answers come from a signed chat.message.created, never from the task (test/chat-asks.test.js)',
'server/chat/client.js': 'the configured OpenVibe.Chat base (ACTOR_CHAT_URL, loopback) with Actor\'s own service token; the room and the message it answers come from a signed chat.message.created, never from the task (test/chat-asks.test.js)',
+ 'server/notify/client.js': 'the configured OpenVibe.Network internal base (OV_NETWORK_INTERNAL_URL, loopback) with Actor\'s own service tokens: identity resolve and one notification push; the subject is the task\'s owner and the link is the desk page Actor built, never a URL from the task (test/notify-client.test.js)',
'server/agents/robots.js': 'the configured OpenVibe.Bot base (ACTOR_BOT_URL) with Actor\'s own service token; the robot id must be rob_<ULID> and the subject usr_<ULID> before anything is sent (test/robots.test.js)',
'server/agents/robots.js': 'the configured OpenVibe.Bot base (ACTOR_BOT_URL) with Actor\'s own service token; the robot id must be rob_<ULID> and the subject usr_<ULID> before anything is sent (test/robots.test.js)',
'server/identity/projects.js': 'Network\'s internal membership read (configured networkInternalUrl) with Actor\'s own service token (network.project.member.read); the project and subject ids are path-encoded and come from a parsed OVRN and a checked usr_ id (test/resource-control.test.js)',
'server/identity/projects.js': 'Network\'s internal membership read (configured networkInternalUrl) with Actor\'s own service token (network.project.member.read); the project and subject ids are path-encoded and come from a parsed OVRN and a checked usr_ id (test/resource-control.test.js)',
'server/agents/tools.js': 'the configured OpenVibe.Tools and OpenVibe.Search bases; ids come from Tools\' catalog, a page URL goes to Tools as input',
'server/agents/tools.js': 'the configured OpenVibe.Tools and OpenVibe.Search bases; ids come from Tools\' catalog, a page URL goes to Tools as input',