openvibers/openvibe.codes Commits

Expose owner coding agent sheets from real attempts (#400)

OP OpenVibers committed 2 days ago · 15ba246970 · 2 files, +74 −1

* Expose owner coding agent sheets from real attempts

* Use ISO day for coding agent activity

Changes

ddiff --git a/server/http/tasks-api.js b/server/http/tasks-api.js
iindex 78a0032..2840a81 100644
---- a/server/http/tasks-api.js
++++ b/server/http/tasks-api.js
@@@ -56,7 +56,7 @@ function createTasksApi({ config, store, keys, run, harnesses, actorLimits, task
// The Dock's Code tab calls these from another OpenVibe site with the person's bearer token: an allowed origin
// is reflected so the page may read the answer (server/http/openvibe-origins.js). Only the Dock's own two
// routes: another router's preflight must fall through to its own answer.
-- r.use(corsOpenVibe({ methods: 'GET, POST, OPTIONS', paths: ['/tasks', /^\/tasks\/[^/]+\/stop$/] }));
++ r.use(corsOpenVibe({ methods: 'GET, POST, OPTIONS', paths: ['/tasks', /^\/tasks\/[^/]+\/stop$/, /^\/me\/agents\/[^/]+\/sheet$/] }));
const benchTokens = config.oauth.clientSecret ? createServiceTokenClient({
tokenUrl: `${config.networkInternalUrl}/oauth/token`, clientId: config.oauth.clientId,
clientSecret: config.oauth.clientSecret, audience: 'openvibe.bench', scope: 'bench.run.publish',
@@@ -192,6 +192,40 @@ function createTasksApi({ config, store, keys, run, harnesses, actorLimits, task
return res.json({ ok: out.ok !== false, machines: latest.machines, agents, at: latest.at, ...(out.problem ? { problem: out.problem } : {}) });
});
++ // One read for a coding harness on the person's machines. Attempts, not the task's initial agent, decide
++ // attribution: a task may switch harnesses midway. Codes has no per-harness memory or schedule records.
++ r.get('/me/agents/:id/sheet', guardAny('codes.task.read'), actorLimits.reads('codes.read'), async (req, res) => {
++ if (!req.principal.owner) return problem(req, res, 403, 'codes.agent_owner_required', 'sign in as the owner to read this agent');
++ const id = String(req.params.id || '');
++ const offer = /^[a-z][a-z0-9-]{0,63}$/.test(id) && harnesses.list().find((item) => item.id === id);
++ if (!offer) return problem(req, res, 404, 'codes.agent_not_found', 'no such coding agent');
++ const fleetRows = fleet ? await fleet.agents(req.principal.subject, harnesses) : [];
++ const availability = fleetRows.find((row) => row.agent === id) || null;
++ const rows = await db.many(`SELECT a.id, a.task_id, a.n, a.state, a.why, a.reason, a.cost_usd,
++ a.machine, a.started_at, a.finished_at, a.created_at, t.prompt, t.repo, t.outcome, t.outcome_note
++ FROM codes_attempts a JOIN run_tasks t ON t.id = a.task_id
++ WHERE t.subject = $1 AND a.agent = $2 ORDER BY a.created_at DESC LIMIT 100`, [req.principal.subject, id]);
++ const work = rows.map((row) => ({
++ id: row.id, task_id: row.task_id, title: String(row.prompt || '').replace(/\s+/g, ' ').slice(0, 240),
++ what: row.why || null, result: row.outcome_note || row.reason || null, state: row.state,
++ outcome: row.outcome || null, cost_usd: row.cost_usd == null ? null : Number(row.cost_usd),
++ machine: row.machine || null, repo: row.repo || null, started_at: row.started_at || row.created_at,
++ finished_at: row.finished_at || null, url: `${config.baseUrl}/tasks/${row.task_id}`,
++ }));
++ const now = work.find((row) => ['queued', 'running'].includes(row.state)) || null;
++ const activity = Object.values(work.reduce((days, row) => {
++ const day = new Date(row.started_at).toISOString().slice(0, 10);
++ (days[day] ||= { day, entries: [] }).entries.push({ task_id: row.task_id, what: row.what || row.title, result: row.result, outcome: row.outcome, cost_usd: row.cost_usd, url: row.url });
++ return days;
++ }, {}));
++ return res.set('Cache-Control', 'private, no-store').json({
++ agent: { id, name: offer.name, face: null, state: now ? 'working' : availability && availability.state || 'idle',
++ kind: 'coding harness', availability },
++ now, work, activity, asks: [], asks_empty: 'Requests to approve coding work will appear here when this agent needs you.',
++ computer: null, schedules: [], memory: [], identity: { provider: offer.provider || null, capabilities: offer.capabilities || {} },
++ });
++ });
++
/**
* The Dock's Code tab start (the home composer's three ways, as JSON): { text, mode, repo?, attachments? }.
* Only the owner starts one this way (a session, or their own Network token); a service keeps the
ddiff --git a/test/agent-sheet.test.js b/test/agent-sheet.test.js
nnew file mode 100644
iindex 0000000..69dca17
---- /dev/null
++++ b/test/agent-sheet.test.js
@@@ -0,0 +1,39 @@
++'use strict';
++const assert = require('assert');
++const { boot, check, done } = require('./helpers/boot');
++
++(async () => {
++ const t = await boot();
++ const ada = t.network.addUser('ada');
++ const ben = t.network.addUser('ben');
++ const db = t.ctx.store.db;
++ try {
++ await db.exec(`INSERT INTO run_tasks (id, subject, project_id, repo, prompt, agent, state, created_at, updated_at)
++ VALUES ('tsk_00000000000000000000000001', $1, 'prj_00000000000000000000000001',
++ 'https://github.com/example/example', 'Repair the build', 'codex', 'succeeded', 1000, 1000)`, [ada.subject]);
++ await db.exec(`INSERT INTO codes_attempts (id, task_id, n, agent, state, kind, cost_usd, why, created_at)
++ VALUES ('att_00000000000000000000000001', 'tsk_00000000000000000000000001', 1,
++ 'codex', 'done', 'first', 0.25, 'Fixed the failing check', now())`);
++ await check('one owner read has the coding agent and its real attempt', async () => {
++ const r = await t.get('/api/v1/me/agents/codex/sheet', { as: ada });
++ assert.strictEqual(r.status, 200, r.text);
++ const sheet = r.json();
++ assert.strictEqual(sheet.agent.id, 'codex');
++ assert.strictEqual(sheet.work.length, 1);
++ assert.strictEqual(sheet.work[0].cost_usd, 0.25);
++ assert.strictEqual(sheet.activity[0].entries[0].what, 'Fixed the failing check');
++ assert.match(sheet.activity[0].day, /^\d{4}-\d{2}-\d{2}$/);
++ assert.deepStrictEqual(sheet.memory, []);
++ assert.deepStrictEqual(sheet.schedules, []);
++ assert.strictEqual(r.headers.get('cache-control'), 'private, no-store');
++ });
++ await check('a different owner and a signed-out caller see no work', async () => {
++ const other = await t.get('/api/v1/me/agents/codex/sheet', { as: ben });
++ assert.strictEqual(other.status, 200);
++ assert.deepStrictEqual(other.json().work, []);
++ assert.strictEqual((await t.get('/api/v1/me/agents/codex/sheet')).status, 401);
++ assert.strictEqual((await t.get('/api/v1/me/agents/unknown-agent/sheet', { as: ada })).status, 404);
++ });
++ } finally { await t.close(); }
++ done();
++})().catch((e) => { console.error(e.stack || e); process.exit(1); });