openvibers/openvibe.codes Commits
Agents: your AI keys from the one place, Your AI (#212)
Changes
@@@ -18,6 +18,12 @@* put(subject, provider, body) → the connection as AI stored it (the hint, never the key)* remove(subject, provider) → true when there was one* test(subject, provider) → { ok, detail }: one tiny request through AI, or why not++ * mine(token) → the person's own keys in OpenVibe.AI (their "Your AI" page, L36), read with++ * their own token: { credentials: [{ provider, hint, status, … }] }++ *++ * Since 2026-10-31 a person keeps their keys in one place, ai.openvibe.services/you, for every site and agent; Codes'++ * tasks run on the person's behalf, so OpenVibe.AI uses those keys first. The keys stored here before stay usable++ * until the person removes them.*/const { createServiceTokenClient } = require('openvibe-sdk/auth');@@@ -165,7 +171,21 @@ function createConnections({ config, fetchImpl = globalThis.fetch, log = consolereturn keys.map((k) => k.provider);}-- return { list, put, remove, test, providers, present, PROVIDERS };++ /** The person's own keys in OpenVibe.AI (GET /api/v1/me/credentials with their own token): hints and states only. */++ async function mine(token) {++ if (!token) throw Object.assign(new Error('the person is not signed in'), { status: 401, code: 'token.missing' });++ let res;++ try {++ res = await fetchImpl(`${base}/api/v1/me/credentials`, { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } });++ } catch (e) {++ throw Object.assign(new Error('OpenVibe.AI did not answer'), { status: 502, code: 'ai.unavailable' });++ }++ const body = await res.json().catch(() => ({}));++ if (!res.ok) throw Object.assign(new Error((body && (body.detail || body.title)) || `OpenVibe.AI answered ${res.status}`), { status: res.status, code: (body && body.code) || 'ai.error' });++ return { credentials: Array.isArray(body.credentials) ? body.credentials : [] };++ }++++ return { list, put, remove, test, mine, providers, present, PROVIDERS };}module.exports = { createConnections, PROVIDERS, ANTHROPIC_FORMAT, providerOf, present };@@@ -64,7 +64,9 @@ const asObject = (v) => {if (typeof v === 'string') { try { const p = JSON.parse(v); return p && typeof p === 'object' && !Array.isArray(p) ? p : null; } catch { return null; } }return null;};--const PROVIDER_NAMES = { anthropic: 'Anthropic', openai: 'OpenAI', deepseek: 'DeepSeek', minimax: 'MiniMax', openrouter: 'OpenRouter', custom: 'Custom (OpenAI-compatible)' };++const PROVIDER_NAMES = { anthropic: 'Anthropic', openai: 'OpenAI', gemini: 'Google Gemini', deepseek: 'DeepSeek', mistral: 'Mistral', xai: 'xAI', groq: 'Groq', cerebras: 'Cerebras', together: 'Together AI', fireworks: 'Fireworks', minimax: 'MiniMax', openrouter: 'OpenRouter', custom: 'Custom (OpenAI-compatible)' };++// A person's keys live in one place for every site and agent (plan L36, L39): Codes shows them and links there.++const YOUR_AI = 'https://ai.openvibe.services/you';function createC4Routes(ctx) {const r = asyncRouter();@@@ -556,33 +558,29 @@ ${list ? `<ul class="c4-rows c4-fleet">${list}</ul>` : `<div class="c4-emptyline};// ── Agents: the person's connections (keys live in OpenVibe.AI) ────────────────────────────────────────────-- const connectionsCard = async (req) => {++ const connectionsCard = async (req, res = null) => {let list = { keys: [], plans: [] };++ let mine = null;let problem = null;try { list = await connections.list(req.viewer.subject); } catch (e) {problem = { status: e && e.status, detail: (e && e.message) || 'the connections could not be read' };}-- const now = Date.now();-- const rows = (list.keys || []).map((k) => `<li class="c4-row c4-row-flat"><span class="c4-row-dot c4-dot-ok" aria-hidden="true"></span><span class="c4-row-label">${esc(PROVIDER_NAMES[k.provider] || k.provider)}</span><span class="c4-row-note">${esc(k.hint || 'key stored')}${k.budget_usd_per_day != null ? ` · cap $${Number(k.budget_usd_per_day).toFixed(2)} a day` : ''}${k.last_used_at ? ` · last used ${esc(age(k.last_used_at))}` : ''}</span><span class="c4-inline-actions"><form method="post" action="/agents/connections/${esc(k.provider)}/test"><button class="c4-opill" type="submit">Test</button></form><form method="post" action="/agents/connections/${esc(k.provider)}/remove"><button class="c4-opill c4-opill-quiet" type="submit">Remove</button></form></span></li>`).join('');++ try { mine = await sso.asViewer(req, res, (token) => connections.mine(token)); } catch (e) { mine = null; }++ const state = (k) => (k.status === 'ok' ? pill('Working', 'ok') : k.status === 'failing' ? pill('Not working', 'bad') : pill('Not tested', 'skip'));++ const own = ((mine && mine.credentials) || []).map((k) => `<li class="c4-row c4-row-flat"><span class="c4-row-dot${k.status === 'ok' ? ' c4-dot-ok' : k.status === 'failing' ? ' c4-dot-bad' : ''}" aria-hidden="true"></span><span class="c4-row-label">${esc(PROVIDER_NAMES[k.provider] || k.provider)}</span><span class="c4-row-note">\u2026${esc(k.hint || '????')}${k.checked_at ? ` · checked ${esc(age(k.checked_at))}` : ''}</span>${state(k)}</li>`).join('');++ const before = (list.keys || []).map((k) => `<li class="c4-row c4-row-flat"><span class="c4-row-dot c4-dot-skip" aria-hidden="true"></span><span class="c4-row-label">${esc(PROVIDER_NAMES[k.provider] || k.provider)}</span><span class="c4-row-note">${esc(k.hint || 'key stored')}${k.last_used_at ? ` · last used ${esc(age(k.last_used_at))}` : ''}</span><span class="c4-inline-actions"><form method="post" action="/agents/connections/${esc(k.provider)}/test"><button class="c4-opill" type="submit">Test</button></form><form method="post" action="/agents/connections/${esc(k.provider)}/remove"><button class="c4-opill c4-opill-quiet" type="submit">Remove</button></form></span></li>`).join('');const plans = (list.plans || []).map((p) => `<li class="c4-row c4-row-flat"><span class="c4-row-dot c4-dot-skip" aria-hidden="true"></span><span class="c4-row-label">${esc(p.label || p.provider || 'a plan')}</span><span class="c4-row-note">on ${esc(p.machine || 'your machine')}</span>${pill('On your machine', 'skip')}</li>`).join('');-- return `<section class="c4-card" id="connections"><div class="c4-card-head"><h2>Connections</h2></div>--<p class="c4-sub">Keys live in OpenVibe.AI, never here</p>--${problem ? note(`Your connections could not be read: ${esc(problem.detail)}`, 'bad') : ''}--${rows || plans ? `<ul class="c4-rows">${rows}${plans}</ul>` : `<div class="c4-emptyline">No provider keys yet. Add one below and the router can use it, and so can a task.</div>`}--<h3 class="c4-subhead">Add or replace a key</h3>--<form class="c4-form" method="post" action="/agents/connections">-- <label>Provider <select name="provider">${connections.PROVIDERS.map((p) => `<option value="${esc(p)}">${esc(PROVIDER_NAMES[p] || p)}</option>`).join('')}</select></label>-- <label>Key <input type="password" name="key" required minlength="8" maxlength="400" autocomplete="off" spellcheck="false" placeholder="sk-…"></label>-- <label>Base URL <input type="url" name="base_url" placeholder="Only for a custom provider" autocomplete="off"></label>-- <label>Daily cap <input type="number" name="budget_usd_per_day" min="0" step="0.01" placeholder="Optional, in dollars"></label>-- <button class="c4-button" type="submit">Save the key</button>--</form>--<p class="c4-hint">The key is forwarded to OpenVibe.AI and sealed there; Codes keeps only the four-character hint. Test sends one tiny request with your key.</p>++ return `<section class="c4-card" id="connections"><div class="c4-card-head"><h2>Your AI keys</h2><a class="c4-button" href="${YOUR_AI}">Add or manage keys \u2197</a></div>++<p class="c4-sub">One place for every OpenVibe site and agent: your tasks here use these first, with no fee.</p>++${own ? `<ul class="c4-rows">${own}</ul>` : `<div class="c4-emptyline">${mine ? 'No keys yet.' : 'Your keys could not be read just now.'} <a href="${YOUR_AI}">Add one on Your AI</a>: Anthropic, OpenAI, Google, DeepSeek and more.</div>`}++${plans ? `<h3 class="c4-subhead">Plans on your machines</h3><ul class="c4-rows">${plans}</ul>` : ''}++${problem ? note(`The keys saved in Codes could not be read: ${esc(problem.detail)}`, 'bad') : ''}++${before ? `<h3 class="c4-subhead">Saved in Codes before</h3><ul class="c4-rows">${before}</ul><p class="c4-hint">These still work for your tasks. Add them on <a href="${YOUR_AI}">Your AI</a> to use them on every site, then remove them here.</p>` : ''}</section>`;};r.get('/agents', async (req, res) => {-- const content = `${header('Agents', 'The agents your tasks can run, what they have left, and the keys behind them.')}${viewer(req) ? `${flash(req)}${await fleetCard(req, res)}${await connectionsCard(req)}` : signIn(req)}`;++ const content = `${header('Agents', 'The agents your tasks can run, what they have left, and the keys behind them.')}${viewer(req) ? `${flash(req)}${await fleetCard(req, res)}${await connectionsCard(req, res)}` : signIn(req)}`;page(req, res, 'Agents', wrap(content));});r.post('/agents/connections', form, async (req, res) => {@@@ -123,7 +123,20 @@ const TASK3 = 'tsk_01ARZ3NDEKTSV4RRFFQ69G5FAX';const remove = await t.get('/agents/connections/deepseek/remove', { as: ada, method: 'POST', form: { x: '1' } });assert.strictEqual(remove.status, 303);const after = await t.get('/agents', { as: ada });-- assert.ok(after.text.includes('No provider keys yet'), 'the key is forgotten');++ assert.ok(!after.text.includes('Saved in Codes before'), 'the key is forgotten');++ });++++ await check('Agents: your keys from Your AI, read with your own token, and the one place to manage them', async () => {++ t.network.state.selfKeys = [{ provider: 'anthropic', hint: 'WXYZ', status: 'ok', checked_at: new Date().toISOString() }, { provider: 'gemini', hint: 'AB12', status: 'failing' }];++ const page = await t.get('/agents', { as: ada });++ assert.ok(page.text.includes('<h2>Your AI keys</h2>') && page.text.includes('href="https://ai.openvibe.services/you"'), 'the one place');++ assert.ok(page.text.includes('Anthropic') && page.text.includes('\u2026WXYZ') && page.text.includes('Working'));++ assert.ok(page.text.includes('Google Gemini') && page.text.includes('Not working'));++ assert.ok(t.network.state.meRequests.some((h) => h.startsWith('Bearer ')), 'read as the person');++ assert.ok(!page.text.includes('action="/agents/connections"'), 'no second place to add a key');++ t.network.state.selfKeys = [];++ const empty = await t.get('/agents', { as: ada });++ assert.ok(empty.text.includes('No keys yet.') && empty.text.includes('Add one on Your AI'));});await check('Machines: the person’s own machines, their state, and Node’s pairing code flow', async () => {@@@ -307,6 +307,13 @@ async function startNetwork({ sandboxAudiences = ['openvibe.events', 'openvibe.mreturn json(200, { access_token: mintApp(a, body.audience, wanted), token_type: 'Bearer', expires_in: 300, scope: wanted.join(' ') });}// ── OpenVibe.AI's stand-in (Codes C3): the vault of a person's own keys and the two model endpoints ──++ // The person's own keys on Your AI (L36): read with their own token; tests put rows in st.selfKeys.++ if (url.pathname === '/api/v1/me/credentials' && req.method === 'GET') {++ st.meRequests = st.meRequests || [];++ st.meRequests.push(String(req.headers.authorization || ''));++ if (!String(req.headers.authorization || '').startsWith('Bearer ')) return problem(json, 401, 'token.missing', 'a person token is required');++ return json(200, { credentials: st.selfKeys || [] });++ }if (url.pathname.startsWith('/api/v1/credentials/')) {const parts = url.pathname.split('/').filter(Boolean); // api, v1, credentials, subject, providerconst subject = parts[3]; const provider = parts[4];
@@ -18,6 +18,12 @@
@@ -18,6 +18,12 @@
* put(subject, provider, body) → the connection as AI stored it (the hint, never the key)
* put(subject, provider, body) → the connection as AI stored it (the hint, never the key)
* remove(subject, provider) → true when there was one
* remove(subject, provider) → true when there was one
* test(subject, provider) → { ok, detail }: one tiny request through AI, or why not
* test(subject, provider) → { ok, detail }: one tiny request through AI, or why not
+ * mine(token) → the person's own keys in OpenVibe.AI (their "Your AI" page, L36), read with
+ * their own token: { credentials: [{ provider, hint, status, … }] }
+ *
+ * Since 2026-10-31 a person keeps their keys in one place, ai.openvibe.services/you, for every site and agent; Codes'
+ * tasks run on the person's behalf, so OpenVibe.AI uses those keys first. The keys stored here before stay usable
+ * until the person removes them.
*/
*/
const { createServiceTokenClient } = require('openvibe-sdk/auth');
const { createServiceTokenClient } = require('openvibe-sdk/auth');
@@ -165,7 +171,21 @@ function createConnections({ config, fetchImpl = globalThis.fetch, log = console
@@ -165,7 +171,21 @@ function createConnections({ config, fetchImpl = globalThis.fetch, log = console
return keys.map((k) => k.provider);
return keys.map((k) => k.provider);
}
}
- return { list, put, remove, test, providers, present, PROVIDERS };
+ /** The person's own keys in OpenVibe.AI (GET /api/v1/me/credentials with their own token): hints and states only. */
+ async function mine(token) {
+ if (!token) throw Object.assign(new Error('the person is not signed in'), { status: 401, code: 'token.missing' });
+ let res;
+ try {
+ res = await fetchImpl(`${base}/api/v1/me/credentials`, { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } });
+ } catch (e) {
+ throw Object.assign(new Error('OpenVibe.AI did not answer'), { status: 502, code: 'ai.unavailable' });
+ }
+ const body = await res.json().catch(() => ({}));
+ if (!res.ok) throw Object.assign(new Error((body && (body.detail || body.title)) || `OpenVibe.AI answered ${res.status}`), { status: res.status, code: (body && body.code) || 'ai.error' });
+ return { credentials: Array.isArray(body.credentials) ? body.credentials : [] };
+ }
+
+ return { list, put, remove, test, mine, providers, present, PROVIDERS };
}
}
module.exports = { createConnections, PROVIDERS, ANTHROPIC_FORMAT, providerOf, present };
module.exports = { createConnections, PROVIDERS, ANTHROPIC_FORMAT, providerOf, present };
@@ -64,7 +64,9 @@ const asObject = (v) => {
@@ -64,7 +64,9 @@ const asObject = (v) => {
if (typeof v === 'string') { try { const p = JSON.parse(v); return p && typeof p === 'object' && !Array.isArray(p) ? p : null; } catch { return null; } }
if (typeof v === 'string') { try { const p = JSON.parse(v); return p && typeof p === 'object' && !Array.isArray(p) ? p : null; } catch { return null; } }
return null;
return null;
};
};
-const PROVIDER_NAMES = { anthropic: 'Anthropic', openai: 'OpenAI', deepseek: 'DeepSeek', minimax: 'MiniMax', openrouter: 'OpenRouter', custom: 'Custom (OpenAI-compatible)' };
+const PROVIDER_NAMES = { anthropic: 'Anthropic', openai: 'OpenAI', gemini: 'Google Gemini', deepseek: 'DeepSeek', mistral: 'Mistral', xai: 'xAI', groq: 'Groq', cerebras: 'Cerebras', together: 'Together AI', fireworks: 'Fireworks', minimax: 'MiniMax', openrouter: 'OpenRouter', custom: 'Custom (OpenAI-compatible)' };
+// A person's keys live in one place for every site and agent (plan L36, L39): Codes shows them and links there.
+const YOUR_AI = 'https://ai.openvibe.services/you';
function createC4Routes(ctx) {
function createC4Routes(ctx) {
const r = asyncRouter();
const r = asyncRouter();
@@ -556,33 +558,29 @@ ${list ? `<ul class="c4-rows c4-fleet">${list}</ul>` : `<div class="c4-emptyline
@@ -556,33 +558,29 @@ ${list ? `<ul class="c4-rows c4-fleet">${list}</ul>` : `<div class="c4-emptyline
};
};
// ── Agents: the person's connections (keys live in OpenVibe.AI) ────────────────────────────────────────────
// ── Agents: the person's connections (keys live in OpenVibe.AI) ────────────────────────────────────────────
- const connectionsCard = async (req) => {
+ const connectionsCard = async (req, res = null) => {
let list = { keys: [], plans: [] };
let list = { keys: [], plans: [] };
+ let mine = null;
let problem = null;
let problem = null;
try { list = await connections.list(req.viewer.subject); } catch (e) {
try { list = await connections.list(req.viewer.subject); } catch (e) {
problem = { status: e && e.status, detail: (e && e.message) || 'the connections could not be read' };
problem = { status: e && e.status, detail: (e && e.message) || 'the connections could not be read' };
}
}
- const now = Date.now();
- const rows = (list.keys || []).map((k) => `<li class="c4-row c4-row-flat"><span class="c4-row-dot c4-dot-ok" aria-hidden="true"></span><span class="c4-row-label">${esc(PROVIDER_NAMES[k.provider] || k.provider)}</span><span class="c4-row-note">${esc(k.hint || 'key stored')}${k.budget_usd_per_day != null ? ` · cap $${Number(k.budget_usd_per_day).toFixed(2)} a day` : ''}${k.last_used_at ? ` · last used ${esc(age(k.last_used_at))}` : ''}</span><span class="c4-inline-actions"><form method="post" action="/agents/connections/${esc(k.provider)}/test"><button class="c4-opill" type="submit">Test</button></form><form method="post" action="/agents/connections/${esc(k.provider)}/remove"><button class="c4-opill c4-opill-quiet" type="submit">Remove</button></form></span></li>`).join('');
+ try { mine = await sso.asViewer(req, res, (token) => connections.mine(token)); } catch (e) { mine = null; }
+ const state = (k) => (k.status === 'ok' ? pill('Working', 'ok') : k.status === 'failing' ? pill('Not working', 'bad') : pill('Not tested', 'skip'));
+ const own = ((mine && mine.credentials) || []).map((k) => `<li class="c4-row c4-row-flat"><span class="c4-row-dot${k.status === 'ok' ? ' c4-dot-ok' : k.status === 'failing' ? ' c4-dot-bad' : ''}" aria-hidden="true"></span><span class="c4-row-label">${esc(PROVIDER_NAMES[k.provider] || k.provider)}</span><span class="c4-row-note">\u2026${esc(k.hint || '????')}${k.checked_at ? ` · checked ${esc(age(k.checked_at))}` : ''}</span>${state(k)}</li>`).join('');
+ const before = (list.keys || []).map((k) => `<li class="c4-row c4-row-flat"><span class="c4-row-dot c4-dot-skip" aria-hidden="true"></span><span class="c4-row-label">${esc(PROVIDER_NAMES[k.provider] || k.provider)}</span><span class="c4-row-note">${esc(k.hint || 'key stored')}${k.last_used_at ? ` · last used ${esc(age(k.last_used_at))}` : ''}</span><span class="c4-inline-actions"><form method="post" action="/agents/connections/${esc(k.provider)}/test"><button class="c4-opill" type="submit">Test</button></form><form method="post" action="/agents/connections/${esc(k.provider)}/remove"><button class="c4-opill c4-opill-quiet" type="submit">Remove</button></form></span></li>`).join('');
const plans = (list.plans || []).map((p) => `<li class="c4-row c4-row-flat"><span class="c4-row-dot c4-dot-skip" aria-hidden="true"></span><span class="c4-row-label">${esc(p.label || p.provider || 'a plan')}</span><span class="c4-row-note">on ${esc(p.machine || 'your machine')}</span>${pill('On your machine', 'skip')}</li>`).join('');
const plans = (list.plans || []).map((p) => `<li class="c4-row c4-row-flat"><span class="c4-row-dot c4-dot-skip" aria-hidden="true"></span><span class="c4-row-label">${esc(p.label || p.provider || 'a plan')}</span><span class="c4-row-note">on ${esc(p.machine || 'your machine')}</span>${pill('On your machine', 'skip')}</li>`).join('');
- return `<section class="c4-card" id="connections"><div class="c4-card-head"><h2>Connections</h2></div>
-<p class="c4-sub">Keys live in OpenVibe.AI, never here</p>
-${problem ? note(`Your connections could not be read: ${esc(problem.detail)}`, 'bad') : ''}
-${rows || plans ? `<ul class="c4-rows">${rows}${plans}</ul>` : `<div class="c4-emptyline">No provider keys yet. Add one below and the router can use it, and so can a task.</div>`}
-<h3 class="c4-subhead">Add or replace a key</h3>
-<form class="c4-form" method="post" action="/agents/connections">
- <label>Provider <select name="provider">${connections.PROVIDERS.map((p) => `<option value="${esc(p)}">${esc(PROVIDER_NAMES[p] || p)}</option>`).join('')}</select></label>
- <label>Key <input type="password" name="key" required minlength="8" maxlength="400" autocomplete="off" spellcheck="false" placeholder="sk-…"></label>
- <label>Base URL <input type="url" name="base_url" placeholder="Only for a custom provider" autocomplete="off"></label>
- <label>Daily cap <input type="number" name="budget_usd_per_day" min="0" step="0.01" placeholder="Optional, in dollars"></label>
- <button class="c4-button" type="submit">Save the key</button>
-</form>
-<p class="c4-hint">The key is forwarded to OpenVibe.AI and sealed there; Codes keeps only the four-character hint. Test sends one tiny request with your key.</p>
+ return `<section class="c4-card" id="connections"><div class="c4-card-head"><h2>Your AI keys</h2><a class="c4-button" href="${YOUR_AI}">Add or manage keys \u2197</a></div>
+<p class="c4-sub">One place for every OpenVibe site and agent: your tasks here use these first, with no fee.</p>
+${own ? `<ul class="c4-rows">${own}</ul>` : `<div class="c4-emptyline">${mine ? 'No keys yet.' : 'Your keys could not be read just now.'} <a href="${YOUR_AI}">Add one on Your AI</a>: Anthropic, OpenAI, Google, DeepSeek and more.</div>`}
+${plans ? `<h3 class="c4-subhead">Plans on your machines</h3><ul class="c4-rows">${plans}</ul>` : ''}
+${problem ? note(`The keys saved in Codes could not be read: ${esc(problem.detail)}`, 'bad') : ''}
+${before ? `<h3 class="c4-subhead">Saved in Codes before</h3><ul class="c4-rows">${before}</ul><p class="c4-hint">These still work for your tasks. Add them on <a href="${YOUR_AI}">Your AI</a> to use them on every site, then remove them here.</p>` : ''}
</section>`;
</section>`;
};
};
r.get('/agents', async (req, res) => {
r.get('/agents', async (req, res) => {
- const content = `${header('Agents', 'The agents your tasks can run, what they have left, and the keys behind them.')}${viewer(req) ? `${flash(req)}${await fleetCard(req, res)}${await connectionsCard(req)}` : signIn(req)}`;
+ const content = `${header('Agents', 'The agents your tasks can run, what they have left, and the keys behind them.')}${viewer(req) ? `${flash(req)}${await fleetCard(req, res)}${await connectionsCard(req, res)}` : signIn(req)}`;
page(req, res, 'Agents', wrap(content));
page(req, res, 'Agents', wrap(content));
});
});
r.post('/agents/connections', form, async (req, res) => {
r.post('/agents/connections', form, async (req, res) => {
@@ -123,7 +123,20 @@ const TASK3 = 'tsk_01ARZ3NDEKTSV4RRFFQ69G5FAX';
@@ -123,7 +123,20 @@ const TASK3 = 'tsk_01ARZ3NDEKTSV4RRFFQ69G5FAX';
const remove = await t.get('/agents/connections/deepseek/remove', { as: ada, method: 'POST', form: { x: '1' } });
const remove = await t.get('/agents/connections/deepseek/remove', { as: ada, method: 'POST', form: { x: '1' } });
assert.strictEqual(remove.status, 303);
assert.strictEqual(remove.status, 303);
const after = await t.get('/agents', { as: ada });
const after = await t.get('/agents', { as: ada });
- assert.ok(after.text.includes('No provider keys yet'), 'the key is forgotten');
+ assert.ok(!after.text.includes('Saved in Codes before'), 'the key is forgotten');
+ });
+
+ await check('Agents: your keys from Your AI, read with your own token, and the one place to manage them', async () => {
+ t.network.state.selfKeys = [{ provider: 'anthropic', hint: 'WXYZ', status: 'ok', checked_at: new Date().toISOString() }, { provider: 'gemini', hint: 'AB12', status: 'failing' }];
+ const page = await t.get('/agents', { as: ada });
+ assert.ok(page.text.includes('<h2>Your AI keys</h2>') && page.text.includes('href="https://ai.openvibe.services/you"'), 'the one place');
+ assert.ok(page.text.includes('Anthropic') && page.text.includes('\u2026WXYZ') && page.text.includes('Working'));
+ assert.ok(page.text.includes('Google Gemini') && page.text.includes('Not working'));
+ assert.ok(t.network.state.meRequests.some((h) => h.startsWith('Bearer ')), 'read as the person');
+ assert.ok(!page.text.includes('action="/agents/connections"'), 'no second place to add a key');
+ t.network.state.selfKeys = [];
+ const empty = await t.get('/agents', { as: ada });
+ assert.ok(empty.text.includes('No keys yet.') && empty.text.includes('Add one on Your AI'));
});
});
await check('Machines: the person’s own machines, their state, and Node’s pairing code flow', async () => {
await check('Machines: the person’s own machines, their state, and Node’s pairing code flow', async () => {
@@ -307,6 +307,13 @@ async function startNetwork({ sandboxAudiences = ['openvibe.events', 'openvibe.m
@@ -307,6 +307,13 @@ async function startNetwork({ sandboxAudiences = ['openvibe.events', 'openvibe.m
return json(200, { access_token: mintApp(a, body.audience, wanted), token_type: 'Bearer', expires_in: 300, scope: wanted.join(' ') });
return json(200, { access_token: mintApp(a, body.audience, wanted), token_type: 'Bearer', expires_in: 300, scope: wanted.join(' ') });
}
}
// ── OpenVibe.AI's stand-in (Codes C3): the vault of a person's own keys and the two model endpoints ──
// ── OpenVibe.AI's stand-in (Codes C3): the vault of a person's own keys and the two model endpoints ──
+ // The person's own keys on Your AI (L36): read with their own token; tests put rows in st.selfKeys.
+ if (url.pathname === '/api/v1/me/credentials' && req.method === 'GET') {
+ st.meRequests = st.meRequests || [];
+ st.meRequests.push(String(req.headers.authorization || ''));
+ if (!String(req.headers.authorization || '').startsWith('Bearer ')) return problem(json, 401, 'token.missing', 'a person token is required');
+ return json(200, { credentials: st.selfKeys || [] });
+ }
if (url.pathname.startsWith('/api/v1/credentials/')) {
if (url.pathname.startsWith('/api/v1/credentials/')) {
const parts = url.pathname.split('/').filter(Boolean); // api, v1, credentials, subject, provider
const parts = url.pathname.split('/').filter(Boolean); // api, v1, credentials, subject, provider
const subject = parts[3]; const provider = parts[4];
const subject = parts[3]; const provider = parts[4];