openvibers/openvibe.codes Commits
Codes: a task only ever pushes a new branch of its own (never main/release/… or an existing branch) — §9 gate (#66)
Changes
@@@ -37,6 +37,24 @@ function git(args, { cwd, env = {}, timeout = 120_000 } = {}) {});}++// Branches a task never pushes to, whatever it names (plan §9 gate: Codes never writes a protected branch directly). The++// check that matters is that the branch does not exist yet (pushBranch asks GitHub); these names are refused before++// anything is asked.++const PROTECTED = /^(main|master|trunk|dev|develop|development|staging|stage|production|prod|stable|live|gh-pages|release|releases|hotfix)([/-].*)?$/i;++++/**++ * The branch a task may push: a new one. → { branch } or { refused }. `exists` is GitHub's answer for that name (true++ * when it already exists, null when GitHub could not be asked: refused too, never assumed new).++ */++function pushTarget(requested, base, { exists = false, now = Date.now() } = {}) {++ const branch = requested || `openvibe-codes/${now.toString(36)}`;++ if (branch === base) return { refused: 'a task never pushes to the branch it started from' };++ if (PROTECTED.test(branch)) return { refused: `a task pushes only a new branch of its own, never ${branch}` };++ if (exists === null) return { refused: 'GitHub could not say whether the branch exists, so nothing was pushed' };++ if (exists) return { refused: `${branch} already exists: a task pushes only a new branch` };++ return { branch };++}++/** Any credential an URL or a message might carry, replaced. */function scrub(s) {return String(s).replace(/(https?:\/\/)[^@/\s]+@/g, '$1***@').replace(/gh[pousr]_[A-Za-z0-9]{20,}/g, '***');@@@ -62,6 +80,7 @@ function parseArgs(raw, env = process.env) {pull_request: a.push.pull_request === true,} : null;if (push && push.branch && (!BRANCH.test(push.branch) || push.branch.startsWith('-'))) throw new Error('push.branch must be a branch name');++ if (push && push.branch && PROTECTED.test(push.branch)) throw new Error(`push.branch must be a new branch of the task's own, never ${push.branch}`);return {repo: repo.toString(), ref, prompt, permission, push,agent: typeof a.agent === 'string' && /^[a-z0-9-]{1,40}$/.test(a.agent) ? a.agent : null,@@@ -111,21 +130,30 @@ function githubRepo(url) {return m ? { owner: m[1], name: m[2] } : null;}--/** Commit on a new branch and push it with GITHUB_TOKEN (never the base branch); a pull request when asked. */++/**++ * Commit on a new branch and push it with GITHUB_TOKEN; a pull request when asked. Only ever a branch that does not++ * exist yet: never the base, never a protected name, never someone's existing branch (GitHub is asked first, and a push++ * is never forced, so a branch made meanwhile is not overwritten either).++ */async function pushBranch(dir, job, { env, fetchImpl, base }) {const token = env.GITHUB_TOKEN;const gh = githubRepo(job.repo);if (!token || !gh) return { pushed: false, reason: !gh ? 'push is for github.com repositories' : 'this machine has no GITHUB_TOKEN' };-- const branch = job.push.branch || `openvibe-codes/${Date.now().toString(36)}`;-- if (branch === base) return { pushed: false, reason: 'a task never pushes to the branch it started from' };++ const first = pushTarget(job.push.branch, base);++ if (first.refused) return { pushed: false, reason: first.refused };++ const branch = first.branch;++ const remoteUrl = `https://x-access-token:${token}@github.com/${gh.owner}/${gh.name}.git`;++ let exists = null;++ try { exists = (await git(['ls-remote', '--heads', remoteUrl, `refs/heads/${branch}`], { cwd: dir, timeout: 60_000 })).trim() !== ''; } catch { exists = null; }++ const checked = pushTarget(branch, base, { exists });++ if (checked.refused) return { pushed: false, reason: checked.refused };const who = {GIT_AUTHOR_NAME: env.GIT_AUTHOR_NAME || 'OpenVibe.Codes', GIT_AUTHOR_EMAIL: env.GIT_AUTHOR_EMAIL || '[email protected]',GIT_COMMITTER_NAME: env.GIT_AUTHOR_NAME || 'OpenVibe.Codes', GIT_COMMITTER_EMAIL: env.GIT_AUTHOR_EMAIL || '[email protected]',};await git(['checkout', '-b', branch], { cwd: dir });await git(['commit', '-m', job.prompt.split('\n')[0].slice(0, 72) || 'OpenVibe.Codes task'], { cwd: dir, env: who });-- const remote = `https://x-access-token:${token}@github.com/${gh.owner}/${gh.name}.git`;-- await git(['push', remote, `HEAD:refs/heads/${branch}`], { cwd: dir, timeout: 180_000 });++ await git(['push', remoteUrl, `HEAD:refs/heads/${branch}`], { cwd: dir, timeout: 180_000 });const out = { pushed: true, branch, branch_url: `https://github.com/${gh.owner}/${gh.name}/tree/${encodeURIComponent(branch)}` };if (job.push.pull_request) {const r = await fetchImpl(`https://api.github.com/repos/${gh.owner}/${gh.name}/pulls`, {@@@ -214,4 +242,4 @@ function resultWriter(emit) {};}--module.exports = { runJob, parseArgs, compact, resultWriter, scrub, githubRepo, JOB_PROTOCOL };++module.exports = { runJob, parseArgs, compact, resultWriter, scrub, githubRepo, pushTarget, JOB_PROTOCOL };@@@ -1,12 +1,12 @@{"name": "openvibe-codes",-- "version": "0.3.0",++ "version": "0.3.1","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-codes",-- "version": "0.3.0",++ "version": "0.3.1","license": "AGPL-3.0-only","dependencies": {"cookie-parser": "^1.4.7",@@@ -1,6 +1,6 @@{"name": "openvibe-codes",-- "version": "0.3.0",++ "version": "0.3.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",@@@ -56,6 +56,8 @@ function fakeHarness({ fail = false } = {}) {[{ repo: 'https://github.com/a/b', prompt: ' ' }, /prompt/],[{ repo: 'https://github.com/a/b', prompt: 'x', ref: '--upload-pack=evil' }, /ref/],[{ repo: 'https://github.com/a/b', prompt: 'x', push: { branch: '-x' } }, /push.branch/],++ [{ repo: 'https://github.com/a/b', prompt: 'x', push: { branch: 'main' } }, /never main/],++ [{ repo: 'https://github.com/a/b', prompt: 'x', push: { branch: 'release/2.0' } }, /never release\/2\.0/],]) assert.throws(() => parseArgs(JSON.stringify(input)), why);assert.throws(() => parseArgs('not json'), /JSON/);assert.strictEqual(parseArgs(JSON.stringify({ repo: 'file:///tmp/x', prompt: 'x' }), env).repo, 'file:///tmp/x');@@@ -95,6 +97,17 @@ function fakeHarness({ fail = false } = {}) {assert.strictEqual(fs.readFileSync(path.join(src.dir, 'README.md'), 'utf8'), '# Demo\n');});++ await check('a push is only ever a new branch: never the base, a protected name, an existing branch, or unchecked', () => {++ const { pushTarget } = require('../harness/job');++ assert.deepStrictEqual(pushTarget('fix/typo', 'main'), { branch: 'fix/typo' });++ assert.match(pushTarget(null, 'main', { now: 1 }).branch, /^openvibe-codes\/1$/);++ assert.match(pushTarget('dev', 'dev').refused, /started from/);++ for (const name of ['main', 'master', 'develop', 'production', 'release/1.2', 'gh-pages', 'Main']) assert.match(pushTarget(name, 'feature').refused, /never/, name);++ assert.match(pushTarget('fix/typo', 'main', { exists: true }).refused, /already exists/);++ assert.match(pushTarget('fix/typo', 'main', { exists: null }).refused, /could not say/);++ assert.match(fs.readFileSync(path.join(__dirname, '..', 'harness', 'job.js'), 'utf8'), /\['push', remoteUrl, `HEAD:refs\/heads\/\$\{branch\}`\]/, 'never a forced refspec');++ });++await check('a failed agent or a bad clone ends with a reason, never a crash', async () => {const src = repository();const failed = await runJob({ input: JSON.stringify({ repo: src.url, prompt: 'x' }), harness: fakeHarness({ fail: true }), emit: () => {}, writeResult: () => {}, env });
@@ -37,6 +37,24 @@ function git(args, { cwd, env = {}, timeout = 120_000 } = {}) {
@@ -37,6 +37,24 @@ function git(args, { cwd, env = {}, timeout = 120_000 } = {}) {
});
});
}
}
+// Branches a task never pushes to, whatever it names (plan §9 gate: Codes never writes a protected branch directly). The
+// check that matters is that the branch does not exist yet (pushBranch asks GitHub); these names are refused before
+// anything is asked.
+const PROTECTED = /^(main|master|trunk|dev|develop|development|staging|stage|production|prod|stable|live|gh-pages|release|releases|hotfix)([/-].*)?$/i;
+
+/**
+ * The branch a task may push: a new one. → { branch } or { refused }. `exists` is GitHub's answer for that name (true
+ * when it already exists, null when GitHub could not be asked: refused too, never assumed new).
+ */
+function pushTarget(requested, base, { exists = false, now = Date.now() } = {}) {
+ const branch = requested || `openvibe-codes/${now.toString(36)}`;
+ if (branch === base) return { refused: 'a task never pushes to the branch it started from' };
+ if (PROTECTED.test(branch)) return { refused: `a task pushes only a new branch of its own, never ${branch}` };
+ if (exists === null) return { refused: 'GitHub could not say whether the branch exists, so nothing was pushed' };
+ if (exists) return { refused: `${branch} already exists: a task pushes only a new branch` };
+ return { branch };
+}
+
/** Any credential an URL or a message might carry, replaced. */
/** Any credential an URL or a message might carry, replaced. */
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, '***');
@@ -62,6 +80,7 @@ function parseArgs(raw, env = process.env) {
@@ -62,6 +80,7 @@ function parseArgs(raw, env = process.env) {
pull_request: a.push.pull_request === true,
pull_request: a.push.pull_request === true,
} : null;
} : null;
if (push && push.branch && (!BRANCH.test(push.branch) || push.branch.startsWith('-'))) throw new Error('push.branch must be a branch name');
if (push && push.branch && (!BRANCH.test(push.branch) || push.branch.startsWith('-'))) throw new Error('push.branch must be a branch name');
+ if (push && push.branch && PROTECTED.test(push.branch)) throw new Error(`push.branch must be a new branch of the task's own, never ${push.branch}`);
return {
return {
repo: repo.toString(), ref, prompt, permission, push,
repo: repo.toString(), ref, prompt, permission, push,
agent: typeof a.agent === 'string' && /^[a-z0-9-]{1,40}$/.test(a.agent) ? a.agent : null,
agent: typeof a.agent === 'string' && /^[a-z0-9-]{1,40}$/.test(a.agent) ? a.agent : null,
@@ -111,21 +130,30 @@ function githubRepo(url) {
@@ -111,21 +130,30 @@ function githubRepo(url) {
return m ? { owner: m[1], name: m[2] } : null;
return m ? { owner: m[1], name: m[2] } : null;
}
}
-/** Commit on a new branch and push it with GITHUB_TOKEN (never the base branch); a pull request when asked. */
+/**
+ * Commit on a new branch and push it with GITHUB_TOKEN; a pull request when asked. Only ever a branch that does not
+ * exist yet: never the base, never a protected name, never someone's existing branch (GitHub is asked first, and a push
+ * is never forced, so a branch made meanwhile is not overwritten either).
+ */
async function pushBranch(dir, job, { env, fetchImpl, base }) {
async function pushBranch(dir, job, { env, fetchImpl, base }) {
const token = env.GITHUB_TOKEN;
const token = env.GITHUB_TOKEN;
const gh = githubRepo(job.repo);
const gh = githubRepo(job.repo);
if (!token || !gh) return { pushed: false, reason: !gh ? 'push is for github.com repositories' : 'this machine has no GITHUB_TOKEN' };
if (!token || !gh) return { pushed: false, reason: !gh ? 'push is for github.com repositories' : 'this machine has no GITHUB_TOKEN' };
- const branch = job.push.branch || `openvibe-codes/${Date.now().toString(36)}`;
- if (branch === base) return { pushed: false, reason: 'a task never pushes to the branch it started from' };
+ const first = pushTarget(job.push.branch, base);
+ if (first.refused) return { pushed: false, reason: first.refused };
+ const branch = first.branch;
+ const remoteUrl = `https://x-access-token:${token}@github.com/${gh.owner}/${gh.name}.git`;
+ let exists = null;
+ try { exists = (await git(['ls-remote', '--heads', remoteUrl, `refs/heads/${branch}`], { cwd: dir, timeout: 60_000 })).trim() !== ''; } catch { exists = null; }
+ const checked = pushTarget(branch, base, { exists });
+ if (checked.refused) return { pushed: false, reason: checked.refused };
const who = {
const who = {
GIT_AUTHOR_NAME: env.GIT_AUTHOR_NAME || 'OpenVibe.Codes', GIT_AUTHOR_EMAIL: env.GIT_AUTHOR_EMAIL || '[email protected]',
GIT_AUTHOR_NAME: env.GIT_AUTHOR_NAME || 'OpenVibe.Codes', GIT_AUTHOR_EMAIL: env.GIT_AUTHOR_EMAIL || '[email protected]',
GIT_COMMITTER_NAME: env.GIT_AUTHOR_NAME || 'OpenVibe.Codes', GIT_COMMITTER_EMAIL: env.GIT_AUTHOR_EMAIL || '[email protected]',
GIT_COMMITTER_NAME: env.GIT_AUTHOR_NAME || 'OpenVibe.Codes', GIT_COMMITTER_EMAIL: env.GIT_AUTHOR_EMAIL || '[email protected]',
};
};
await git(['checkout', '-b', branch], { cwd: dir });
await git(['checkout', '-b', branch], { cwd: dir });
await git(['commit', '-m', job.prompt.split('\n')[0].slice(0, 72) || 'OpenVibe.Codes task'], { cwd: dir, env: who });
await git(['commit', '-m', job.prompt.split('\n')[0].slice(0, 72) || 'OpenVibe.Codes task'], { cwd: dir, env: who });
- const remote = `https://x-access-token:${token}@github.com/${gh.owner}/${gh.name}.git`;
- await git(['push', remote, `HEAD:refs/heads/${branch}`], { cwd: dir, timeout: 180_000 });
+ await git(['push', remoteUrl, `HEAD:refs/heads/${branch}`], { cwd: dir, timeout: 180_000 });
const out = { pushed: true, branch, branch_url: `https://github.com/${gh.owner}/${gh.name}/tree/${encodeURIComponent(branch)}` };
const out = { pushed: true, branch, branch_url: `https://github.com/${gh.owner}/${gh.name}/tree/${encodeURIComponent(branch)}` };
if (job.push.pull_request) {
if (job.push.pull_request) {
const r = await fetchImpl(`https://api.github.com/repos/${gh.owner}/${gh.name}/pulls`, {
const r = await fetchImpl(`https://api.github.com/repos/${gh.owner}/${gh.name}/pulls`, {
@@ -214,4 +242,4 @@ function resultWriter(emit) {
@@ -214,4 +242,4 @@ function resultWriter(emit) {
};
};
}
}
-module.exports = { runJob, parseArgs, compact, resultWriter, scrub, githubRepo, JOB_PROTOCOL };
+module.exports = { runJob, parseArgs, compact, resultWriter, scrub, githubRepo, pushTarget, JOB_PROTOCOL };
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-codes",
"name": "openvibe-codes",
- "version": "0.3.0",
+ "version": "0.3.1",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-codes",
"name": "openvibe-codes",
- "version": "0.3.0",
+ "version": "0.3.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.3.0",
+ "version": "0.3.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",
@@ -56,6 +56,8 @@ function fakeHarness({ fail = false } = {}) {
@@ -56,6 +56,8 @@ function fakeHarness({ fail = false } = {}) {
[{ repo: 'https://github.com/a/b', prompt: ' ' }, /prompt/],
[{ repo: 'https://github.com/a/b', prompt: ' ' }, /prompt/],
[{ repo: 'https://github.com/a/b', prompt: 'x', ref: '--upload-pack=evil' }, /ref/],
[{ repo: 'https://github.com/a/b', prompt: 'x', ref: '--upload-pack=evil' }, /ref/],
[{ repo: 'https://github.com/a/b', prompt: 'x', push: { branch: '-x' } }, /push.branch/],
[{ repo: 'https://github.com/a/b', prompt: 'x', push: { branch: '-x' } }, /push.branch/],
+ [{ repo: 'https://github.com/a/b', prompt: 'x', push: { branch: 'main' } }, /never main/],
+ [{ repo: 'https://github.com/a/b', prompt: 'x', push: { branch: 'release/2.0' } }, /never release\/2\.0/],
]) assert.throws(() => parseArgs(JSON.stringify(input)), why);
]) assert.throws(() => parseArgs(JSON.stringify(input)), why);
assert.throws(() => parseArgs('not json'), /JSON/);
assert.throws(() => parseArgs('not json'), /JSON/);
assert.strictEqual(parseArgs(JSON.stringify({ repo: 'file:///tmp/x', prompt: 'x' }), env).repo, 'file:///tmp/x');
assert.strictEqual(parseArgs(JSON.stringify({ repo: 'file:///tmp/x', prompt: 'x' }), env).repo, 'file:///tmp/x');
@@ -95,6 +97,17 @@ function fakeHarness({ fail = false } = {}) {
@@ -95,6 +97,17 @@ function fakeHarness({ fail = false } = {}) {
assert.strictEqual(fs.readFileSync(path.join(src.dir, 'README.md'), 'utf8'), '# Demo\n');
assert.strictEqual(fs.readFileSync(path.join(src.dir, 'README.md'), 'utf8'), '# Demo\n');
});
});
+ await check('a push is only ever a new branch: never the base, a protected name, an existing branch, or unchecked', () => {
+ const { pushTarget } = require('../harness/job');
+ assert.deepStrictEqual(pushTarget('fix/typo', 'main'), { branch: 'fix/typo' });
+ assert.match(pushTarget(null, 'main', { now: 1 }).branch, /^openvibe-codes\/1$/);
+ assert.match(pushTarget('dev', 'dev').refused, /started from/);
+ for (const name of ['main', 'master', 'develop', 'production', 'release/1.2', 'gh-pages', 'Main']) assert.match(pushTarget(name, 'feature').refused, /never/, name);
+ assert.match(pushTarget('fix/typo', 'main', { exists: true }).refused, /already exists/);
+ assert.match(pushTarget('fix/typo', 'main', { exists: null }).refused, /could not say/);
+ assert.match(fs.readFileSync(path.join(__dirname, '..', 'harness', 'job.js'), 'utf8'), /\['push', remoteUrl, `HEAD:refs\/heads\/\$\{branch\}`\]/, 'never a forced refspec');
+ });
+
await check('a failed agent or a bad clone ends with a reason, never a crash', async () => {
await check('a failed agent or a bad clone ends with a reason, never a crash', async () => {
const src = repository();
const src = repository();
const failed = await runJob({ input: JSON.stringify({ repo: src.url, prompt: 'x' }), harness: fakeHarness({ fail: true }), emit: () => {}, writeResult: () => {}, env });
const failed = await runJob({ input: JSON.stringify({ repo: src.url, prompt: 'x' }), harness: fakeHarness({ fail: true }), emit: () => {}, writeResult: () => {}, env });