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)

OP OpenVibers committed 29 days ago · bc2148c0ca · 4 files, +50 −9

* Codes: a task only ever pushes a new branch of its own (never main/release/… or an existing branch) — §9 gate

Plan §9 gate: **Codes never writes a protected branch directly.**

Before, `openvibe-codes job` refused only to push to the branch the task started from. A task that named `main` while starting from another branch, or any branch that already existed, could have been pushed to (fast-forward), and no test covered it.

Now a push is only ever a new branch of the task's own:
- **Protected names** are refused, at input (`parseArgs`) and again before pushing: main, master, trunk, dev, develop, staging, production, prod, stable, live, gh-pages, release*, hotfix*.
- **GitHub is asked first** (`git ls-remote --heads`): an existing branch is refused, and if GitHub cannot be asked, nothing is pushed.
- **The push is never forced**, so a branch created in the meantime is not overwritten either.
- **New helper**: `pushTarget(requested, base, { exists })`, exported.

This is version 0.3.1. The worker installs Codes by tag, so the Node installer's `CODES_VERSION` follows in a Node PR.

Tests: `test/job.test.js` adds a check that refuses the base, the protected names, an existing branch and an unchecked one, and confirms the refspec is unforced. `parseArgs` refuses `main` and `release/2.0`. The full suite passes (28/28).

* Codes: the lockfile's own version is 0.3.1 too

Changes

ddiff --git a/harness/job.js b/harness/job.js
iindex d9f383b..5ea0014 100644
---- a/harness/job.js
++++ b/harness/job.js
@@@ -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 };
ddiff --git a/package-lock.json b/package-lock.json
iindex 00c3692..d231e69 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -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",
ddiff --git a/package.json b/package.json
iindex e4b1c94..59cf73c 100644
---- a/package.json
++++ b/package.json
@@@ -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",
ddiff --git a/test/job.test.js b/test/job.test.js
iindex 93f5cf7..987bdcd 100644
---- a/test/job.test.js
++++ b/test/job.test.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 });