openvibers/openvibe.network Commits
Relay owner agent sheets through Network frame (#817)
Changes
@@@ -780,7 +780,7 @@ async function createFrameService(db, config, analytics, { privateKey = null, isconst dockAuth = (req) => (/^Bearer [A-Za-z0-9._~+/=-]{10,4096}$/.test(String(req.get('authorization') || '')) ? req.get('authorization') : '');const DOCK_ID = /^[A-Za-z0-9._~:-]{1,160}$/;const dockId = (v) => (DOCK_ID.test(String(v || '')) ? String(v) : '');-- function dockRelay(service, method, upstream, { image = false } = {}) {++ function dockRelay(service, method, upstream, { image = false, maxBytes = 64 * 1024 } = {}) {const upstreamPath = typeof upstream === 'function' ? upstream : () => upstream;const fallback = service === 'codes' ? 'http://127.0.0.1:4900' : 'http://127.0.0.1:4950';return async (req, res) => {@@@ -801,13 +801,14 @@ async function createFrameService(db, config, analytics, { privateKey = null, is}const text = await r.text();res.set('Content-Type', 'application/json; charset=utf-8');-- return res.status(r.status).send(text.slice(0, 64 * 1024));++ if (Buffer.byteLength(text) > maxBytes) return res.status(502).json({ error: 'The Dock service answer was too large' });++ return res.status(r.status).send(text);} catch { return res.status(502).json({ error: 'The Dock service did not answer' }); }};}const dockJson = express.json({ limit: '8kb' });-- router.options(['/agent-view', '/agent-view/*', '/agent/*', '/tasks/*', '/actor', '/actor/*', '/codes-tasks', '/codes-tasks/*'], (req, res) => {-- res.set({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Authorization, Content-Type', 'Access-Control-Allow-Methods': 'GET, POST', 'Access-Control-Max-Age': '600' });++ router.options(['/agent-view', '/agent-view/*', '/agent/*', '/agents/*', '/codes-agents/*', '/tasks/*', '/actor', '/actor/*', '/codes-tasks', '/codes-tasks/*'], (req, res) => {++ res.set({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Authorization, Content-Type', 'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE', 'Access-Control-Max-Age': '600' });res.status(204).end();});// OpenVibe.Codes: the person's agent stream, its last still, and the four controls.@@@ -836,6 +837,22 @@ async function createFrameService(db, config, analytics, { privateKey = null, isrouter.get('/actor', dockRelay('actor', 'GET', '/api/v1/me/actor'));for (const what of ['pause', 'resume', 'tell']) router.post(`/actor/${what}`, dockJson, dockRelay('actor', 'POST', `/api/v1/me/actor/${what}`));for (const what of ['answer', 'decline']) router.post(`/actor/asks/:id/${what}`, dockJson, dockRelay('actor', 'POST', (req) => (dockId(req.params.id) ? `/api/v1/me/actor/asks/${encodeURIComponent(req.params.id)}/${what}` : null)));++ // One owner's agent sheet on any site. Actor checks ownership and capabilities; the relay accepts only its++ // fixed ID shapes and passes the person's Bearer upstream. The larger answer includes up to 100 work records.++ const AGENT_ID = /^agt_[0-9A-HJKMNP-TV-Z]{26}$/;++ const MEMORY_ID = /^mem_[0-9A-HJKMNP-TV-Z]{26}$/;++ const agentPath = (req, tail = '') => AGENT_ID.test(String(req.params.id || '')) ? `/api/v1/me/agents/${req.params.id}${tail}` : null;++ router.get('/agents/:id/sheet', dockRelay('actor', 'GET', (req) => {++ const path = agentPath(req);++ return path ? `${path}?view=sheet` : null;++ }, { maxBytes: 512 * 1024 }));++ router.get('/codes-agents/:id/sheet', dockRelay('codes', 'GET', (req) =>++ /^[a-z][a-z0-9-]{0,63}$/.test(String(req.params.id || '')) ? `/api/v1/me/agents/${req.params.id}/sheet` : null,++ { maxBytes: 512 * 1024 }));++ router.patch('/agents/:id', dockJson, dockRelay('actor', 'PATCH', (req) => agentPath(req)));++ router.post('/agents/:id/memory', dockJson, dockRelay('actor', 'POST', (req) => agentPath(req, '/memory')));++ router.patch('/agents/:id/memory/:mem', dockJson, dockRelay('actor', 'PATCH', (req) => MEMORY_ID.test(String(req.params.mem || '')) ? agentPath(req, `/memory/${req.params.mem}`) : null));++ router.delete('/agents/:id/memory/:mem', dockRelay('actor', 'DELETE', (req) => MEMORY_ID.test(String(req.params.mem || '')) ? agentPath(req, `/memory/${req.params.mem}`) : null));// OpenVibe.Codes' person tasks API (the Dock's Code tab): the latest tasks running first, one start, one stop.// ?limit= is the list's cap (the Dock asks with 8); the stop's id is a tsk_ id, anything else is not a task.const TASK_ID = /^tsk_[0-9A-HJKMNP-TV-Z]{26}$/;@@@ -11,6 +11,8 @@ const { getDb } = require('../server/db/database');const { createFrameService } = require('../server/frame/service');const TOKEN = 'Bearer abcdefghijklmnop.qrstu';++const AGENT_ID = 'agt_00000000000000000000000000';++const MEMORY_ID = 'mem_00000000000000000000000000';// A one-pixel JPEG: the still the Dock shows when nothing is live.const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0xff, 0xd9]);@@@ -22,7 +24,8 @@ const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49,const chunks = [];req.on('data', (c) => chunks.push(c));req.on('end', () => {-- asked.push({ svc: name, method: req.method, url: req.url, auth: req.headers.authorization || '', cookie: req.headers.cookie || '', body: Buffer.concat(chunks).toString('utf8') });++ req.body = Buffer.concat(chunks).toString('utf8');++ asked.push({ svc: name, method: req.method, url: req.url, auth: req.headers.authorization || '', cookie: req.headers.cookie || '', body: req.body });if (down) { res.statusCode = 503; return res.end('{"error":"down"}'); }return handler(req, res, new URL(req.url, 'http://x'));});@@@ -30,6 +33,10 @@ const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49,return server;};const codes = stub('codes', (req, res, u) => {++ if (u.pathname === '/api/v1/me/agents/codex/sheet' && req.method === 'GET') {++ res.setHeader('content-type', 'application/json');++ return res.end(JSON.stringify({ agent: { id: 'codex', name: 'Codex' }, work: [] }));++ }if (u.pathname === '/api/v1/me/agent-view' && req.method === 'GET') {res.setHeader('content-type', 'application/json');return res.end(JSON.stringify({ agent: { id: 'ags_1', state: 'live', title: 'Ship it' } }));@@@ -47,6 +54,10 @@ const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49,});const actor = stub('actor', (req, res, u) => {res.setHeader('content-type', 'application/json');++ if (u.pathname === `/api/v1/me/agents/${AGENT_ID}` && u.searchParams.get('view') === 'sheet') return res.end(JSON.stringify({ agent: { id: AGENT_ID, name: 'Helper' }, work: [], asks_empty: 'Requests to approve or decline will appear here.' }));++ if (u.pathname === `/api/v1/me/agents/${AGENT_ID}` && req.method === 'PATCH') return res.end(JSON.stringify({ status: JSON.parse(req.body || '{}').status }));++ if (u.pathname === `/api/v1/me/agents/${AGENT_ID}/memory` && req.method === 'POST') { res.statusCode = 201; return res.end(JSON.stringify({ text: JSON.parse(req.body || '{}').text })); }++ if (u.pathname === `/api/v1/me/agents/${AGENT_ID}/memory/${MEMORY_ID}` && (req.method === 'PATCH' || req.method === 'DELETE')) { res.statusCode = req.method === 'DELETE' ? 204 : 200; return res.end(req.method === 'DELETE' ? '' : JSON.stringify({ text: JSON.parse(req.body || '{}').text })); }if (u.pathname === '/api/v1/agent/timeline' && req.method === 'GET') return res.end(JSON.stringify({ now: null, needs_you: [], entries: [], counts: { mode: u.searchParams.get('mode') } }));if (/^\/api\/v1\/agent\/approvals\/[a-z]+\/[A-Za-z0-9._~-]+\/decide$/.test(u.pathname) && req.method === 'POST') { res.statusCode = 201; return res.end(JSON.stringify({ product: 'actor', state: 'approved' })); }if (u.pathname === '/api/v1/agent/kill-switch' && req.method === 'POST') return res.end(JSON.stringify({ ok: true }));@@@ -154,6 +165,34 @@ const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49,assert.ok(asked.some((x) => x.url === `/api/v1/me/actor/asks/ask_1/${what}` && x.method === 'POST'));}++ // The sheet and its owner edits use fixed paths, pass only the Bearer, and reject malformed IDs locally.++ ({ r, d } = await get(`/agents/${AGENT_ID}/sheet`, { Authorization: TOKEN, Cookie: 'ov_token=secret' }));++ assert.strictEqual(r.status, 200);++ assert.strictEqual(d.agent.name, 'Helper');++ hit = asked.at(-1);++ assert.strictEqual(hit.url, `/api/v1/me/agents/${AGENT_ID}?view=sheet`);++ assert.strictEqual(hit.auth, TOKEN);++ assert.strictEqual(hit.cookie, '');++ const sheetCalls = asked.length;++ ({ r } = await get(`/agents/${AGENT_ID}/sheet`));++ assert.strictEqual(r.status, 401);++ ({ r } = await get('/agents/bad/sheet', { Authorization: TOKEN }));++ assert.strictEqual(r.status, 404);++ assert.strictEqual(asked.length, sheetCalls);++ ({ r, d } = await get('/codes-agents/codex/sheet', { Authorization: TOKEN }));++ assert.strictEqual(r.status, 200);++ assert.strictEqual(d.agent.id, 'codex');++ assert.strictEqual(asked.at(-1).url, '/api/v1/me/agents/codex/sheet');++ const patch = await fetch(`${base}/agents/${AGENT_ID}`, { method: 'PATCH', headers: { Authorization: TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'paused' }) });++ assert.strictEqual((await patch.json()).status, 'paused');++ ({ r, d } = await send(`/agents/${AGENT_ID}/memory`, { text: 'Keep this fact' }));++ assert.strictEqual(r.status, 201);++ assert.strictEqual(d.text, 'Keep this fact');++ const editMemory = await fetch(`${base}/agents/${AGENT_ID}/memory/${MEMORY_ID}`, { method: 'PATCH', headers: { Authorization: TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ text: 'Revised fact' }) });++ assert.strictEqual((await editMemory.json()).text, 'Revised fact');++ const removeMemory = await fetch(`${base}/agents/${AGENT_ID}/memory/${MEMORY_ID}`, { method: 'DELETE', headers: { Authorization: TOKEN } });++ assert.strictEqual(removeMemory.status, 204);++// A preflight is answered for every path; an upstream status passes back, and a service that is not there is// a 502, never a hang.const opt = await fetch(`${base}/agent/timeline`, { method: 'OPTIONS' });
@@ -780,7 +780,7 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
@@ -780,7 +780,7 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
const dockAuth = (req) => (/^Bearer [A-Za-z0-9._~+/=-]{10,4096}$/.test(String(req.get('authorization') || '')) ? req.get('authorization') : '');
const dockAuth = (req) => (/^Bearer [A-Za-z0-9._~+/=-]{10,4096}$/.test(String(req.get('authorization') || '')) ? req.get('authorization') : '');
const DOCK_ID = /^[A-Za-z0-9._~:-]{1,160}$/;
const DOCK_ID = /^[A-Za-z0-9._~:-]{1,160}$/;
const dockId = (v) => (DOCK_ID.test(String(v || '')) ? String(v) : '');
const dockId = (v) => (DOCK_ID.test(String(v || '')) ? String(v) : '');
- function dockRelay(service, method, upstream, { image = false } = {}) {
+ function dockRelay(service, method, upstream, { image = false, maxBytes = 64 * 1024 } = {}) {
const upstreamPath = typeof upstream === 'function' ? upstream : () => upstream;
const upstreamPath = typeof upstream === 'function' ? upstream : () => upstream;
const fallback = service === 'codes' ? 'http://127.0.0.1:4900' : 'http://127.0.0.1:4950';
const fallback = service === 'codes' ? 'http://127.0.0.1:4900' : 'http://127.0.0.1:4950';
return async (req, res) => {
return async (req, res) => {
@@ -801,13 +801,14 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
@@ -801,13 +801,14 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
}
}
const text = await r.text();
const text = await r.text();
res.set('Content-Type', 'application/json; charset=utf-8');
res.set('Content-Type', 'application/json; charset=utf-8');
- return res.status(r.status).send(text.slice(0, 64 * 1024));
+ if (Buffer.byteLength(text) > maxBytes) return res.status(502).json({ error: 'The Dock service answer was too large' });
+ return res.status(r.status).send(text);
} catch { return res.status(502).json({ error: 'The Dock service did not answer' }); }
} catch { return res.status(502).json({ error: 'The Dock service did not answer' }); }
};
};
}
}
const dockJson = express.json({ limit: '8kb' });
const dockJson = express.json({ limit: '8kb' });
- router.options(['/agent-view', '/agent-view/*', '/agent/*', '/tasks/*', '/actor', '/actor/*', '/codes-tasks', '/codes-tasks/*'], (req, res) => {
- res.set({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Authorization, Content-Type', 'Access-Control-Allow-Methods': 'GET, POST', 'Access-Control-Max-Age': '600' });
+ router.options(['/agent-view', '/agent-view/*', '/agent/*', '/agents/*', '/codes-agents/*', '/tasks/*', '/actor', '/actor/*', '/codes-tasks', '/codes-tasks/*'], (req, res) => {
+ res.set({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Authorization, Content-Type', 'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE', 'Access-Control-Max-Age': '600' });
res.status(204).end();
res.status(204).end();
});
});
// OpenVibe.Codes: the person's agent stream, its last still, and the four controls.
// OpenVibe.Codes: the person's agent stream, its last still, and the four controls.
@@ -836,6 +837,22 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
@@ -836,6 +837,22 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
router.get('/actor', dockRelay('actor', 'GET', '/api/v1/me/actor'));
router.get('/actor', dockRelay('actor', 'GET', '/api/v1/me/actor'));
for (const what of ['pause', 'resume', 'tell']) router.post(`/actor/${what}`, dockJson, dockRelay('actor', 'POST', `/api/v1/me/actor/${what}`));
for (const what of ['pause', 'resume', 'tell']) router.post(`/actor/${what}`, dockJson, dockRelay('actor', 'POST', `/api/v1/me/actor/${what}`));
for (const what of ['answer', 'decline']) router.post(`/actor/asks/:id/${what}`, dockJson, dockRelay('actor', 'POST', (req) => (dockId(req.params.id) ? `/api/v1/me/actor/asks/${encodeURIComponent(req.params.id)}/${what}` : null)));
for (const what of ['answer', 'decline']) router.post(`/actor/asks/:id/${what}`, dockJson, dockRelay('actor', 'POST', (req) => (dockId(req.params.id) ? `/api/v1/me/actor/asks/${encodeURIComponent(req.params.id)}/${what}` : null)));
+ // One owner's agent sheet on any site. Actor checks ownership and capabilities; the relay accepts only its
+ // fixed ID shapes and passes the person's Bearer upstream. The larger answer includes up to 100 work records.
+ const AGENT_ID = /^agt_[0-9A-HJKMNP-TV-Z]{26}$/;
+ const MEMORY_ID = /^mem_[0-9A-HJKMNP-TV-Z]{26}$/;
+ const agentPath = (req, tail = '') => AGENT_ID.test(String(req.params.id || '')) ? `/api/v1/me/agents/${req.params.id}${tail}` : null;
+ router.get('/agents/:id/sheet', dockRelay('actor', 'GET', (req) => {
+ const path = agentPath(req);
+ return path ? `${path}?view=sheet` : null;
+ }, { maxBytes: 512 * 1024 }));
+ router.get('/codes-agents/:id/sheet', dockRelay('codes', 'GET', (req) =>
+ /^[a-z][a-z0-9-]{0,63}$/.test(String(req.params.id || '')) ? `/api/v1/me/agents/${req.params.id}/sheet` : null,
+ { maxBytes: 512 * 1024 }));
+ router.patch('/agents/:id', dockJson, dockRelay('actor', 'PATCH', (req) => agentPath(req)));
+ router.post('/agents/:id/memory', dockJson, dockRelay('actor', 'POST', (req) => agentPath(req, '/memory')));
+ router.patch('/agents/:id/memory/:mem', dockJson, dockRelay('actor', 'PATCH', (req) => MEMORY_ID.test(String(req.params.mem || '')) ? agentPath(req, `/memory/${req.params.mem}`) : null));
+ router.delete('/agents/:id/memory/:mem', dockRelay('actor', 'DELETE', (req) => MEMORY_ID.test(String(req.params.mem || '')) ? agentPath(req, `/memory/${req.params.mem}`) : null));
// OpenVibe.Codes' person tasks API (the Dock's Code tab): the latest tasks running first, one start, one stop.
// OpenVibe.Codes' person tasks API (the Dock's Code tab): the latest tasks running first, one start, one stop.
// ?limit= is the list's cap (the Dock asks with 8); the stop's id is a tsk_ id, anything else is not a task.
// ?limit= is the list's cap (the Dock asks with 8); the stop's id is a tsk_ id, anything else is not a task.
const TASK_ID = /^tsk_[0-9A-HJKMNP-TV-Z]{26}$/;
const TASK_ID = /^tsk_[0-9A-HJKMNP-TV-Z]{26}$/;
@@ -11,6 +11,8 @@ const { getDb } = require('../server/db/database');
@@ -11,6 +11,8 @@ const { getDb } = require('../server/db/database');
const { createFrameService } = require('../server/frame/service');
const { createFrameService } = require('../server/frame/service');
const TOKEN = 'Bearer abcdefghijklmnop.qrstu';
const TOKEN = 'Bearer abcdefghijklmnop.qrstu';
+const AGENT_ID = 'agt_00000000000000000000000000';
+const MEMORY_ID = 'mem_00000000000000000000000000';
// A one-pixel JPEG: the still the Dock shows when nothing is live.
// A one-pixel JPEG: the still the Dock shows when nothing is live.
const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0xff, 0xd9]);
const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0xff, 0xd9]);
@@ -22,7 +24,8 @@ const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49,
@@ -22,7 +24,8 @@ const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49,
const chunks = [];
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
req.on('end', () => {
- asked.push({ svc: name, method: req.method, url: req.url, auth: req.headers.authorization || '', cookie: req.headers.cookie || '', body: Buffer.concat(chunks).toString('utf8') });
+ req.body = Buffer.concat(chunks).toString('utf8');
+ asked.push({ svc: name, method: req.method, url: req.url, auth: req.headers.authorization || '', cookie: req.headers.cookie || '', body: req.body });
if (down) { res.statusCode = 503; return res.end('{"error":"down"}'); }
if (down) { res.statusCode = 503; return res.end('{"error":"down"}'); }
return handler(req, res, new URL(req.url, 'http://x'));
return handler(req, res, new URL(req.url, 'http://x'));
});
});
@@ -30,6 +33,10 @@ const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49,
@@ -30,6 +33,10 @@ const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49,
return server;
return server;
};
};
const codes = stub('codes', (req, res, u) => {
const codes = stub('codes', (req, res, u) => {
+ if (u.pathname === '/api/v1/me/agents/codex/sheet' && req.method === 'GET') {
+ res.setHeader('content-type', 'application/json');
+ return res.end(JSON.stringify({ agent: { id: 'codex', name: 'Codex' }, work: [] }));
+ }
if (u.pathname === '/api/v1/me/agent-view' && req.method === 'GET') {
if (u.pathname === '/api/v1/me/agent-view' && req.method === 'GET') {
res.setHeader('content-type', 'application/json');
res.setHeader('content-type', 'application/json');
return res.end(JSON.stringify({ agent: { id: 'ags_1', state: 'live', title: 'Ship it' } }));
return res.end(JSON.stringify({ agent: { id: 'ags_1', state: 'live', title: 'Ship it' } }));
@@ -47,6 +54,10 @@ const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49,
@@ -47,6 +54,10 @@ const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49,
});
});
const actor = stub('actor', (req, res, u) => {
const actor = stub('actor', (req, res, u) => {
res.setHeader('content-type', 'application/json');
res.setHeader('content-type', 'application/json');
+ if (u.pathname === `/api/v1/me/agents/${AGENT_ID}` && u.searchParams.get('view') === 'sheet') return res.end(JSON.stringify({ agent: { id: AGENT_ID, name: 'Helper' }, work: [], asks_empty: 'Requests to approve or decline will appear here.' }));
+ if (u.pathname === `/api/v1/me/agents/${AGENT_ID}` && req.method === 'PATCH') return res.end(JSON.stringify({ status: JSON.parse(req.body || '{}').status }));
+ if (u.pathname === `/api/v1/me/agents/${AGENT_ID}/memory` && req.method === 'POST') { res.statusCode = 201; return res.end(JSON.stringify({ text: JSON.parse(req.body || '{}').text })); }
+ if (u.pathname === `/api/v1/me/agents/${AGENT_ID}/memory/${MEMORY_ID}` && (req.method === 'PATCH' || req.method === 'DELETE')) { res.statusCode = req.method === 'DELETE' ? 204 : 200; return res.end(req.method === 'DELETE' ? '' : JSON.stringify({ text: JSON.parse(req.body || '{}').text })); }
if (u.pathname === '/api/v1/agent/timeline' && req.method === 'GET') return res.end(JSON.stringify({ now: null, needs_you: [], entries: [], counts: { mode: u.searchParams.get('mode') } }));
if (u.pathname === '/api/v1/agent/timeline' && req.method === 'GET') return res.end(JSON.stringify({ now: null, needs_you: [], entries: [], counts: { mode: u.searchParams.get('mode') } }));
if (/^\/api\/v1\/agent\/approvals\/[a-z]+\/[A-Za-z0-9._~-]+\/decide$/.test(u.pathname) && req.method === 'POST') { res.statusCode = 201; return res.end(JSON.stringify({ product: 'actor', state: 'approved' })); }
if (/^\/api\/v1\/agent\/approvals\/[a-z]+\/[A-Za-z0-9._~-]+\/decide$/.test(u.pathname) && req.method === 'POST') { res.statusCode = 201; return res.end(JSON.stringify({ product: 'actor', state: 'approved' })); }
if (u.pathname === '/api/v1/agent/kill-switch' && req.method === 'POST') return res.end(JSON.stringify({ ok: true }));
if (u.pathname === '/api/v1/agent/kill-switch' && req.method === 'POST') return res.end(JSON.stringify({ ok: true }));
@@ -154,6 +165,34 @@ const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49,
@@ -154,6 +165,34 @@ const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49,
assert.ok(asked.some((x) => x.url === `/api/v1/me/actor/asks/ask_1/${what}` && x.method === 'POST'));
assert.ok(asked.some((x) => x.url === `/api/v1/me/actor/asks/ask_1/${what}` && x.method === 'POST'));
}
}
+ // The sheet and its owner edits use fixed paths, pass only the Bearer, and reject malformed IDs locally.
+ ({ r, d } = await get(`/agents/${AGENT_ID}/sheet`, { Authorization: TOKEN, Cookie: 'ov_token=secret' }));
+ assert.strictEqual(r.status, 200);
+ assert.strictEqual(d.agent.name, 'Helper');
+ hit = asked.at(-1);
+ assert.strictEqual(hit.url, `/api/v1/me/agents/${AGENT_ID}?view=sheet`);
+ assert.strictEqual(hit.auth, TOKEN);
+ assert.strictEqual(hit.cookie, '');
+ const sheetCalls = asked.length;
+ ({ r } = await get(`/agents/${AGENT_ID}/sheet`));
+ assert.strictEqual(r.status, 401);
+ ({ r } = await get('/agents/bad/sheet', { Authorization: TOKEN }));
+ assert.strictEqual(r.status, 404);
+ assert.strictEqual(asked.length, sheetCalls);
+ ({ r, d } = await get('/codes-agents/codex/sheet', { Authorization: TOKEN }));
+ assert.strictEqual(r.status, 200);
+ assert.strictEqual(d.agent.id, 'codex');
+ assert.strictEqual(asked.at(-1).url, '/api/v1/me/agents/codex/sheet');
+ const patch = await fetch(`${base}/agents/${AGENT_ID}`, { method: 'PATCH', headers: { Authorization: TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'paused' }) });
+ assert.strictEqual((await patch.json()).status, 'paused');
+ ({ r, d } = await send(`/agents/${AGENT_ID}/memory`, { text: 'Keep this fact' }));
+ assert.strictEqual(r.status, 201);
+ assert.strictEqual(d.text, 'Keep this fact');
+ const editMemory = await fetch(`${base}/agents/${AGENT_ID}/memory/${MEMORY_ID}`, { method: 'PATCH', headers: { Authorization: TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ text: 'Revised fact' }) });
+ assert.strictEqual((await editMemory.json()).text, 'Revised fact');
+ const removeMemory = await fetch(`${base}/agents/${AGENT_ID}/memory/${MEMORY_ID}`, { method: 'DELETE', headers: { Authorization: TOKEN } });
+ assert.strictEqual(removeMemory.status, 204);
+
// A preflight is answered for every path; an upstream status passes back, and a service that is not there is
// A preflight is answered for every path; an upstream status passes back, and a service that is not there is
// a 502, never a hang.
// a 502, never a hang.
const opt = await fetch(`${base}/agent/timeline`, { method: 'OPTIONS' });
const opt = await fetch(`${base}/agent/timeline`, { method: 'OPTIONS' });