openvibers/openvibe.codes Commits
Route hosted CLI calls through Exchange (#397)
Changes
@@@ -52,6 +52,8 @@ OV_OAUTH_CLIENT_SECRET=# OpenVibe.Exchange's compatible API: the deepseek harness's default path (a run token names the person; the same# chat-completions API and model ids as OpenVibe.AI). CODES_EXCHANGE_URL overrides https://api.openvibe.exchange.#CODES_EXCHANGE_URL=https://api.openvibe.exchange++# Hosted CLI runs use Exchange by default with their task-scoped run token. Set CODES_CLI_EXCHANGE=0 to roll back to AI.++#CODES_CLI_EXCHANGE=1# Goals and the independent verifier (AL1), off by default. With CODES_GOALS_ENABLED=1 a task may carry a goal and a# typed check; the harness runs the check in the task's workspace and a cheap fast model on OpenVibe.AI accepts or@@@ -100,6 +100,8 @@ function load(env = process.env) {// key and whose budget the run is. Two hours is the longest a coding attempt may run.runTokens: {ttlSeconds: Math.min(7200, Math.max(60, int(env.CODES_RUN_TOKEN_TTL_S, 7200))),++ // Hosted CLI requests use Exchange by default after the production canary; set to 0 to roll back to AI.++ cliExchange: env.CODES_CLI_EXCHANGE == null || bool(env.CODES_CLI_EXCHANGE),// The harness env variable names a job may set (harness/job.js): the model APIs' own names, nothing else.envNames: ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_CUSTOM_HEADERS', 'OPENAI_BASE_URL','OPENAI_API_KEY', 'OPENAI_CUSTOM_HEADERS', 'DEEPSEEK_BASE_URL', 'DEEPSEEK_API_KEY', 'OPENVIBE_AI_URL',@@@ -10,7 +10,8 @@* are, so a run can be traced), never logged and never answered; the transcript normaliser redacts it everywhere a* line is written (server/domain/redact.js).*-- * The environment the harness gets points it at OpenVibe.AI:++ * The environment the harness gets points hosted CLI requests at OpenVibe.Exchange, with the run token as its key.++ * The rollback switch points them at OpenVibe.AI. The harness env names remain the CLI's own API names:* Claude Code ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_CUSTOM_HEADERS* Codex, OpenAI-like OPENAI_BASE_URL, OPENAI_API_KEY* The credential header (x-openvibe-credential) names the provider whose key the run uses, when the person has one.@@@ -31,6 +32,7 @@ const USER = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;/** The harnesses that speak Anthropic's Messages API (the rest speak OpenAI's Chat Completions). */const ANTHROPIC_HARNESSES = ['claude-code', 'claude-other'];++const CLI_HARNESSES = new Set(['claude-code', 'claude-other', 'codex', 'opencode', 'command-code', 'aider', 'minimax-code', 'muse']);/** The claims of a JWT, decoded but not verified (the receiver verifies; this is for the jti and the expiry). */function claims(token) {@@@ -61,7 +63,8 @@ function exchangeBase(config) {* has a key for that provider) the header that says which key.*/function envFor({ config, harness, token, provider, ownKey }) {-- const base = aiBase(config);++ const cliExchange = CLI_HARNESSES.has(harness) && (!config.runTokens || config.runTokens.cliExchange !== false);++ const base = cliExchange ? exchangeBase(config) : aiBase(config);const credential = ownKey && provider ? `x-openvibe-credential: ${provider}` : null;if (ANTHROPIC_HARNESSES.includes(harness)) {return {@@@ -11,6 +11,7 @@ const os = require('os');const path = require('path');const { createHarness, exchangeKeyOf, exchangeModelOf, platformHost } = require('../harness');const { createStore } = require('../harness/sessions');++const { load: loadConfig } = require('../server/config');const { check, done } = require('./helpers/boot');/** A stand-in Exchange: records every request; fails the test if the direct DeepSeek host is called on the default path. */@@@ -120,6 +121,34 @@ const collect = async (run) => { const out = []; for await (const e of run) out.assert.strictEqual(own.DEEPSEEK_API_KEY, 'run-token');});++ await check('hosted CLI run tokens use Exchange for both API shapes and retain the provider selection header', () => {++ const { envFor } = require('../server/domain/run-tokens');++ const config = loadConfig({ CODES_EXCHANGE_URL: 'https://api.openvibe.exchange' });++ assert.strictEqual(config.runTokens.cliExchange, true);++ for (const harness of ['claude-code', 'claude-other']) {++ const env = envFor({ config, harness, token: 'run-token', provider: 'anthropic', ownKey: true });++ assert.strictEqual(env.ANTHROPIC_BASE_URL, 'https://api.openvibe.exchange');++ assert.strictEqual(env.ANTHROPIC_AUTH_TOKEN, 'run-token');++ assert.strictEqual(env.ANTHROPIC_CUSTOM_HEADERS, 'x-openvibe-credential: anthropic');++ }++ for (const harness of ['codex', 'opencode', 'command-code', 'aider', 'minimax-code', 'muse']) {++ const env = envFor({ config, harness, token: 'run-token', provider: 'openai', ownKey: true });++ assert.strictEqual(env.OPENAI_BASE_URL, 'https://api.openvibe.exchange/v1');++ assert.strictEqual(env.OPENAI_API_KEY, 'run-token');++ assert.strictEqual(env.OPENAI_CUSTOM_HEADERS, 'x-openvibe-credential: openai');++ }++ });++++ await check('CODES_CLI_EXCHANGE=0 rolls hosted CLI requests back to OpenVibe.AI', () => {++ const { envFor } = require('../server/domain/run-tokens');++ const config = loadConfig({ CODES_CLI_EXCHANGE: '0' });++ assert.strictEqual(config.runTokens.cliExchange, false);++ const anthropic = envFor({ config, harness: 'claude-code', token: 'run-token', provider: 'anthropic', ownKey: false });++ const openai = envFor({ config, harness: 'codex', token: 'run-token', provider: 'openai', ownKey: false });++ assert.strictEqual(anthropic.ANTHROPIC_BASE_URL, 'https://ai.openvibe.services');++ assert.strictEqual(openai.OPENAI_BASE_URL, 'https://ai.openvibe.services/v1');++ });++await check('without an Exchange key or an own DeepSeek key the agent is unavailable, and the reason names both', () => {const h = createHarness({ env: baseEnv, store: createStore({ env: baseEnv }) });const ds = h.agents().find((a) => a.id === 'deepseek');
@@ -52,6 +52,8 @@ OV_OAUTH_CLIENT_SECRET=
@@ -52,6 +52,8 @@ OV_OAUTH_CLIENT_SECRET=
# OpenVibe.Exchange's compatible API: the deepseek harness's default path (a run token names the person; the same
# OpenVibe.Exchange's compatible API: the deepseek harness's default path (a run token names the person; the same
# chat-completions API and model ids as OpenVibe.AI). CODES_EXCHANGE_URL overrides https://api.openvibe.exchange.
# chat-completions API and model ids as OpenVibe.AI). CODES_EXCHANGE_URL overrides https://api.openvibe.exchange.
#CODES_EXCHANGE_URL=https://api.openvibe.exchange
#CODES_EXCHANGE_URL=https://api.openvibe.exchange
+# Hosted CLI runs use Exchange by default with their task-scoped run token. Set CODES_CLI_EXCHANGE=0 to roll back to AI.
+#CODES_CLI_EXCHANGE=1
# Goals and the independent verifier (AL1), off by default. With CODES_GOALS_ENABLED=1 a task may carry a goal and a
# Goals and the independent verifier (AL1), off by default. With CODES_GOALS_ENABLED=1 a task may carry a goal and a
# typed check; the harness runs the check in the task's workspace and a cheap fast model on OpenVibe.AI accepts or
# typed check; the harness runs the check in the task's workspace and a cheap fast model on OpenVibe.AI accepts or
@@ -100,6 +100,8 @@ function load(env = process.env) {
@@ -100,6 +100,8 @@ function load(env = process.env) {
// key and whose budget the run is. Two hours is the longest a coding attempt may run.
// key and whose budget the run is. Two hours is the longest a coding attempt may run.
runTokens: {
runTokens: {
ttlSeconds: Math.min(7200, Math.max(60, int(env.CODES_RUN_TOKEN_TTL_S, 7200))),
ttlSeconds: Math.min(7200, Math.max(60, int(env.CODES_RUN_TOKEN_TTL_S, 7200))),
+ // Hosted CLI requests use Exchange by default after the production canary; set to 0 to roll back to AI.
+ cliExchange: env.CODES_CLI_EXCHANGE == null || bool(env.CODES_CLI_EXCHANGE),
// The harness env variable names a job may set (harness/job.js): the model APIs' own names, nothing else.
// The harness env variable names a job may set (harness/job.js): the model APIs' own names, nothing else.
envNames: ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_CUSTOM_HEADERS', 'OPENAI_BASE_URL',
envNames: ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_CUSTOM_HEADERS', 'OPENAI_BASE_URL',
'OPENAI_API_KEY', 'OPENAI_CUSTOM_HEADERS', 'DEEPSEEK_BASE_URL', 'DEEPSEEK_API_KEY', 'OPENVIBE_AI_URL',
'OPENAI_API_KEY', 'OPENAI_CUSTOM_HEADERS', 'DEEPSEEK_BASE_URL', 'DEEPSEEK_API_KEY', 'OPENVIBE_AI_URL',
@@ -10,7 +10,8 @@
@@ -10,7 +10,8 @@
* are, so a run can be traced), never logged and never answered; the transcript normaliser redacts it everywhere a
* are, so a run can be traced), never logged and never answered; the transcript normaliser redacts it everywhere a
* line is written (server/domain/redact.js).
* line is written (server/domain/redact.js).
*
*
- * The environment the harness gets points it at OpenVibe.AI:
+ * The environment the harness gets points hosted CLI requests at OpenVibe.Exchange, with the run token as its key.
+ * The rollback switch points them at OpenVibe.AI. The harness env names remain the CLI's own API names:
* Claude Code ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_CUSTOM_HEADERS
* Claude Code ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_CUSTOM_HEADERS
* Codex, OpenAI-like OPENAI_BASE_URL, OPENAI_API_KEY
* Codex, OpenAI-like OPENAI_BASE_URL, OPENAI_API_KEY
* The credential header (x-openvibe-credential) names the provider whose key the run uses, when the person has one.
* The credential header (x-openvibe-credential) names the provider whose key the run uses, when the person has one.
@@ -31,6 +32,7 @@ const USER = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
@@ -31,6 +32,7 @@ const USER = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
/** The harnesses that speak Anthropic's Messages API (the rest speak OpenAI's Chat Completions). */
/** The harnesses that speak Anthropic's Messages API (the rest speak OpenAI's Chat Completions). */
const ANTHROPIC_HARNESSES = ['claude-code', 'claude-other'];
const ANTHROPIC_HARNESSES = ['claude-code', 'claude-other'];
+const CLI_HARNESSES = new Set(['claude-code', 'claude-other', 'codex', 'opencode', 'command-code', 'aider', 'minimax-code', 'muse']);
/** The claims of a JWT, decoded but not verified (the receiver verifies; this is for the jti and the expiry). */
/** The claims of a JWT, decoded but not verified (the receiver verifies; this is for the jti and the expiry). */
function claims(token) {
function claims(token) {
@@ -61,7 +63,8 @@ function exchangeBase(config) {
@@ -61,7 +63,8 @@ function exchangeBase(config) {
* has a key for that provider) the header that says which key.
* has a key for that provider) the header that says which key.
*/
*/
function envFor({ config, harness, token, provider, ownKey }) {
function envFor({ config, harness, token, provider, ownKey }) {
- const base = aiBase(config);
+ const cliExchange = CLI_HARNESSES.has(harness) && (!config.runTokens || config.runTokens.cliExchange !== false);
+ const base = cliExchange ? exchangeBase(config) : aiBase(config);
const credential = ownKey && provider ? `x-openvibe-credential: ${provider}` : null;
const credential = ownKey && provider ? `x-openvibe-credential: ${provider}` : null;
if (ANTHROPIC_HARNESSES.includes(harness)) {
if (ANTHROPIC_HARNESSES.includes(harness)) {
return {
return {
@@ -11,6 +11,7 @@ const os = require('os');
@@ -11,6 +11,7 @@ const os = require('os');
const path = require('path');
const path = require('path');
const { createHarness, exchangeKeyOf, exchangeModelOf, platformHost } = require('../harness');
const { createHarness, exchangeKeyOf, exchangeModelOf, platformHost } = require('../harness');
const { createStore } = require('../harness/sessions');
const { createStore } = require('../harness/sessions');
+const { load: loadConfig } = require('../server/config');
const { check, done } = require('./helpers/boot');
const { check, done } = require('./helpers/boot');
/** A stand-in Exchange: records every request; fails the test if the direct DeepSeek host is called on the default path. */
/** A stand-in Exchange: records every request; fails the test if the direct DeepSeek host is called on the default path. */
@@ -120,6 +121,34 @@ const collect = async (run) => { const out = []; for await (const e of run) out.
@@ -120,6 +121,34 @@ const collect = async (run) => { const out = []; for await (const e of run) out.
assert.strictEqual(own.DEEPSEEK_API_KEY, 'run-token');
assert.strictEqual(own.DEEPSEEK_API_KEY, 'run-token');
});
});
+ await check('hosted CLI run tokens use Exchange for both API shapes and retain the provider selection header', () => {
+ const { envFor } = require('../server/domain/run-tokens');
+ const config = loadConfig({ CODES_EXCHANGE_URL: 'https://api.openvibe.exchange' });
+ assert.strictEqual(config.runTokens.cliExchange, true);
+ for (const harness of ['claude-code', 'claude-other']) {
+ const env = envFor({ config, harness, token: 'run-token', provider: 'anthropic', ownKey: true });
+ assert.strictEqual(env.ANTHROPIC_BASE_URL, 'https://api.openvibe.exchange');
+ assert.strictEqual(env.ANTHROPIC_AUTH_TOKEN, 'run-token');
+ assert.strictEqual(env.ANTHROPIC_CUSTOM_HEADERS, 'x-openvibe-credential: anthropic');
+ }
+ for (const harness of ['codex', 'opencode', 'command-code', 'aider', 'minimax-code', 'muse']) {
+ const env = envFor({ config, harness, token: 'run-token', provider: 'openai', ownKey: true });
+ assert.strictEqual(env.OPENAI_BASE_URL, 'https://api.openvibe.exchange/v1');
+ assert.strictEqual(env.OPENAI_API_KEY, 'run-token');
+ assert.strictEqual(env.OPENAI_CUSTOM_HEADERS, 'x-openvibe-credential: openai');
+ }
+ });
+
+ await check('CODES_CLI_EXCHANGE=0 rolls hosted CLI requests back to OpenVibe.AI', () => {
+ const { envFor } = require('../server/domain/run-tokens');
+ const config = loadConfig({ CODES_CLI_EXCHANGE: '0' });
+ assert.strictEqual(config.runTokens.cliExchange, false);
+ const anthropic = envFor({ config, harness: 'claude-code', token: 'run-token', provider: 'anthropic', ownKey: false });
+ const openai = envFor({ config, harness: 'codex', token: 'run-token', provider: 'openai', ownKey: false });
+ assert.strictEqual(anthropic.ANTHROPIC_BASE_URL, 'https://ai.openvibe.services');
+ assert.strictEqual(openai.OPENAI_BASE_URL, 'https://ai.openvibe.services/v1');
+ });
+
await check('without an Exchange key or an own DeepSeek key the agent is unavailable, and the reason names both', () => {
await check('without an Exchange key or an own DeepSeek key the agent is unavailable, and the reason names both', () => {
const h = createHarness({ env: baseEnv, store: createStore({ env: baseEnv }) });
const h = createHarness({ env: baseEnv, store: createStore({ env: baseEnv }) });
const ds = h.agents().find((a) => a.id === 'deepseek');
const ds = h.agents().find((a) => a.id === 'deepseek');