openvibers/openvibe.codes Commits
Codes CLI 0.6.1: private repositories clone with a token the machine holds, never through OpenVibe (#220)
Changes
@@@ -58,7 +58,8 @@"user JSON API for signed-in people to list, start, read and cancel their own tasks","proof of a finished task: its tests, screenshots, diff and a second-model review, and the outcome in plain words","the conductor (C9b): one Claude Code session on the person's own machine, driven from the browser or the phone, with its approvals",-- "the job runner (CLI 0.6.0): each turn at the effort a person picks, in each agent's own words, and a conversation's next turn starting from the work before it (the last diff applied with git apply --3way, and the conversation so far), on any agent"++ "the job runner (CLI 0.6.0): each turn at the effort a person picks, in each agent's own words, and a conversation's next turn starting from the work before it (the last diff applied with git apply --3way, and the conversation so far), on any agent",++ "the job runner (CLI 0.6.1): \"edit\" means edits on the person's own computer (OpenCode and Command Code run unattended only in a Node sandbox or for full; Aider suggests no shell commands there), and a private repository clones with a token the machine holds (GITHUB_TOKEN, GITLAB_TOKEN, OPENVIBE_GIT_CREDENTIALS) sent as a header, never in the URL or .git/config"],"updated": "2026-10-31"}@@@ -65,6 +65,48 @@ function pushTarget(requested, base, { exists = false, now = Date.now() } = {})}/** Any credential an URL or a message might carry, replaced. */++/**++ * A private repository clones with a token the machine holds (the owner, 2026-11-01: "some people might have private++ * Gits"). The tokens are the person's, in their Node's config for this runner (docs/worker.md): they never pass through++ * OpenVibe. Inside the job's sandbox nothing else of theirs is reachable, so these are the only way in:++ *++ * GITHUB_TOKEN github.com (the same token that pushes the branch)++ * GITLAB_TOKEN gitlab.com++ * OPENVIBE_GIT_CREDENTIALS any host, one per line in git's credential-store form: https://user:token@host++ *++ * The token goes to git as an Authorization header for that origin only, through GIT_CONFIG_* variables: never in the++ * command line, never in the clone's remote URL, never in .git/config the agent can read.++ * → { env } to add to git's environment, or {} when no token is for this host.++ */++function gitAuthFor(repo, env = process.env) {++ let u;++ try { u = new URL(String(repo || '')); } catch { return {}; }++ if (u.protocol !== 'https:') return {};++ const host = u.host.toLowerCase();++ const creds = new Map();++ if (env.GITHUB_TOKEN) creds.set('github.com', { user: 'x-access-token', token: String(env.GITHUB_TOKEN) });++ if (env.GITLAB_TOKEN) creds.set('gitlab.com', { user: 'oauth2', token: String(env.GITLAB_TOKEN) });++ String(env.OPENVIBE_GIT_CREDENTIALS || '').split(/[\n,]+/).map((l) => l.trim()).filter(Boolean).forEach((line) => {++ try {++ const c = new URL(line);++ if (c.protocol === 'https:' && c.password) creds.set(c.host.toLowerCase(), { user: decodeURIComponent(c.username || 'git'), token: decodeURIComponent(c.password) });++ } catch { /* a line that is not https://user:token@host is skipped */ }++ });++ const c = creds.get(host);++ if (!c || !c.token || /[\r\n]/.test(c.token + c.user)) return {};++ const basic = Buffer.from(`${c.user}:${c.token}`).toString('base64');++ return { env: { GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: `http.https://${host}/.extraheader`, GIT_CONFIG_VALUE_0: `Authorization: Basic ${basic}` } };++}++++/** Why a clone failed, in words a person can act on (a private repository with no token for its host). */++function cloneProblem(message, repo) {++ const m = String(message || '');++ if (!/could not read Username|Authentication failed|terminal prompts disabled|403|401|not found/i.test(m)) return m;++ let host = 'its host';++ try { host = new URL(String(repo)).host; } catch { /* keep the words */ }++ return `${m} — if the repository is private, give this machine's Codes runner a token for ${host} (GITHUB_TOKEN, GITLAB_TOKEN, or an OPENVIBE_GIT_CREDENTIALS line https://you:token@${host}) in the Node's config.json env, then restart the Node`;++}++function scrub(s) {return String(s).replace(/(https?:\/\/)[^@/\s]+@/g, '$1***@').replace(/gh[pousr]_[A-Za-z0-9]{20,}/g, '***');}@@@ -463,7 +505,11 @@ async function runJob({ input, harness, emit, writeResult, env = process.env, tmlet adapterOptions = null;if (job.kind === 'code') {say({ t: 'step', text: `Cloning ${scrub(job.repo)}${job.ref ? ` (${job.ref})` : ''}` });-- await git(['clone', '--depth', '50', ...(job.ref ? ['--branch', job.ref] : []), '--', job.repo, work], { timeout: 300_000 });++ const auth = gitAuthFor(job.repo, env);++ try {++ await git(['clone', '--depth', '50', ...(job.ref ? ['--branch', job.ref] : []), '--', job.repo, work], { timeout: 300_000, ...(auth.env ? { env: auth.env } : {}) });++ } catch (e) { throw new Error(cloneProblem(e.message, job.repo)); }++ if (auth.env) say({ t: 'step', text: 'Cloned with this machine\'s token for a private repository' });base = (await git(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: work })).trim();result.base = base;say({ t: 'step', text: `Working on ${base}` });@@@ -594,4 +640,4 @@ function resultWriter(emit) {};}--module.exports = { runJob, parseArgs, compact, sessionLog, stepInput, resultWriter, scrub, githubRepo, pushTarget, jobEnv, ENV_NAMES, JOB_PROTOCOL, codesToken, conductorMcpConfig, writeAttachments };++module.exports = { gitAuthFor, cloneProblem, runJob, parseArgs, compact, sessionLog, stepInput, resultWriter, scrub, githubRepo, pushTarget, jobEnv, ENV_NAMES, JOB_PROTOCOL, codesToken, conductorMcpConfig, writeAttachments };@@@ -1,12 +1,12 @@{"name": "openvibe-codes",-- "version": "0.6.0",++ "version": "0.6.1","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-codes",-- "version": "0.6.0",++ "version": "0.6.1","license": "AGPL-3.0-only","dependencies": {"cookie-parser": "^1.4.7",@@@ -1,6 +1,6 @@{"name": "openvibe-codes",-- "version": "0.6.0",++ "version": "0.6.1","private": true,"description": "OpenVibe.Codes: the open coding-agent harness. Run a coding task on Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API or your own model, with routing by capability and cost and hand-offs between agents; plus the site with the agent catalog, a public routing API and the way to improve OpenVibe itself.","main": "harness/index.js",nnew file mode 100644@@@ -0,0 +1,48 @@++'use strict';++/**++ * Private repositories (the owner, 2026-11-01: "some people might have private Gits"): a job clones with a token the++ * machine holds (GITHUB_TOKEN, GITLAB_TOKEN, or an OPENVIBE_GIT_CREDENTIALS line https://user:token@host from the Node's++ * config), as an Authorization header for that origin only, through GIT_CONFIG_* variables — never on the command++ * line, never in the remote URL or .git/config. A failed clone of a private repository says what to do.++ */++const assert = require('assert');++const { execFileSync } = require('child_process');++const { check, done } = require('./helpers/boot');++const { gitAuthFor, cloneProblem } = require('../harness/job');++++const header = (auth) => auth.env && auth.env.GIT_CONFIG_VALUE_0;++const basicOf = (auth) => Buffer.from(String(header(auth)).replace(/^Authorization: Basic /, ''), 'base64').toString();++++(async () => {++ await check('a GitHub, GitLab or any-host token becomes a header for that origin only', () => {++ const env = { GITHUB_TOKEN: 'ghp_fake0123456789abcdefghij', GITLAB_TOKEN: 'glpat-fake', OPENVIBE_GIT_CREDENTIALS: 'https://ada:tok%[email protected]\nhttps://bob:[email protected]' };++ const gh = gitAuthFor('https://github.com/someone/private-thing', env);++ assert.strictEqual(gh.env.GIT_CONFIG_COUNT, '1');++ assert.strictEqual(gh.env.GIT_CONFIG_KEY_0, 'http.https://github.com/.extraheader');++ assert.strictEqual(basicOf(gh), 'x-access-token:ghp_fake0123456789abcdefghij');++ assert.strictEqual(basicOf(gitAuthFor('https://gitlab.com/group/sub/project.git', env)), 'oauth2:glpat-fake');++ assert.strictEqual(basicOf(gitAuthFor('https://git.openvibe.codes/ada/site', env)), 'ada:tok/en', 'a credential line, decoded');++ assert.strictEqual(basicOf(gitAuthFor('https://code.example.org/team/app', env)), 'bob:pw');++ assert.deepStrictEqual(gitAuthFor('https://bitbucket.org/x/y', env), {}, 'no token for that host, no header');++ assert.deepStrictEqual(gitAuthFor('http://github.com/x/y', env), {}, 'never over plain http');++ assert.deepStrictEqual(gitAuthFor('https://github.com/x/y', {}), {}, 'a machine with no token sends none');++ assert.deepStrictEqual(gitAuthFor('https://evil.example/x', { OPENVIBE_GIT_CREDENTIALS: 'https://a:[email protected]/%0d%0aX-Injected: 1' }).env ? 'header' : 'none', 'header', 'the path of a line is not part of the header');++ assert.deepStrictEqual(gitAuthFor('https://evil.example/x', { OPENVIBE_GIT_CREDENTIALS: 'https://a%0d%0aX:[email protected]' }), {}, 'a line that would break the header is refused');++ assert.deepStrictEqual(gitAuthFor('not a url', env), {});++ });++++ await check('the installed git reads the header from the environment, and nothing writes it to a config file', () => {++ const auth = gitAuthFor('https://github.com/someone/private-thing', { GITHUB_TOKEN: 'ghp_fake0123456789abcdefghij' });++ const got = execFileSync('git', ['config', '--get', 'http.https://github.com/.extraheader'], { env: { ...process.env, ...auth.env }, encoding: 'utf8' }).trim();++ assert.strictEqual(got, header(auth));++ assert.ok(!Object.values(auth.env).some((v) => v.includes('ghp_fake')), 'the raw token is in no variable: only its Basic form');++ });++++ await check('a failed clone of a private repository says what to do, and other failures are left as they are', () => {++ const said = cloneProblem("fatal: could not read Username for 'https://github.com': terminal prompts disabled", 'https://github.com/someone/private-thing');++ assert.match(said, /if the repository is private, give this machine's Codes runner a token for github\.com/);++ assert.match(said, /GITHUB_TOKEN, GITLAB_TOKEN, or an OPENVIBE_GIT_CREDENTIALS line https:\/\/you:token@github\.com/);++ assert.strictEqual(cloneProblem('fatal: unable to access: Could not resolve host', 'https://x.y/a'), 'fatal: unable to access: Could not resolve host');++ });++ done();++})().catch((e) => { console.error(e); process.exit(1); });
@@ -58,7 +58,8 @@
@@ -58,7 +58,8 @@
"user JSON API for signed-in people to list, start, read and cancel their own tasks",
"user JSON API for signed-in people to list, start, read and cancel their own tasks",
"proof of a finished task: its tests, screenshots, diff and a second-model review, and the outcome in plain words",
"proof of a finished task: its tests, screenshots, diff and a second-model review, and the outcome in plain words",
"the conductor (C9b): one Claude Code session on the person's own machine, driven from the browser or the phone, with its approvals",
"the conductor (C9b): one Claude Code session on the person's own machine, driven from the browser or the phone, with its approvals",
- "the job runner (CLI 0.6.0): each turn at the effort a person picks, in each agent's own words, and a conversation's next turn starting from the work before it (the last diff applied with git apply --3way, and the conversation so far), on any agent"
+ "the job runner (CLI 0.6.0): each turn at the effort a person picks, in each agent's own words, and a conversation's next turn starting from the work before it (the last diff applied with git apply --3way, and the conversation so far), on any agent",
+ "the job runner (CLI 0.6.1): \"edit\" means edits on the person's own computer (OpenCode and Command Code run unattended only in a Node sandbox or for full; Aider suggests no shell commands there), and a private repository clones with a token the machine holds (GITHUB_TOKEN, GITLAB_TOKEN, OPENVIBE_GIT_CREDENTIALS) sent as a header, never in the URL or .git/config"
],
],
"updated": "2026-10-31"
"updated": "2026-10-31"
}
}
@@ -65,6 +65,48 @@ function pushTarget(requested, base, { exists = false, now = Date.now() } = {})
@@ -65,6 +65,48 @@ function pushTarget(requested, base, { exists = false, now = Date.now() } = {})
}
}
/** Any credential an URL or a message might carry, replaced. */
/** Any credential an URL or a message might carry, replaced. */
+/**
+ * A private repository clones with a token the machine holds (the owner, 2026-11-01: "some people might have private
+ * Gits"). The tokens are the person's, in their Node's config for this runner (docs/worker.md): they never pass through
+ * OpenVibe. Inside the job's sandbox nothing else of theirs is reachable, so these are the only way in:
+ *
+ * GITHUB_TOKEN github.com (the same token that pushes the branch)
+ * GITLAB_TOKEN gitlab.com
+ * OPENVIBE_GIT_CREDENTIALS any host, one per line in git's credential-store form: https://user:token@host
+ *
+ * The token goes to git as an Authorization header for that origin only, through GIT_CONFIG_* variables: never in the
+ * command line, never in the clone's remote URL, never in .git/config the agent can read.
+ * → { env } to add to git's environment, or {} when no token is for this host.
+ */
+function gitAuthFor(repo, env = process.env) {
+ let u;
+ try { u = new URL(String(repo || '')); } catch { return {}; }
+ if (u.protocol !== 'https:') return {};
+ const host = u.host.toLowerCase();
+ const creds = new Map();
+ if (env.GITHUB_TOKEN) creds.set('github.com', { user: 'x-access-token', token: String(env.GITHUB_TOKEN) });
+ if (env.GITLAB_TOKEN) creds.set('gitlab.com', { user: 'oauth2', token: String(env.GITLAB_TOKEN) });
+ String(env.OPENVIBE_GIT_CREDENTIALS || '').split(/[\n,]+/).map((l) => l.trim()).filter(Boolean).forEach((line) => {
+ try {
+ const c = new URL(line);
+ if (c.protocol === 'https:' && c.password) creds.set(c.host.toLowerCase(), { user: decodeURIComponent(c.username || 'git'), token: decodeURIComponent(c.password) });
+ } catch { /* a line that is not https://user:token@host is skipped */ }
+ });
+ const c = creds.get(host);
+ if (!c || !c.token || /[\r\n]/.test(c.token + c.user)) return {};
+ const basic = Buffer.from(`${c.user}:${c.token}`).toString('base64');
+ return { env: { GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: `http.https://${host}/.extraheader`, GIT_CONFIG_VALUE_0: `Authorization: Basic ${basic}` } };
+}
+
+/** Why a clone failed, in words a person can act on (a private repository with no token for its host). */
+function cloneProblem(message, repo) {
+ const m = String(message || '');
+ if (!/could not read Username|Authentication failed|terminal prompts disabled|403|401|not found/i.test(m)) return m;
+ let host = 'its host';
+ try { host = new URL(String(repo)).host; } catch { /* keep the words */ }
+ return `${m} — if the repository is private, give this machine's Codes runner a token for ${host} (GITHUB_TOKEN, GITLAB_TOKEN, or an OPENVIBE_GIT_CREDENTIALS line https://you:token@${host}) in the Node's config.json env, then restart the Node`;
+}
+
function scrub(s) {
function scrub(s) {
return String(s).replace(/(https?:\/\/)[^@/\s]+@/g, '$1***@').replace(/gh[pousr]_[A-Za-z0-9]{20,}/g, '***');
return String(s).replace(/(https?:\/\/)[^@/\s]+@/g, '$1***@').replace(/gh[pousr]_[A-Za-z0-9]{20,}/g, '***');
}
}
@@ -463,7 +505,11 @@ async function runJob({ input, harness, emit, writeResult, env = process.env, tm
@@ -463,7 +505,11 @@ async function runJob({ input, harness, emit, writeResult, env = process.env, tm
let adapterOptions = null;
let adapterOptions = null;
if (job.kind === 'code') {
if (job.kind === 'code') {
say({ t: 'step', text: `Cloning ${scrub(job.repo)}${job.ref ? ` (${job.ref})` : ''}` });
say({ t: 'step', text: `Cloning ${scrub(job.repo)}${job.ref ? ` (${job.ref})` : ''}` });
- await git(['clone', '--depth', '50', ...(job.ref ? ['--branch', job.ref] : []), '--', job.repo, work], { timeout: 300_000 });
+ const auth = gitAuthFor(job.repo, env);
+ try {
+ await git(['clone', '--depth', '50', ...(job.ref ? ['--branch', job.ref] : []), '--', job.repo, work], { timeout: 300_000, ...(auth.env ? { env: auth.env } : {}) });
+ } catch (e) { throw new Error(cloneProblem(e.message, job.repo)); }
+ if (auth.env) say({ t: 'step', text: 'Cloned with this machine\'s token for a private repository' });
base = (await git(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: work })).trim();
base = (await git(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: work })).trim();
result.base = base;
result.base = base;
say({ t: 'step', text: `Working on ${base}` });
say({ t: 'step', text: `Working on ${base}` });
@@ -594,4 +640,4 @@ function resultWriter(emit) {
@@ -594,4 +640,4 @@ function resultWriter(emit) {
};
};
}
}
-module.exports = { runJob, parseArgs, compact, sessionLog, stepInput, resultWriter, scrub, githubRepo, pushTarget, jobEnv, ENV_NAMES, JOB_PROTOCOL, codesToken, conductorMcpConfig, writeAttachments };
+module.exports = { gitAuthFor, cloneProblem, runJob, parseArgs, compact, sessionLog, stepInput, resultWriter, scrub, githubRepo, pushTarget, jobEnv, ENV_NAMES, JOB_PROTOCOL, codesToken, conductorMcpConfig, writeAttachments };
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-codes",
"name": "openvibe-codes",
- "version": "0.6.0",
+ "version": "0.6.1",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-codes",
"name": "openvibe-codes",
- "version": "0.6.0",
+ "version": "0.6.1",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"cookie-parser": "^1.4.7",
"cookie-parser": "^1.4.7",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-codes",
"name": "openvibe-codes",
- "version": "0.6.0",
+ "version": "0.6.1",
"private": true,
"private": true,
"description": "OpenVibe.Codes: the open coding-agent harness. Run a coding task on Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API or your own model, with routing by capability and cost and hand-offs between agents; plus the site with the agent catalog, a public routing API and the way to improve OpenVibe itself.",
"description": "OpenVibe.Codes: the open coding-agent harness. Run a coding task on Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API or your own model, with routing by capability and cost and hand-offs between agents; plus the site with the agent catalog, a public routing API and the way to improve OpenVibe itself.",
"main": "harness/index.js",
"main": "harness/index.js",
new file mode 100644
new file mode 100644
@@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
+'use strict';
+/**
+ * Private repositories (the owner, 2026-11-01: "some people might have private Gits"): a job clones with a token the
+ * machine holds (GITHUB_TOKEN, GITLAB_TOKEN, or an OPENVIBE_GIT_CREDENTIALS line https://user:token@host from the Node's
+ * config), as an Authorization header for that origin only, through GIT_CONFIG_* variables — never on the command
+ * line, never in the remote URL or .git/config. A failed clone of a private repository says what to do.
+ */
+const assert = require('assert');
+const { execFileSync } = require('child_process');
+const { check, done } = require('./helpers/boot');
+const { gitAuthFor, cloneProblem } = require('../harness/job');
+
+const header = (auth) => auth.env && auth.env.GIT_CONFIG_VALUE_0;
+const basicOf = (auth) => Buffer.from(String(header(auth)).replace(/^Authorization: Basic /, ''), 'base64').toString();
+
+(async () => {
+ await check('a GitHub, GitLab or any-host token becomes a header for that origin only', () => {
+ const env = { GITHUB_TOKEN: 'ghp_fake0123456789abcdefghij', GITLAB_TOKEN: 'glpat-fake', OPENVIBE_GIT_CREDENTIALS: 'https://ada:tok%[email protected]\nhttps://bob:[email protected]' };
+ const gh = gitAuthFor('https://github.com/someone/private-thing', env);
+ assert.strictEqual(gh.env.GIT_CONFIG_COUNT, '1');
+ assert.strictEqual(gh.env.GIT_CONFIG_KEY_0, 'http.https://github.com/.extraheader');
+ assert.strictEqual(basicOf(gh), 'x-access-token:ghp_fake0123456789abcdefghij');
+ assert.strictEqual(basicOf(gitAuthFor('https://gitlab.com/group/sub/project.git', env)), 'oauth2:glpat-fake');
+ assert.strictEqual(basicOf(gitAuthFor('https://git.openvibe.codes/ada/site', env)), 'ada:tok/en', 'a credential line, decoded');
+ assert.strictEqual(basicOf(gitAuthFor('https://code.example.org/team/app', env)), 'bob:pw');
+ assert.deepStrictEqual(gitAuthFor('https://bitbucket.org/x/y', env), {}, 'no token for that host, no header');
+ assert.deepStrictEqual(gitAuthFor('http://github.com/x/y', env), {}, 'never over plain http');
+ assert.deepStrictEqual(gitAuthFor('https://github.com/x/y', {}), {}, 'a machine with no token sends none');
+ assert.deepStrictEqual(gitAuthFor('https://evil.example/x', { OPENVIBE_GIT_CREDENTIALS: 'https://a:[email protected]/%0d%0aX-Injected: 1' }).env ? 'header' : 'none', 'header', 'the path of a line is not part of the header');
+ assert.deepStrictEqual(gitAuthFor('https://evil.example/x', { OPENVIBE_GIT_CREDENTIALS: 'https://a%0d%0aX:[email protected]' }), {}, 'a line that would break the header is refused');
+ assert.deepStrictEqual(gitAuthFor('not a url', env), {});
+ });
+
+ await check('the installed git reads the header from the environment, and nothing writes it to a config file', () => {
+ const auth = gitAuthFor('https://github.com/someone/private-thing', { GITHUB_TOKEN: 'ghp_fake0123456789abcdefghij' });
+ const got = execFileSync('git', ['config', '--get', 'http.https://github.com/.extraheader'], { env: { ...process.env, ...auth.env }, encoding: 'utf8' }).trim();
+ assert.strictEqual(got, header(auth));
+ assert.ok(!Object.values(auth.env).some((v) => v.includes('ghp_fake')), 'the raw token is in no variable: only its Basic form');
+ });
+
+ await check('a failed clone of a private repository says what to do, and other failures are left as they are', () => {
+ const said = cloneProblem("fatal: could not read Username for 'https://github.com': terminal prompts disabled", 'https://github.com/someone/private-thing');
+ assert.match(said, /if the repository is private, give this machine's Codes runner a token for github\.com/);
+ assert.match(said, /GITHUB_TOKEN, GITLAB_TOKEN, or an OPENVIBE_GIT_CREDENTIALS line https:\/\/you:token@github\.com/);
+ assert.strictEqual(cloneProblem('fatal: unable to access: Could not resolve host', 'https://x.y/a'), 'fatal: unable to access: Could not resolve host');
+ });
+ done();
+})().catch((e) => { console.error(e); process.exit(1); });