openvibers/openvibe.codes Commits
Publish harness offers as Fabric resource offers of kind harness (#9)
Changes
@@@ -48,7 +48,7 @@ Everything is server-rendered and usable without JavaScript. The only scripts of- **OpenVibe.Events** — the outbox relay publishes `codes.app.*` with Codes' service token (`events.event.publish`); the Events playground calls it with the app's token (`events.app.publish`); the project archive pulls the project's app events with a Network export token (`events.app.read`).- **OpenVibe.Media** — the Media playground uploads with the app's token into the project's namespace; the project archive lists the project's objects, namespaces and download URLs with a Network export token (`media.object.list`, `media.object.read`).- **PostgreSQL 18 and Valkey 9** (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through `openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional).--- **openvibe-contracts** v0.84.0, **openvibe-sdk v0.26.0**, **openvibe-shared v2.4.0** (pinned tag tarballs; the docs show the tag and the package version, and say so when they differ).++- **openvibe-contracts** v0.86.0, **openvibe-sdk v0.26.0**, **openvibe-shared v2.4.0** (pinned tag tarballs; the docs show the tag and the package version, and say so when they differ).No path in Codes sends or accepts a shared loopback key (tested by grep and at runtime).@@@ -69,7 +69,7 @@ In Network (`server/identity/principals.js` / `server/db/database.js`, as for co- OAuth client `codes`, name `OpenVibe.Codes`, redirect URI `https://openvibe.codes/auth/callback`.- Service grant `['codes', 'events.event.publish', 'openvibe.events', []]` (the outbox relay).--The Codes service manifest, `codes.release.manage|read` and `codes.app-manifest@1` are released in openvibe-contracts (tag v0.27.0; this repository pins v0.84.0); the CI contracts check is blocking.++The Codes service manifest, `codes.release.manage|read` and `codes.app-manifest@1` are released in openvibe-contracts (tag v0.27.0; this repository pins v0.86.0); the CI contracts check is blocking.For the playgrounds to succeed end to end (not Codes' code; configuration elsewhere): Network `DEV_SANDBOX_AUDIENCES` including `openvibe.media` and `openvibe.events`, a staff-set allowance containing `media.object.upload` and `events.app.publish` for the project, a Media tenant keyed by the project id, and OpenVibe.Events serving `events.app.publish` for app tokens. On 2026-09-23 these were in place on production: a sandbox app uploaded to Media (tenant `prj_…-sandbox`) and published and read app events through public endpoints. That run used curl, not the Codes playgrounds, and it is not yet a committed, repeatable check.@@@ -7,9 +7,9 @@"runtime": "node 22, express 4, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9","port": 4900,"domain": "openvibe.codes",-- "contracts": "openvibe-contracts v0.84.0",++ "contracts": "openvibe-contracts v0.86.0","packages": [-- "openvibe-contracts v0.84.0",++ "openvibe-contracts v0.86.0","openvibe-sdk v0.26.0","openvibe-shared v2.4.0"],@@@ -30,7 +30,7 @@"Policy pages rendered from the published ADRs; licensing read from package metadata; governance pages (code of conduct, contributing, contributor ladder, moderation policy) rendered from the repository's Markdown","Release API guarded by openvibe-contracts requireCapability (codes.release.manage, codes.release.read)","Readiness (/api/ready), /release.json and loopback-only /metrics",-- "Coding-harness registry: a validated catalog of harnesses and their agents (capabilities, address, price and limits), a task placement adapter over openvibe-sdk/placement, and a public read API and read-only docs page (GET /api/v1/harnesses, POST /api/v1/harnesses/route, /docs/harnesses)"++ "Coding-harness registry: a validated catalog of harnesses and their agents (capabilities, address, price and limits), a task placement adapter over openvibe-sdk/placement, and a public read API and read-only docs page (GET /api/v1/harnesses, POST /api/v1/harnesses/route, /docs/harnesses); each harness × agent pair is routed as a platform.resource-offer@1 of kind harness with task:<kind> and harness:<flag> capabilities and a platform.harness-offer@1 detail"],"notYet": ["The playgrounds have never been used on production (0 playground runs); the one production end-to-end run used curl, and it is not a committed, repeatable check",@@@ -16,7 +16,7 @@"express-rate-limit": "^7.5.0","helmet": "^8.1.0","iovalkey": "^0.4.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.84.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.4.0","pg": "^8.23.0"@@@ -1157,9 +1157,8 @@}},"node_modules/openvibe-contracts": {-- "version": "0.84.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.84.0",-- "integrity": "sha512-wnmQbzffd2xXSGk3Zr1JxZYIL4oSlnJTo8lGpUApBPoDKeQ3U4VCLJgt3cZpbtkbl5NH1qNjBDXkwx+uASwoyA==",++ "version": "0.86.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -28,7 +28,7 @@"express-rate-limit": "^7.5.0","helmet": "^8.1.0","iovalkey": "^0.4.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.84.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.4.0","pg": "^8.23.0"@@@ -7,33 +7,42 @@const contracts = require('openvibe-contracts');const { plan } = require('openvibe-sdk/placement');--// Every coding harness offers edit and review until task capabilities reach harness-offer@1.--const BASE_CAPABILITIES = ['edit', 'review'];--const HARNESS_CAPABILITIES = ['host_access', 'mcp', 'long_autonomy', 'resume'];++// A catalog row without task_capabilities takes edit and review.++const DEFAULT_TASKS = ['edit', 'review'];++// harness-offer@1 capability flag → the resource-offer@1 capability it adds when true.++const HARNESS_FLAGS = { host_access: 'harness:host-access', mcp: 'harness:mcp', long_autonomy: 'harness:long-autonomy', resume: 'harness:resume' };const TASKS = Object.freeze({-- edit: ['edit'],-- review: ['review'],-- long: ['edit', 'long_autonomy'],-- resume: ['resume'],++ edit: ['task:edit'],++ review: ['task:review'],++ long: ['task:edit', 'harness:long-autonomy'],++ resume: ['harness:resume'],});++/** One platform.resource-offer@1 of kind harness; its detail is the catalog row as platform.harness-offer@1. */function toOffer(harness, agent, opts = {}) {-- const caps = harness.capabilities || {};-- const limits = harness.limits || {};++ const { agents, ...row } = harness;++ const caps = row.capabilities || {};++ const limits = row.limits || {};++ const tasks = row.task_capabilities || DEFAULT_TASKS;++ const maxContextTokens = (agent.context_limits && agent.context_limits.input_tokens) ?? limits.max_context_tokens;const health = typeof opts.health === 'string' ? { status: opts.health } : opts.health || { status: 'up' };const offer = {offer_id: `${harness.id}:${agent.id || agent.model}`,-- kind: 'provider',++ kind: 'harness',provider: agent.provider || harness.provider,region: 'global',trust: opts.trust || 'external',-- capabilities: [...BASE_CAPABILITIES, ...HARNESS_CAPABILITIES.filter((name) => caps[name] === true)],++ capabilities: [...tasks.map((task) => `task:${task}`), ...Object.keys(HARNESS_FLAGS).filter((name) => caps[name] === true).map((name) => HARNESS_FLAGS[name])],capacity: { workers: { harness: limits.max_concurrent_runs } },health,pricing: { model: 'per-operation', unit: 'token', marginal_usd_per_unit: agent.price_per_1k_tokens.fresh_usd / 1000 },-- // Not an SDK Offer field: the planner ignores it; kept for callers sizing a run.-- max_context_tokens: (agent.context_limits && agent.context_limits.input_tokens) ?? limits.max_context_tokens,++ updated_at: new Date(opts.now ?? Date.now()).toISOString(),++ detail: {++ ...row,++ limits: maxContextTokens == null ? limits : { ...limits, max_context_tokens: maxContextTokens },++ task_capabilities: [...tasks],++ },};if (opts.latencyMs != null) offer.latency_ms = { run_p95: opts.latencyMs };return offer;@@@ -61,10 +70,10 @@ function route({ task, extra = {}, harnesses, opts = {}, now = Date.now() }) {const req = requirementsFor(task, extra);const offers = harnesses.list().flatMap((harness) => harnesses.agents(harness.id).map((agent) => {const id = `${harness.id}:${agent.id || agent.model}`;-- return toOffer(harness, agent, opts[id] || {});++ return toOffer(harness, agent, { now, ...opts[id] });}));const result = plan(req, offers, { now });-- // placement-result@1 types `selected` as a string, so a plan with no eligible candidate cannot validate.++ // The Contracts compat gate refused widening placement-result@1 `selected` to null: a plan with nothing selected skips the check.if (result.selected !== null) {const check = contracts.validate('platform.placement-result@1', JSON.parse(JSON.stringify(result)));if (!check.valid) {@@@ -19,11 +19,11 @@ const sdkPkg = require('openvibe-sdk/package.json');await check('package.json pins the contracts, SDK and shared tags that are installed', async () => {const tag = (dep) => (pkg.dependencies[dep].match(/refs\/tags\/(v[0-9.]+)$/) || [])[1];-- assert.strictEqual(tag('openvibe-contracts'), 'v0.84.0');++ assert.strictEqual(tag('openvibe-contracts'), 'v0.86.0');assert.strictEqual(tag('openvibe-sdk'), `v${sdkPkg.version}`);assert.strictEqual(tag('openvibe-shared'), `v${require('openvibe-shared/package.json').version}`);assert.strictEqual(sdkPkg.version, '0.26.0');-- // The contracts tag v0.84.0 carries the released codes manifest and codes.app-manifest@1.++ // The contracts tag v0.86.0 carries the released codes manifest and codes.app-manifest@1.assert.ok(contracts.services.get('codes') && contracts.services.get('codes').status === 'alpha');assert.ok(contracts.resolve('codes.app-manifest@1'));});@@@ -33,13 +33,13 @@ const sdkPkg = require('openvibe-sdk/package.json');const r = await t.get(p);assert.strictEqual(r.status, 200, p);assert.ok(r.text.includes(`openvibe-contracts v${contractsVersion}`), `${p} names the installed contracts version`);-- assert.ok(r.text.includes('OpenVibe.Contracts/tree/v0.84.0'), `${p} links the pinned tag`);-- if (contractsVersion !== '0.84.0') assert.ok(r.text.includes('(tag v0.84.0)'), `${p} says the tag differs from the package version`);++ assert.ok(r.text.includes('OpenVibe.Contracts/tree/v0.86.0'), `${p} links the pinned tag`);++ if (contractsVersion !== '0.86.0') assert.ok(r.text.includes('(tag v0.86.0)'), `${p} says the tag differs from the package version`);assert.ok(r.text.includes(`openvibe-sdk v${sdkPkg.version}`), `${p} names the SDK version`);}const v = (await t.get('/api/v1/docs/versions')).json();assert.strictEqual(v.contracts, contractsVersion);-- assert.strictEqual(v.contracts_tag, 'v0.84.0');++ assert.strictEqual(v.contracts_tag, 'v0.86.0');assert.strictEqual(v.sdk, sdkPkg.version);});@@@ -3,6 +3,7 @@const assert = require('assert');const fs = require('fs');const path = require('path');++const contracts = require('openvibe-contracts');const { createHarnesses } = require('../server/domain/harnesses');const { TASKS, toOffer, requirementsFor, route } = require('../server/domain/harness-placement');const { check, done } = require('./helpers/boot');@@@ -31,35 +32,55 @@ function priced(row, freshUsd) {context_limits: { input_tokens: 100000, output_tokens: 8000 },price_per_1k_tokens: { fresh_usd: 0.003, cached_usd: 0.0003, output_usd: 0.015 },};-- assert.deepStrictEqual(toOffer(harness, agent, { trust: 'partner', health: 'degraded', latencyMs: 900 }), {++ const { agents, ...row } = harness;++ assert.deepStrictEqual(toOffer(harness, agent, { trust: 'partner', health: 'degraded', latencyMs: 900, now }), {offer_id: 'command-code:command-code-test',-- kind: 'provider',++ kind: 'harness',provider: 'example',region: 'global',trust: 'partner',-- capabilities: ['edit', 'review', 'host_access', 'mcp', 'resume'],++ capabilities: ['task:edit', 'task:review', 'harness:host-access', 'harness:mcp', 'harness:resume'],capacity: { workers: { harness: 2 } },health: { status: 'degraded' },pricing: { model: 'per-operation', unit: 'token', marginal_usd_per_unit: 0.003 / 1000 },-- max_context_tokens: 100000,++ updated_at: '2026-10-02T12:00:00.000Z',++ detail: { ...row, limits: { ...row.limits, max_context_tokens: 100000 }, task_capabilities: ['edit', 'review'] },latency_ms: { run_p95: 900 },});const deepseek = rows.find((row) => row.id === 'deepseek');const offer = toOffer(deepseek, deepseek.agents[0]);assert.strictEqual(offer.offer_id, 'deepseek:deepseek-chat');-- assert.deepStrictEqual(offer.capabilities, ['edit', 'review']);++ assert.deepStrictEqual(offer.capabilities, ['task:edit', 'task:review']);assert.strictEqual(offer.trust, 'external');assert.deepStrictEqual(offer.health, { status: 'up' });assert.strictEqual('latency_ms' in offer, false);-- assert.strictEqual(offer.max_context_tokens, 64000);++ assert.strictEqual('max_context_tokens' in offer, false);++ assert.strictEqual(offer.detail.limits.max_context_tokens, 64000);++ assert.strictEqual('agents' in offer.detail, false);assert.strictEqual(toOffer(deepseek, { ...deepseek.agents[0], id: undefined }).offer_id, `deepseek:${deepseek.agents[0].model}`);});++ await check('every seed offer validates as platform.resource-offer@1 of kind harness', async () => {++ const harnesses = createHarnesses({ catalogPath });++ const offers = harnesses.list().flatMap((harness) => harnesses.agents(harness.id).map((agent) => [harness, toOffer(harness, agent, { now })]));++ assert.strictEqual(offers.length, 3);++ for (const [harness, offer] of offers) {++ const result = contracts.validate('platform.resource-offer@1', JSON.parse(JSON.stringify(offer)));++ assert.ok(result.valid, `${offer.offer_id}: ${JSON.stringify(result.errors)}`);++ assert.strictEqual(offer.detail.id, harness.id);++ }++ const browsing = { ...rows[0], task_capabilities: ['edit', 'browse'] };++ const offer = toOffer(browsing, browsing.agents[0], { now });++ assert.deepStrictEqual(offer.capabilities, ['task:edit', 'task:browse', 'harness:host-access', 'harness:mcp', 'harness:long-autonomy', 'harness:resume']);++ assert.ok(contracts.validate('platform.resource-offer@1', JSON.parse(JSON.stringify(offer))).valid);++ });++await check('requirementsFor maps each task and merges extras', async () => {assert.deepStrictEqual(requirementsFor('long').capabilities, TASKS.long);-- const req = requirementsFor('edit', { trust: 'first-party', capabilities: ['mcp'] });-- assert.deepStrictEqual(req.capabilities, ['edit', 'mcp']);++ assert.deepStrictEqual(requirementsFor('review').capabilities, ['task:review']);++ const req = requirementsFor('edit', { trust: 'first-party', capabilities: ['harness:mcp'] });++ assert.deepStrictEqual(req.capabilities, ['task:edit', 'harness:mcp']);assert.deepStrictEqual(req.trust, ['first-party']);assert.strictEqual(req.objective, 'balanced');});@@@ -69,7 +90,7 @@ function priced(row, freshUsd) {const r = route({ task: 'review', harnesses, now });assert.ok(r.selected);const [harnessId] = r.selected.split(':');-- assert.ok(toOffer(harnesses.get(harnessId), harnesses.agents(harnessId)[0]).capabilities.includes('review'));++ assert.ok(toOffer(harnesses.get(harnessId), harnesses.agents(harnessId)[0]).capabilities.includes('task:review'));assert.ok(r.reasons.length);assert.deepStrictEqual(r.candidates.map((c) => c.id), ['claude-code:claude-code-sonnet', 'codex:codex-default', 'deepseek:deepseek-chat']);});@@@ -104,6 +125,17 @@ function priced(row, freshUsd) {assert.strictEqual(flipped.selected, 'claude-code:claude-code-sonnet');});++ await check('a task:browse requirement excludes harnesses without it', async () => {++ const [claude, codex] = rows;++ const harnesses = stub([{ ...claude, task_capabilities: ['edit', 'review', 'browse'] }, codex]);++ const r = route({ task: 'edit', extra: { capabilities: ['task:browse'] }, harnesses, now });++ assert.strictEqual(r.selected, 'claude-code:claude-code-sonnet');++ assert.deepStrictEqual(r.candidates.find((c) => c.id === 'codex:codex-default'), { id: 'codex:codex-default', included: false, reason: 'lacks task:browse' });++ const none = route({ task: 'edit', extra: { capabilities: ['task:browse'] }, harnesses: createHarnesses({ catalogPath }), now });++ assert.strictEqual(none.selected, null);++ assert.ok(none.candidates.every((c) => c.reason === 'lacks task:browse'));++ });++await check('an unknown task throws harness.task_unknown', async () => {assert.throws(() => requirementsFor('deploy'), (error) => error.code === 'harness.task_unknown');assert.throws(() => requirementsFor('toString'), (error) => error.code === 'harness.task_unknown');
@@ -48,7 +48,7 @@ Everything is server-rendered and usable without JavaScript. The only scripts of
@@ -48,7 +48,7 @@ Everything is server-rendered and usable without JavaScript. The only scripts of
- **OpenVibe.Events** — the outbox relay publishes `codes.app.*` with Codes' service token (`events.event.publish`); the Events playground calls it with the app's token (`events.app.publish`); the project archive pulls the project's app events with a Network export token (`events.app.read`).
- **OpenVibe.Events** — the outbox relay publishes `codes.app.*` with Codes' service token (`events.event.publish`); the Events playground calls it with the app's token (`events.app.publish`); the project archive pulls the project's app events with a Network export token (`events.app.read`).
- **OpenVibe.Media** — the Media playground uploads with the app's token into the project's namespace; the project archive lists the project's objects, namespaces and download URLs with a Network export token (`media.object.list`, `media.object.read`).
- **OpenVibe.Media** — the Media playground uploads with the app's token into the project's namespace; the project archive lists the project's objects, namespaces and download URLs with a Network export token (`media.object.list`, `media.object.read`).
- **PostgreSQL 18 and Valkey 9** (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through `openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional).
- **PostgreSQL 18 and Valkey 9** (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through `openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional).
-- **openvibe-contracts** v0.84.0, **openvibe-sdk v0.26.0**, **openvibe-shared v2.4.0** (pinned tag tarballs; the docs show the tag and the package version, and say so when they differ).
+- **openvibe-contracts** v0.86.0, **openvibe-sdk v0.26.0**, **openvibe-shared v2.4.0** (pinned tag tarballs; the docs show the tag and the package version, and say so when they differ).
No path in Codes sends or accepts a shared loopback key (tested by grep and at runtime).
No path in Codes sends or accepts a shared loopback key (tested by grep and at runtime).
@@ -69,7 +69,7 @@ In Network (`server/identity/principals.js` / `server/db/database.js`, as for co
@@ -69,7 +69,7 @@ In Network (`server/identity/principals.js` / `server/db/database.js`, as for co
- OAuth client `codes`, name `OpenVibe.Codes`, redirect URI `https://openvibe.codes/auth/callback`.
- OAuth client `codes`, name `OpenVibe.Codes`, redirect URI `https://openvibe.codes/auth/callback`.
- Service grant `['codes', 'events.event.publish', 'openvibe.events', []]` (the outbox relay).
- Service grant `['codes', 'events.event.publish', 'openvibe.events', []]` (the outbox relay).
-The Codes service manifest, `codes.release.manage|read` and `codes.app-manifest@1` are released in openvibe-contracts (tag v0.27.0; this repository pins v0.84.0); the CI contracts check is blocking.
+The Codes service manifest, `codes.release.manage|read` and `codes.app-manifest@1` are released in openvibe-contracts (tag v0.27.0; this repository pins v0.86.0); the CI contracts check is blocking.
For the playgrounds to succeed end to end (not Codes' code; configuration elsewhere): Network `DEV_SANDBOX_AUDIENCES` including `openvibe.media` and `openvibe.events`, a staff-set allowance containing `media.object.upload` and `events.app.publish` for the project, a Media tenant keyed by the project id, and OpenVibe.Events serving `events.app.publish` for app tokens. On 2026-09-23 these were in place on production: a sandbox app uploaded to Media (tenant `prj_…-sandbox`) and published and read app events through public endpoints. That run used curl, not the Codes playgrounds, and it is not yet a committed, repeatable check.
For the playgrounds to succeed end to end (not Codes' code; configuration elsewhere): Network `DEV_SANDBOX_AUDIENCES` including `openvibe.media` and `openvibe.events`, a staff-set allowance containing `media.object.upload` and `events.app.publish` for the project, a Media tenant keyed by the project id, and OpenVibe.Events serving `events.app.publish` for app tokens. On 2026-09-23 these were in place on production: a sandbox app uploaded to Media (tenant `prj_…-sandbox`) and published and read app events through public endpoints. That run used curl, not the Codes playgrounds, and it is not yet a committed, repeatable check.
@@ -7,9 +7,9 @@
@@ -7,9 +7,9 @@
"runtime": "node 22, express 4, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"runtime": "node 22, express 4, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": 4900,
"port": 4900,
"domain": "openvibe.codes",
"domain": "openvibe.codes",
- "contracts": "openvibe-contracts v0.84.0",
+ "contracts": "openvibe-contracts v0.86.0",
"packages": [
"packages": [
- "openvibe-contracts v0.84.0",
+ "openvibe-contracts v0.86.0",
"openvibe-sdk v0.26.0",
"openvibe-sdk v0.26.0",
"openvibe-shared v2.4.0"
"openvibe-shared v2.4.0"
],
],
@@ -30,7 +30,7 @@
@@ -30,7 +30,7 @@
"Policy pages rendered from the published ADRs; licensing read from package metadata; governance pages (code of conduct, contributing, contributor ladder, moderation policy) rendered from the repository's Markdown",
"Policy pages rendered from the published ADRs; licensing read from package metadata; governance pages (code of conduct, contributing, contributor ladder, moderation policy) rendered from the repository's Markdown",
"Release API guarded by openvibe-contracts requireCapability (codes.release.manage, codes.release.read)",
"Release API guarded by openvibe-contracts requireCapability (codes.release.manage, codes.release.read)",
"Readiness (/api/ready), /release.json and loopback-only /metrics",
"Readiness (/api/ready), /release.json and loopback-only /metrics",
- "Coding-harness registry: a validated catalog of harnesses and their agents (capabilities, address, price and limits), a task placement adapter over openvibe-sdk/placement, and a public read API and read-only docs page (GET /api/v1/harnesses, POST /api/v1/harnesses/route, /docs/harnesses)"
+ "Coding-harness registry: a validated catalog of harnesses and their agents (capabilities, address, price and limits), a task placement adapter over openvibe-sdk/placement, and a public read API and read-only docs page (GET /api/v1/harnesses, POST /api/v1/harnesses/route, /docs/harnesses); each harness × agent pair is routed as a platform.resource-offer@1 of kind harness with task:<kind> and harness:<flag> capabilities and a platform.harness-offer@1 detail"
],
],
"notYet": [
"notYet": [
"The playgrounds have never been used on production (0 playground runs); the one production end-to-end run used curl, and it is not a committed, repeatable check",
"The playgrounds have never been used on production (0 playground runs); the one production end-to-end run used curl, and it is not a committed, repeatable check",
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.84.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.4.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.4.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
@@ -1157,9 +1157,8 @@
@@ -1157,9 +1157,8 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.84.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.84.0",
- "integrity": "sha512-wnmQbzffd2xXSGk3Zr1JxZYIL4oSlnJTo8lGpUApBPoDKeQ3U4VCLJgt3cZpbtkbl5NH1qNjBDXkwx+uASwoyA==",
+ "version": "0.86.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -28,7 +28,7 @@
@@ -28,7 +28,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.84.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.86.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.4.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.4.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
@@ -7,33 +7,42 @@
@@ -7,33 +7,42 @@
const contracts = require('openvibe-contracts');
const contracts = require('openvibe-contracts');
const { plan } = require('openvibe-sdk/placement');
const { plan } = require('openvibe-sdk/placement');
-// Every coding harness offers edit and review until task capabilities reach harness-offer@1.
-const BASE_CAPABILITIES = ['edit', 'review'];
-const HARNESS_CAPABILITIES = ['host_access', 'mcp', 'long_autonomy', 'resume'];
+// A catalog row without task_capabilities takes edit and review.
+const DEFAULT_TASKS = ['edit', 'review'];
+// harness-offer@1 capability flag → the resource-offer@1 capability it adds when true.
+const HARNESS_FLAGS = { host_access: 'harness:host-access', mcp: 'harness:mcp', long_autonomy: 'harness:long-autonomy', resume: 'harness:resume' };
const TASKS = Object.freeze({
const TASKS = Object.freeze({
- edit: ['edit'],
- review: ['review'],
- long: ['edit', 'long_autonomy'],
- resume: ['resume'],
+ edit: ['task:edit'],
+ review: ['task:review'],
+ long: ['task:edit', 'harness:long-autonomy'],
+ resume: ['harness:resume'],
});
});
+/** One platform.resource-offer@1 of kind harness; its detail is the catalog row as platform.harness-offer@1. */
function toOffer(harness, agent, opts = {}) {
function toOffer(harness, agent, opts = {}) {
- const caps = harness.capabilities || {};
- const limits = harness.limits || {};
+ const { agents, ...row } = harness;
+ const caps = row.capabilities || {};
+ const limits = row.limits || {};
+ const tasks = row.task_capabilities || DEFAULT_TASKS;
+ const maxContextTokens = (agent.context_limits && agent.context_limits.input_tokens) ?? limits.max_context_tokens;
const health = typeof opts.health === 'string' ? { status: opts.health } : opts.health || { status: 'up' };
const health = typeof opts.health === 'string' ? { status: opts.health } : opts.health || { status: 'up' };
const offer = {
const offer = {
offer_id: `${harness.id}:${agent.id || agent.model}`,
offer_id: `${harness.id}:${agent.id || agent.model}`,
- kind: 'provider',
+ kind: 'harness',
provider: agent.provider || harness.provider,
provider: agent.provider || harness.provider,
region: 'global',
region: 'global',
trust: opts.trust || 'external',
trust: opts.trust || 'external',
- capabilities: [...BASE_CAPABILITIES, ...HARNESS_CAPABILITIES.filter((name) => caps[name] === true)],
+ capabilities: [...tasks.map((task) => `task:${task}`), ...Object.keys(HARNESS_FLAGS).filter((name) => caps[name] === true).map((name) => HARNESS_FLAGS[name])],
capacity: { workers: { harness: limits.max_concurrent_runs } },
capacity: { workers: { harness: limits.max_concurrent_runs } },
health,
health,
pricing: { model: 'per-operation', unit: 'token', marginal_usd_per_unit: agent.price_per_1k_tokens.fresh_usd / 1000 },
pricing: { model: 'per-operation', unit: 'token', marginal_usd_per_unit: agent.price_per_1k_tokens.fresh_usd / 1000 },
- // Not an SDK Offer field: the planner ignores it; kept for callers sizing a run.
- max_context_tokens: (agent.context_limits && agent.context_limits.input_tokens) ?? limits.max_context_tokens,
+ updated_at: new Date(opts.now ?? Date.now()).toISOString(),
+ detail: {
+ ...row,
+ limits: maxContextTokens == null ? limits : { ...limits, max_context_tokens: maxContextTokens },
+ task_capabilities: [...tasks],
+ },
};
};
if (opts.latencyMs != null) offer.latency_ms = { run_p95: opts.latencyMs };
if (opts.latencyMs != null) offer.latency_ms = { run_p95: opts.latencyMs };
return offer;
return offer;
@@ -61,10 +70,10 @@ function route({ task, extra = {}, harnesses, opts = {}, now = Date.now() }) {
@@ -61,10 +70,10 @@ function route({ task, extra = {}, harnesses, opts = {}, now = Date.now() }) {
const req = requirementsFor(task, extra);
const req = requirementsFor(task, extra);
const offers = harnesses.list().flatMap((harness) => harnesses.agents(harness.id).map((agent) => {
const offers = harnesses.list().flatMap((harness) => harnesses.agents(harness.id).map((agent) => {
const id = `${harness.id}:${agent.id || agent.model}`;
const id = `${harness.id}:${agent.id || agent.model}`;
- return toOffer(harness, agent, opts[id] || {});
+ return toOffer(harness, agent, { now, ...opts[id] });
}));
}));
const result = plan(req, offers, { now });
const result = plan(req, offers, { now });
- // placement-result@1 types `selected` as a string, so a plan with no eligible candidate cannot validate.
+ // The Contracts compat gate refused widening placement-result@1 `selected` to null: a plan with nothing selected skips the check.
if (result.selected !== null) {
if (result.selected !== null) {
const check = contracts.validate('platform.placement-result@1', JSON.parse(JSON.stringify(result)));
const check = contracts.validate('platform.placement-result@1', JSON.parse(JSON.stringify(result)));
if (!check.valid) {
if (!check.valid) {
@@ -19,11 +19,11 @@ const sdkPkg = require('openvibe-sdk/package.json');
@@ -19,11 +19,11 @@ const sdkPkg = require('openvibe-sdk/package.json');
await check('package.json pins the contracts, SDK and shared tags that are installed', async () => {
await check('package.json pins the contracts, SDK and shared tags that are installed', async () => {
const tag = (dep) => (pkg.dependencies[dep].match(/refs\/tags\/(v[0-9.]+)$/) || [])[1];
const tag = (dep) => (pkg.dependencies[dep].match(/refs\/tags\/(v[0-9.]+)$/) || [])[1];
- assert.strictEqual(tag('openvibe-contracts'), 'v0.84.0');
+ assert.strictEqual(tag('openvibe-contracts'), 'v0.86.0');
assert.strictEqual(tag('openvibe-sdk'), `v${sdkPkg.version}`);
assert.strictEqual(tag('openvibe-sdk'), `v${sdkPkg.version}`);
assert.strictEqual(tag('openvibe-shared'), `v${require('openvibe-shared/package.json').version}`);
assert.strictEqual(tag('openvibe-shared'), `v${require('openvibe-shared/package.json').version}`);
assert.strictEqual(sdkPkg.version, '0.26.0');
assert.strictEqual(sdkPkg.version, '0.26.0');
- // The contracts tag v0.84.0 carries the released codes manifest and codes.app-manifest@1.
+ // The contracts tag v0.86.0 carries the released codes manifest and codes.app-manifest@1.
assert.ok(contracts.services.get('codes') && contracts.services.get('codes').status === 'alpha');
assert.ok(contracts.services.get('codes') && contracts.services.get('codes').status === 'alpha');
assert.ok(contracts.resolve('codes.app-manifest@1'));
assert.ok(contracts.resolve('codes.app-manifest@1'));
});
});
@@ -33,13 +33,13 @@ const sdkPkg = require('openvibe-sdk/package.json');
@@ -33,13 +33,13 @@ const sdkPkg = require('openvibe-sdk/package.json');
const r = await t.get(p);
const r = await t.get(p);
assert.strictEqual(r.status, 200, p);
assert.strictEqual(r.status, 200, p);
assert.ok(r.text.includes(`openvibe-contracts v${contractsVersion}`), `${p} names the installed contracts version`);
assert.ok(r.text.includes(`openvibe-contracts v${contractsVersion}`), `${p} names the installed contracts version`);
- assert.ok(r.text.includes('OpenVibe.Contracts/tree/v0.84.0'), `${p} links the pinned tag`);
- if (contractsVersion !== '0.84.0') assert.ok(r.text.includes('(tag v0.84.0)'), `${p} says the tag differs from the package version`);
+ assert.ok(r.text.includes('OpenVibe.Contracts/tree/v0.86.0'), `${p} links the pinned tag`);
+ if (contractsVersion !== '0.86.0') assert.ok(r.text.includes('(tag v0.86.0)'), `${p} says the tag differs from the package version`);
assert.ok(r.text.includes(`openvibe-sdk v${sdkPkg.version}`), `${p} names the SDK version`);
assert.ok(r.text.includes(`openvibe-sdk v${sdkPkg.version}`), `${p} names the SDK version`);
}
}
const v = (await t.get('/api/v1/docs/versions')).json();
const v = (await t.get('/api/v1/docs/versions')).json();
assert.strictEqual(v.contracts, contractsVersion);
assert.strictEqual(v.contracts, contractsVersion);
- assert.strictEqual(v.contracts_tag, 'v0.84.0');
+ assert.strictEqual(v.contracts_tag, 'v0.86.0');
assert.strictEqual(v.sdk, sdkPkg.version);
assert.strictEqual(v.sdk, sdkPkg.version);
});
});
@@ -3,6 +3,7 @@
@@ -3,6 +3,7 @@
const assert = require('assert');
const assert = require('assert');
const fs = require('fs');
const fs = require('fs');
const path = require('path');
const path = require('path');
+const contracts = require('openvibe-contracts');
const { createHarnesses } = require('../server/domain/harnesses');
const { createHarnesses } = require('../server/domain/harnesses');
const { TASKS, toOffer, requirementsFor, route } = require('../server/domain/harness-placement');
const { TASKS, toOffer, requirementsFor, route } = require('../server/domain/harness-placement');
const { check, done } = require('./helpers/boot');
const { check, done } = require('./helpers/boot');
@@ -31,35 +32,55 @@ function priced(row, freshUsd) {
@@ -31,35 +32,55 @@ function priced(row, freshUsd) {
context_limits: { input_tokens: 100000, output_tokens: 8000 },
context_limits: { input_tokens: 100000, output_tokens: 8000 },
price_per_1k_tokens: { fresh_usd: 0.003, cached_usd: 0.0003, output_usd: 0.015 },
price_per_1k_tokens: { fresh_usd: 0.003, cached_usd: 0.0003, output_usd: 0.015 },
};
};
- assert.deepStrictEqual(toOffer(harness, agent, { trust: 'partner', health: 'degraded', latencyMs: 900 }), {
+ const { agents, ...row } = harness;
+ assert.deepStrictEqual(toOffer(harness, agent, { trust: 'partner', health: 'degraded', latencyMs: 900, now }), {
offer_id: 'command-code:command-code-test',
offer_id: 'command-code:command-code-test',
- kind: 'provider',
+ kind: 'harness',
provider: 'example',
provider: 'example',
region: 'global',
region: 'global',
trust: 'partner',
trust: 'partner',
- capabilities: ['edit', 'review', 'host_access', 'mcp', 'resume'],
+ capabilities: ['task:edit', 'task:review', 'harness:host-access', 'harness:mcp', 'harness:resume'],
capacity: { workers: { harness: 2 } },
capacity: { workers: { harness: 2 } },
health: { status: 'degraded' },
health: { status: 'degraded' },
pricing: { model: 'per-operation', unit: 'token', marginal_usd_per_unit: 0.003 / 1000 },
pricing: { model: 'per-operation', unit: 'token', marginal_usd_per_unit: 0.003 / 1000 },
- max_context_tokens: 100000,
+ updated_at: '2026-10-02T12:00:00.000Z',
+ detail: { ...row, limits: { ...row.limits, max_context_tokens: 100000 }, task_capabilities: ['edit', 'review'] },
latency_ms: { run_p95: 900 },
latency_ms: { run_p95: 900 },
});
});
const deepseek = rows.find((row) => row.id === 'deepseek');
const deepseek = rows.find((row) => row.id === 'deepseek');
const offer = toOffer(deepseek, deepseek.agents[0]);
const offer = toOffer(deepseek, deepseek.agents[0]);
assert.strictEqual(offer.offer_id, 'deepseek:deepseek-chat');
assert.strictEqual(offer.offer_id, 'deepseek:deepseek-chat');
- assert.deepStrictEqual(offer.capabilities, ['edit', 'review']);
+ assert.deepStrictEqual(offer.capabilities, ['task:edit', 'task:review']);
assert.strictEqual(offer.trust, 'external');
assert.strictEqual(offer.trust, 'external');
assert.deepStrictEqual(offer.health, { status: 'up' });
assert.deepStrictEqual(offer.health, { status: 'up' });
assert.strictEqual('latency_ms' in offer, false);
assert.strictEqual('latency_ms' in offer, false);
- assert.strictEqual(offer.max_context_tokens, 64000);
+ assert.strictEqual('max_context_tokens' in offer, false);
+ assert.strictEqual(offer.detail.limits.max_context_tokens, 64000);
+ assert.strictEqual('agents' in offer.detail, false);
assert.strictEqual(toOffer(deepseek, { ...deepseek.agents[0], id: undefined }).offer_id, `deepseek:${deepseek.agents[0].model}`);
assert.strictEqual(toOffer(deepseek, { ...deepseek.agents[0], id: undefined }).offer_id, `deepseek:${deepseek.agents[0].model}`);
});
});
+ await check('every seed offer validates as platform.resource-offer@1 of kind harness', async () => {
+ const harnesses = createHarnesses({ catalogPath });
+ const offers = harnesses.list().flatMap((harness) => harnesses.agents(harness.id).map((agent) => [harness, toOffer(harness, agent, { now })]));
+ assert.strictEqual(offers.length, 3);
+ for (const [harness, offer] of offers) {
+ const result = contracts.validate('platform.resource-offer@1', JSON.parse(JSON.stringify(offer)));
+ assert.ok(result.valid, `${offer.offer_id}: ${JSON.stringify(result.errors)}`);
+ assert.strictEqual(offer.detail.id, harness.id);
+ }
+ const browsing = { ...rows[0], task_capabilities: ['edit', 'browse'] };
+ const offer = toOffer(browsing, browsing.agents[0], { now });
+ assert.deepStrictEqual(offer.capabilities, ['task:edit', 'task:browse', 'harness:host-access', 'harness:mcp', 'harness:long-autonomy', 'harness:resume']);
+ assert.ok(contracts.validate('platform.resource-offer@1', JSON.parse(JSON.stringify(offer))).valid);
+ });
+
await check('requirementsFor maps each task and merges extras', async () => {
await check('requirementsFor maps each task and merges extras', async () => {
assert.deepStrictEqual(requirementsFor('long').capabilities, TASKS.long);
assert.deepStrictEqual(requirementsFor('long').capabilities, TASKS.long);
- const req = requirementsFor('edit', { trust: 'first-party', capabilities: ['mcp'] });
- assert.deepStrictEqual(req.capabilities, ['edit', 'mcp']);
+ assert.deepStrictEqual(requirementsFor('review').capabilities, ['task:review']);
+ const req = requirementsFor('edit', { trust: 'first-party', capabilities: ['harness:mcp'] });
+ assert.deepStrictEqual(req.capabilities, ['task:edit', 'harness:mcp']);
assert.deepStrictEqual(req.trust, ['first-party']);
assert.deepStrictEqual(req.trust, ['first-party']);
assert.strictEqual(req.objective, 'balanced');
assert.strictEqual(req.objective, 'balanced');
});
});
@@ -69,7 +90,7 @@ function priced(row, freshUsd) {
@@ -69,7 +90,7 @@ function priced(row, freshUsd) {
const r = route({ task: 'review', harnesses, now });
const r = route({ task: 'review', harnesses, now });
assert.ok(r.selected);
assert.ok(r.selected);
const [harnessId] = r.selected.split(':');
const [harnessId] = r.selected.split(':');
- assert.ok(toOffer(harnesses.get(harnessId), harnesses.agents(harnessId)[0]).capabilities.includes('review'));
+ assert.ok(toOffer(harnesses.get(harnessId), harnesses.agents(harnessId)[0]).capabilities.includes('task:review'));
assert.ok(r.reasons.length);
assert.ok(r.reasons.length);
assert.deepStrictEqual(r.candidates.map((c) => c.id), ['claude-code:claude-code-sonnet', 'codex:codex-default', 'deepseek:deepseek-chat']);
assert.deepStrictEqual(r.candidates.map((c) => c.id), ['claude-code:claude-code-sonnet', 'codex:codex-default', 'deepseek:deepseek-chat']);
});
});
@@ -104,6 +125,17 @@ function priced(row, freshUsd) {
@@ -104,6 +125,17 @@ function priced(row, freshUsd) {
assert.strictEqual(flipped.selected, 'claude-code:claude-code-sonnet');
assert.strictEqual(flipped.selected, 'claude-code:claude-code-sonnet');
});
});
+ await check('a task:browse requirement excludes harnesses without it', async () => {
+ const [claude, codex] = rows;
+ const harnesses = stub([{ ...claude, task_capabilities: ['edit', 'review', 'browse'] }, codex]);
+ const r = route({ task: 'edit', extra: { capabilities: ['task:browse'] }, harnesses, now });
+ assert.strictEqual(r.selected, 'claude-code:claude-code-sonnet');
+ assert.deepStrictEqual(r.candidates.find((c) => c.id === 'codex:codex-default'), { id: 'codex:codex-default', included: false, reason: 'lacks task:browse' });
+ const none = route({ task: 'edit', extra: { capabilities: ['task:browse'] }, harnesses: createHarnesses({ catalogPath }), now });
+ assert.strictEqual(none.selected, null);
+ assert.ok(none.candidates.every((c) => c.reason === 'lacks task:browse'));
+ });
+
await check('an unknown task throws harness.task_unknown', async () => {
await check('an unknown task throws harness.task_unknown', async () => {
assert.throws(() => requirementsFor('deploy'), (error) => error.code === 'harness.task_unknown');
assert.throws(() => requirementsFor('deploy'), (error) => error.code === 'harness.task_unknown');
assert.throws(() => requirementsFor('toString'), (error) => error.code === 'harness.task_unknown');
assert.throws(() => requirementsFor('toString'), (error) => error.code === 'harness.task_unknown');