openvibers/openvibe.codes Commits
Serve the harness catalog and task routing over the API (#7)
Changes
@@@ -33,6 +33,7 @@ const { createReleases } = require('./domain/releases');const { createPlayground } = require('./domain/playground');const { createArchiver } = require('./domain/project-archive');const { createLimitsReader } = require('./domain/limits');++const { createHarnesses } = require('./domain/harnesses');const { createDocsRoutes } = require('./http/docs');const { createToolRoutes } = require('./http/tools');const { createPageRoutes } = require('./http/pages');@@@ -75,7 +76,9 @@ async function createApp(opts = {}) {// Each enforcing service's /limits.json, for /docs/limits and the project usage page.const limits = createLimitsReader();-- const ctx = { config, store, docs, keys, sso, network, outbox, trust, releases, playground, archiver, limits, indexnow, log };++ // The harness catalog (server/data/harness-offers.json), read once: a bad seed row fails boot.++ const harnesses = createHarnesses();++ const ctx = { config, store, docs, keys, sso, network, outbox, trust, releases, playground, archiver, limits, harnesses, indexnow, log };const app = express();app.disable('x-powered-by');@@@ -57,6 +57,8 @@ const BUDGETS = {// Validating a manifest or checking a webhook signature: a developer's pace, not a crawler's.'codes.manifest.validate': { minute: 30, hour: 600 },'codes.webhook.tool': { minute: 30, hour: 600 },++ // Routing a coding task over the harness catalog (no I/O, but a POST: the reads' defaults skip it).++ 'codes.harness.route': { minute: 30, hour: 600 },// Staff setting an app's trust tier.'codes.trust.set': { minute: 30, hour: 300 },};@@@ -5,6 +5,8 @@** GET /docs/versions the pinned versions the docs were generated from* POST /manifests/validate { kind: app|mod, manifest } → { valid, errors, warnings }++ * GET /harnesses the harness catalog, each harness with its agents++ * POST /harnesses/route { task, requirements? } → { selected, reasons, candidates }** codes.release.read (public; no token needed — a token that IS presented must hold it):* GET /apps/:app/releases public releases of an app (never drafts) + trust tier@@@ -25,6 +27,7 @@ const rateLimit = require('express-rate-limit');const { http, serviceAuth } = require('openvibe-contracts');const manifests = require('../domain/manifests');const { ReleaseError } = require('../domain/releases');++const placement = require('../domain/harness-placement');const APP_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;const REL_RE = /^rel_[0-9A-HJKMNP-TV-Z]{26}$/;@@@ -32,7 +35,7 @@ const MANAGE = 'codes.release.manage';const READ = 'codes.release.read';function createApi(ctx) {-- const { config, docs, releases, trust, keys, actorLimits } = ctx;++ const { config, docs, releases, trust, keys, actorLimits, harnesses } = ctx;const r = asyncRouter();// Per-actor limits (http/actor-limits.js): reads take the defaults once the token (if any) is checked;// writes name their budget after the guard and before the body is read.@@@ -55,6 +58,28 @@ function createApi(ctx) {res.status(v.valid ? 200 : 422).json({ ...v, contracts_version: docs.contractsVersion });});++ r.get('/harnesses', reads, (req, res) => {++ res.json({ harnesses: harnesses.list().map((h) => ({ ...h, agents: harnesses.agents(h.id) })) });++ });++++ // Every harness × agent offer with the defaults (health up, no latency); placement runs no I/O.++ r.post('/harnesses/route', limiter, B('codes.harness.route'), json, (req, res) => {++ const b = req.body || {};++ const plain = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);++ if (typeof b.task !== 'string' || !b.task) return problem(req, res, 422, 'harness.invalid', 'task is a non-empty string');++ if (b.requirements !== undefined && !plain(b.requirements)) return problem(req, res, 422, 'harness.invalid', 'requirements is an object');++ const caps = b.requirements && b.requirements.capabilities;++ if (caps !== undefined && !(Array.isArray(caps) && caps.every((c) => typeof c === 'string'))) return problem(req, res, 422, 'harness.invalid', 'requirements.capabilities is an array of strings');++ try {++ res.json(placement.route({ task: b.task, extra: b.requirements || {}, harnesses }));++ } catch (err) {++ if (err && err.code === 'harness.task_unknown') return problem(req, res, 422, 'harness.task_unknown', `task is one of ${Object.keys(placement.TASKS).join(', ')}`);++ const code = err && err.code === 'harness.placement_invalid' ? 'harness.placement_invalid' : 'internal.error';++ ctx.log.error('[Codes] api error:', err && err.message ? err.message.slice(0, 200) : err);++ return problem(req, res, 500, code, 'Internal error');++ }++ });++// Guards: the key the token names is looked up first (the SDK's JWKS client), then the synchronous contracts guard// runs against it.const guardOpts = { issuer: config.networkIssuer, audience: 'openvibe.codes', acceptSandbox: true };nnew file mode 100644@@@ -0,0 +1,77 @@++'use strict';++++/**++ * The harness read API (/api/v1/harnesses): the catalog with its agents, routing a task over the++ * catalog's offers, 422 problems for a bad body or an unknown task, and the per-actor limits on both.++ */++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const { boot, check, done } = require('./helpers/boot');++++const rows = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'server', 'data', 'harness-offers.json'), 'utf8'));++++(async () => {++ const t = await boot();++ const post = (json, headers) => t.get('/api/v1/harnesses/route', { method: 'POST', json, headers });++++ await check('GET /harnesses returns every seed harness with its agents', async () => {++ const r = await t.get('/api/v1/harnesses');++ assert.strictEqual(r.status, 200, r.text.slice(0, 300));++ const { harnesses } = r.json();++ assert.deepStrictEqual(harnesses.map((h) => h.id), rows.map((row) => row.id));++ for (const row of rows) {++ const h = harnesses.find((x) => x.id === row.id);++ // Seed rows without an agent yet (command-code, aider) come back with agents: [].++ assert.deepStrictEqual(h, { ...row, agents: row.agents || [] });++ }++ });++++ await check('POST /harnesses/route {task:review} returns a selected offer and candidates', async () => {++ const r = await post({ task: 'review' });++ assert.strictEqual(r.status, 200, r.text.slice(0, 300));++ const body = r.json();++ assert.strictEqual(typeof body.selected, 'string');++ assert.ok(Array.isArray(body.reasons));++ const agents = rows.reduce((n, row) => n + row.agents.length, 0);++ assert.strictEqual(body.candidates.length, agents);++ assert.ok(body.candidates.some((c) => c.id === body.selected && c.included === true));++ });++++ await check('a bad body is 422 harness.invalid', async () => {++ for (const json of [{}, { task: '' }, { task: 7 }, { task: 'review', requirements: 'x' }, { task: 'review', requirements: [] }, { task: 'review', requirements: { capabilities: 5 } }]) {++ const r = await post(json);++ assert.deepStrictEqual([r.status, r.json().code], [422, 'harness.invalid'], JSON.stringify(json));++ assert.ok(/^application\/problem\+json/.test(r.headers.get('content-type')));++ }++ });++++ await check('an unknown task is 422 harness.task_unknown', async () => {++ const r = await post({ task: 'nope' });++ assert.deepStrictEqual([r.status, r.json().code], [422, 'harness.task_unknown']);++ });++ await t.close();++++ // The per-actor limits (http/actor-limits.js), as test/actor-limits.test.js asserts them.++ const clock = Date.UTC(2026, 8, 27, 12, 0, 15);++ const l = await boot({ actorLimits: true, limitsNow: () => clock, env: { CODES_LIMITS_MINUTE: '3', CODES_LIMITS_HOUR: '100' } });++ const from = (ip, o = {}) => ({ ...o, headers: { 'X-Forwarded-For': ip } });++++ await check('GET /harnesses takes the read limit: 3 a minute per caller, then 429 rate_limited', async () => {++ for (let i = 0; i < 3; i++) assert.strictEqual((await l.get('/api/v1/harnesses', from('203.0.113.7'))).status, 200);++ const r = await l.get('/api/v1/harnesses', from('203.0.113.7'));++ assert.deepStrictEqual([r.status, r.json().code], [429, 'rate_limited']);++ assert.strictEqual((await l.get('/api/v1/harnesses', from('203.0.113.8'))).status, 200, 'another address still passes');++ });++++ await check('POST /harnesses/route has its own budget: 30 a minute per caller, then 429 rate_limited', async () => {++ const route = (ip) => l.get('/api/v1/harnesses/route', from(ip, { method: 'POST', json: { task: 'review' } }));++ for (let i = 0; i < 30; i++) assert.strictEqual((await route('203.0.113.9')).status, 200);++ const r = await route('203.0.113.9');++ assert.deepStrictEqual([r.status, r.json().code], [429, 'rate_limited']);++ assert.ok(r.json().detail.includes('codes.harness.route'), r.json().detail);++ assert.strictEqual((await route('203.0.113.10')).status, 200, 'another address still passes');++ });++ await l.close();++ done();++})();
@@ -33,6 +33,7 @@ const { createReleases } = require('./domain/releases');
@@ -33,6 +33,7 @@ const { createReleases } = require('./domain/releases');
const { createPlayground } = require('./domain/playground');
const { createPlayground } = require('./domain/playground');
const { createArchiver } = require('./domain/project-archive');
const { createArchiver } = require('./domain/project-archive');
const { createLimitsReader } = require('./domain/limits');
const { createLimitsReader } = require('./domain/limits');
+const { createHarnesses } = require('./domain/harnesses');
const { createDocsRoutes } = require('./http/docs');
const { createDocsRoutes } = require('./http/docs');
const { createToolRoutes } = require('./http/tools');
const { createToolRoutes } = require('./http/tools');
const { createPageRoutes } = require('./http/pages');
const { createPageRoutes } = require('./http/pages');
@@ -75,7 +76,9 @@ async function createApp(opts = {}) {
@@ -75,7 +76,9 @@ async function createApp(opts = {}) {
// Each enforcing service's /limits.json, for /docs/limits and the project usage page.
// Each enforcing service's /limits.json, for /docs/limits and the project usage page.
const limits = createLimitsReader();
const limits = createLimitsReader();
- const ctx = { config, store, docs, keys, sso, network, outbox, trust, releases, playground, archiver, limits, indexnow, log };
+ // The harness catalog (server/data/harness-offers.json), read once: a bad seed row fails boot.
+ const harnesses = createHarnesses();
+ const ctx = { config, store, docs, keys, sso, network, outbox, trust, releases, playground, archiver, limits, harnesses, indexnow, log };
const app = express();
const app = express();
app.disable('x-powered-by');
app.disable('x-powered-by');
@@ -57,6 +57,8 @@ const BUDGETS = {
@@ -57,6 +57,8 @@ const BUDGETS = {
// Validating a manifest or checking a webhook signature: a developer's pace, not a crawler's.
// Validating a manifest or checking a webhook signature: a developer's pace, not a crawler's.
'codes.manifest.validate': { minute: 30, hour: 600 },
'codes.manifest.validate': { minute: 30, hour: 600 },
'codes.webhook.tool': { minute: 30, hour: 600 },
'codes.webhook.tool': { minute: 30, hour: 600 },
+ // Routing a coding task over the harness catalog (no I/O, but a POST: the reads' defaults skip it).
+ 'codes.harness.route': { minute: 30, hour: 600 },
// Staff setting an app's trust tier.
// Staff setting an app's trust tier.
'codes.trust.set': { minute: 30, hour: 300 },
'codes.trust.set': { minute: 30, hour: 300 },
};
};
@@ -5,6 +5,8 @@
@@ -5,6 +5,8 @@
*
*
* GET /docs/versions the pinned versions the docs were generated from
* GET /docs/versions the pinned versions the docs were generated from
* POST /manifests/validate { kind: app|mod, manifest } → { valid, errors, warnings }
* POST /manifests/validate { kind: app|mod, manifest } → { valid, errors, warnings }
+ * GET /harnesses the harness catalog, each harness with its agents
+ * POST /harnesses/route { task, requirements? } → { selected, reasons, candidates }
*
*
* codes.release.read (public; no token needed — a token that IS presented must hold it):
* codes.release.read (public; no token needed — a token that IS presented must hold it):
* GET /apps/:app/releases public releases of an app (never drafts) + trust tier
* GET /apps/:app/releases public releases of an app (never drafts) + trust tier
@@ -25,6 +27,7 @@ const rateLimit = require('express-rate-limit');
@@ -25,6 +27,7 @@ const rateLimit = require('express-rate-limit');
const { http, serviceAuth } = require('openvibe-contracts');
const { http, serviceAuth } = require('openvibe-contracts');
const manifests = require('../domain/manifests');
const manifests = require('../domain/manifests');
const { ReleaseError } = require('../domain/releases');
const { ReleaseError } = require('../domain/releases');
+const placement = require('../domain/harness-placement');
const APP_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
const APP_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
const REL_RE = /^rel_[0-9A-HJKMNP-TV-Z]{26}$/;
const REL_RE = /^rel_[0-9A-HJKMNP-TV-Z]{26}$/;
@@ -32,7 +35,7 @@ const MANAGE = 'codes.release.manage';
@@ -32,7 +35,7 @@ const MANAGE = 'codes.release.manage';
const READ = 'codes.release.read';
const READ = 'codes.release.read';
function createApi(ctx) {
function createApi(ctx) {
- const { config, docs, releases, trust, keys, actorLimits } = ctx;
+ const { config, docs, releases, trust, keys, actorLimits, harnesses } = ctx;
const r = asyncRouter();
const r = asyncRouter();
// Per-actor limits (http/actor-limits.js): reads take the defaults once the token (if any) is checked;
// Per-actor limits (http/actor-limits.js): reads take the defaults once the token (if any) is checked;
// writes name their budget after the guard and before the body is read.
// writes name their budget after the guard and before the body is read.
@@ -55,6 +58,28 @@ function createApi(ctx) {
@@ -55,6 +58,28 @@ function createApi(ctx) {
res.status(v.valid ? 200 : 422).json({ ...v, contracts_version: docs.contractsVersion });
res.status(v.valid ? 200 : 422).json({ ...v, contracts_version: docs.contractsVersion });
});
});
+ r.get('/harnesses', reads, (req, res) => {
+ res.json({ harnesses: harnesses.list().map((h) => ({ ...h, agents: harnesses.agents(h.id) })) });
+ });
+
+ // Every harness × agent offer with the defaults (health up, no latency); placement runs no I/O.
+ r.post('/harnesses/route', limiter, B('codes.harness.route'), json, (req, res) => {
+ const b = req.body || {};
+ const plain = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
+ if (typeof b.task !== 'string' || !b.task) return problem(req, res, 422, 'harness.invalid', 'task is a non-empty string');
+ if (b.requirements !== undefined && !plain(b.requirements)) return problem(req, res, 422, 'harness.invalid', 'requirements is an object');
+ const caps = b.requirements && b.requirements.capabilities;
+ if (caps !== undefined && !(Array.isArray(caps) && caps.every((c) => typeof c === 'string'))) return problem(req, res, 422, 'harness.invalid', 'requirements.capabilities is an array of strings');
+ try {
+ res.json(placement.route({ task: b.task, extra: b.requirements || {}, harnesses }));
+ } catch (err) {
+ if (err && err.code === 'harness.task_unknown') return problem(req, res, 422, 'harness.task_unknown', `task is one of ${Object.keys(placement.TASKS).join(', ')}`);
+ const code = err && err.code === 'harness.placement_invalid' ? 'harness.placement_invalid' : 'internal.error';
+ ctx.log.error('[Codes] api error:', err && err.message ? err.message.slice(0, 200) : err);
+ return problem(req, res, 500, code, 'Internal error');
+ }
+ });
+
// Guards: the key the token names is looked up first (the SDK's JWKS client), then the synchronous contracts guard
// Guards: the key the token names is looked up first (the SDK's JWKS client), then the synchronous contracts guard
// runs against it.
// runs against it.
const guardOpts = { issuer: config.networkIssuer, audience: 'openvibe.codes', acceptSandbox: true };
const guardOpts = { issuer: config.networkIssuer, audience: 'openvibe.codes', acceptSandbox: true };
new file mode 100644
new file mode 100644
@@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
+'use strict';
+
+/**
+ * The harness read API (/api/v1/harnesses): the catalog with its agents, routing a task over the
+ * catalog's offers, 422 problems for a bad body or an unknown task, and the per-actor limits on both.
+ */
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { boot, check, done } = require('./helpers/boot');
+
+const rows = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'server', 'data', 'harness-offers.json'), 'utf8'));
+
+(async () => {
+ const t = await boot();
+ const post = (json, headers) => t.get('/api/v1/harnesses/route', { method: 'POST', json, headers });
+
+ await check('GET /harnesses returns every seed harness with its agents', async () => {
+ const r = await t.get('/api/v1/harnesses');
+ assert.strictEqual(r.status, 200, r.text.slice(0, 300));
+ const { harnesses } = r.json();
+ assert.deepStrictEqual(harnesses.map((h) => h.id), rows.map((row) => row.id));
+ for (const row of rows) {
+ const h = harnesses.find((x) => x.id === row.id);
+ // Seed rows without an agent yet (command-code, aider) come back with agents: [].
+ assert.deepStrictEqual(h, { ...row, agents: row.agents || [] });
+ }
+ });
+
+ await check('POST /harnesses/route {task:review} returns a selected offer and candidates', async () => {
+ const r = await post({ task: 'review' });
+ assert.strictEqual(r.status, 200, r.text.slice(0, 300));
+ const body = r.json();
+ assert.strictEqual(typeof body.selected, 'string');
+ assert.ok(Array.isArray(body.reasons));
+ const agents = rows.reduce((n, row) => n + row.agents.length, 0);
+ assert.strictEqual(body.candidates.length, agents);
+ assert.ok(body.candidates.some((c) => c.id === body.selected && c.included === true));
+ });
+
+ await check('a bad body is 422 harness.invalid', async () => {
+ for (const json of [{}, { task: '' }, { task: 7 }, { task: 'review', requirements: 'x' }, { task: 'review', requirements: [] }, { task: 'review', requirements: { capabilities: 5 } }]) {
+ const r = await post(json);
+ assert.deepStrictEqual([r.status, r.json().code], [422, 'harness.invalid'], JSON.stringify(json));
+ assert.ok(/^application\/problem\+json/.test(r.headers.get('content-type')));
+ }
+ });
+
+ await check('an unknown task is 422 harness.task_unknown', async () => {
+ const r = await post({ task: 'nope' });
+ assert.deepStrictEqual([r.status, r.json().code], [422, 'harness.task_unknown']);
+ });
+ await t.close();
+
+ // The per-actor limits (http/actor-limits.js), as test/actor-limits.test.js asserts them.
+ const clock = Date.UTC(2026, 8, 27, 12, 0, 15);
+ const l = await boot({ actorLimits: true, limitsNow: () => clock, env: { CODES_LIMITS_MINUTE: '3', CODES_LIMITS_HOUR: '100' } });
+ const from = (ip, o = {}) => ({ ...o, headers: { 'X-Forwarded-For': ip } });
+
+ await check('GET /harnesses takes the read limit: 3 a minute per caller, then 429 rate_limited', async () => {
+ for (let i = 0; i < 3; i++) assert.strictEqual((await l.get('/api/v1/harnesses', from('203.0.113.7'))).status, 200);
+ const r = await l.get('/api/v1/harnesses', from('203.0.113.7'));
+ assert.deepStrictEqual([r.status, r.json().code], [429, 'rate_limited']);
+ assert.strictEqual((await l.get('/api/v1/harnesses', from('203.0.113.8'))).status, 200, 'another address still passes');
+ });
+
+ await check('POST /harnesses/route has its own budget: 30 a minute per caller, then 429 rate_limited', async () => {
+ const route = (ip) => l.get('/api/v1/harnesses/route', from(ip, { method: 'POST', json: { task: 'review' } }));
+ for (let i = 0; i < 30; i++) assert.strictEqual((await route('203.0.113.9')).status, 200);
+ const r = await route('203.0.113.9');
+ assert.deepStrictEqual([r.status, r.json().code], [429, 'rate_limited']);
+ assert.ok(r.json().detail.includes('codes.harness.route'), r.json().detail);
+ assert.strictEqual((await route('203.0.113.10')).status, 200, 'another address still passes');
+ });
+ await l.close();
+ done();
+})();