feat: Use shared OpenCode harness (no-changelog) (#38602)

This commit is contained in:
Declan Carroll
2026-09-15 08:27:08 +00:00
committed by GitHub
parent 8a7e3951ca
commit c3f9dedef6
11 changed files with 399 additions and 57 deletions
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import {
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
renameSync,
rmSync,
symlinkSync,
} from 'node:fs';
import { homedir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const REPOSITORY = 'n8n-io/cat-bot';
const BUNDLE_ROOT = 'n8n-opencode-harness';
const PLUGIN_FILE = 'n8n-harness.js';
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
export function validateLock(lock) {
if (
lock?.repository !== REPOSITORY ||
!/^[0-9]+\.[0-9]+\.[0-9]+$/.test(lock.version) ||
lock.releaseTag !== `harness-v${lock.version}` ||
lock.assetName !== `n8n-opencode-harness-${lock.version}.tgz` ||
!/^[0-9a-f]{64}$/.test(lock.sha256)
) {
throw new Error('The agent harness lock is invalid.');
}
return lock;
}
export function readLock(lockPath = join(REPO_ROOT, 'agent-harness.lock.json')) {
try {
return validateLock(JSON.parse(readFileSync(lockPath, 'utf8')));
} catch (error) {
throw new Error(`Cannot read the agent harness lock at ${lockPath}: ${error.message}`);
}
}
export function sha256File(path) {
return createHash('sha256').update(readFileSync(path)).digest('hex');
}
export function downloadReleaseAsset(lock, destination, { run = execFileSync } = {}) {
try {
run(
'gh',
[
'release',
'download',
lock.releaseTag,
'--repo',
lock.repository,
'--pattern',
lock.assetName,
'--dir',
destination,
],
{ stdio: 'pipe' },
);
} catch (error) {
const detail = error.stderr?.toString().trim() || error.message;
throw new Error(
`Cannot download the private agent harness release. Confirm that gh can read ${lock.repository}. ${detail}`,
);
}
}
function activatePlugin(pluginPath, pluginLink) {
if (existsSync(pluginLink) && !lstatSync(pluginLink).isSymbolicLink()) {
throw new Error(`Refusing to overwrite the non-symlink path ${pluginLink}.`);
}
mkdirSync(dirname(pluginLink), { recursive: true });
const temporaryLink = `${pluginLink}.tmp-${process.pid}`;
try {
rmSync(temporaryLink, { force: true });
symlinkSync(pluginPath, temporaryLink);
renameSync(temporaryLink, pluginLink);
} finally {
rmSync(temporaryLink, { force: true });
}
}
export function installAgentHarness({
lockPath = join(REPO_ROOT, 'agent-harness.lock.json'),
cacheRoot = join(homedir(), '.cache', 'n8n-agent-harness'),
pluginLink = join(homedir(), '.config', 'opencode', 'plugins', PLUGIN_FILE),
download = downloadReleaseAsset,
extract = (archive, destination) =>
execFileSync('tar', ['-xzf', archive, '-C', destination], { stdio: 'pipe' }),
} = {}) {
const lock = readLock(lockPath);
const versionDir = join(cacheRoot, lock.version);
const bundlePath = join(versionDir, BUNDLE_ROOT);
const pluginPath = join(bundlePath, 'plugins', PLUGIN_FILE);
mkdirSync(cacheRoot, { recursive: true });
// The version cache is write-once and trusted after its checksum succeeds.
const cacheHit = existsSync(pluginPath);
if (!cacheHit) {
const stagingRoot = mkdtempSync(join(cacheRoot, '.install-'));
const stagedVersion = join(stagingRoot, lock.version);
const archivePath = join(stagedVersion, lock.assetName);
try {
mkdirSync(stagedVersion);
download(lock, stagedVersion);
if (sha256File(archivePath) !== lock.sha256) {
throw new Error('Agent harness checksum mismatch.');
}
// Cat-bot validates archive contents before publishing these pinned bytes.
extract(archivePath, stagedVersion);
if (!existsSync(join(stagedVersion, BUNDLE_ROOT, 'plugins', PLUGIN_FILE))) {
throw new Error(`The agent harness bundle has no ${PLUGIN_FILE} plugin.`);
}
rmSync(versionDir, { recursive: true, force: true });
renameSync(stagedVersion, versionDir);
} finally {
rmSync(stagingRoot, { recursive: true, force: true });
}
}
activatePlugin(pluginPath, pluginLink);
return { version: lock.version, cacheHit, bundlePath, pluginLink };
}
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
try {
const result = installAgentHarness();
console.log(
`Agent harness ${result.version} is active${result.cacheHit ? ' from cache' : ''}.`,
);
} catch (error) {
console.error(`Agent harness setup failed: ${error.message}`);
process.exitCode = 1;
}
}
+183
View File
@@ -0,0 +1,183 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import {
copyFileSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
readlinkSync,
rmSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { afterEach, test } from 'node:test';
import {
downloadReleaseAsset,
installAgentHarness,
sha256File,
validateLock,
} from './agent-harness.mjs';
const temporaryDirectories = [];
afterEach(() => {
for (const directory of temporaryDirectories.splice(0)) {
rmSync(directory, { recursive: true, force: true });
}
});
function fixtureDirectory() {
const directory = mkdtempSync(join(tmpdir(), 'agent-harness-test-'));
temporaryDirectories.push(directory);
return directory;
}
function makeAsset(root, version = '1.2.3') {
const source = join(root, 'source');
const bundle = join(source, 'n8n-opencode-harness');
mkdirSync(join(bundle, 'plugins'), { recursive: true });
writeFileSync(join(bundle, 'plugins', 'n8n-harness.js'), 'export default async () => ({});\n');
const asset = join(root, `n8n-opencode-harness-${version}.tgz`);
execFileSync('tar', ['-czf', asset, '-C', source, 'n8n-opencode-harness']);
return asset;
}
function writeLock(root, asset, overrides = {}) {
const version = '1.2.3';
const lock = {
repository: 'n8n-io/cat-bot',
releaseTag: `harness-v${version}`,
version,
assetName: `n8n-opencode-harness-${version}.tgz`,
sha256: sha256File(asset),
...overrides,
};
const lockPath = join(root, 'agent-harness.lock.json');
writeFileSync(lockPath, JSON.stringify(lock));
return lockPath;
}
function installerPaths(root) {
return {
cacheRoot: join(root, 'cache'),
pluginLink: join(root, 'config', 'opencode', 'plugins', 'n8n-harness.js'),
};
}
function copyAsset(asset) {
return (lock, destination) => copyFileSync(asset, join(destination, lock.assetName));
}
test('validates the pinned release values', () => {
const lock = {
repository: 'n8n-io/cat-bot',
releaseTag: 'harness-v1.2.3',
version: '1.2.3',
assetName: 'n8n-opencode-harness-1.2.3.tgz',
sha256: 'a'.repeat(64),
};
assert.equal(validateLock(lock), lock);
for (const invalid of [
{ repository: 'other/repo' },
{ releaseTag: 'latest' },
{ version: '../outside' },
{ assetName: 'other.tgz' },
{ sha256: 'invalid' },
]) {
assert.throws(() => validateLock({ ...lock, ...invalid }), /lock is invalid/);
}
});
test('installs the verified release and activates its plugin', () => {
const root = fixtureDirectory();
const asset = makeAsset(root);
const result = installAgentHarness({
...installerPaths(root),
lockPath: writeLock(root, asset),
download: copyAsset(asset),
});
assert.equal(result.cacheHit, false);
assert.equal(
readlinkSync(result.pluginLink),
join(result.bundlePath, 'plugins', 'n8n-harness.js'),
);
assert.match(readFileSync(result.pluginLink, 'utf8'), /export default/);
assert.deepEqual(readdirSync(join(root, 'cache')), ['1.2.3']);
});
test('reuses an installed release without downloading it again', () => {
const root = fixtureDirectory();
const asset = makeAsset(root);
const options = {
...installerPaths(root),
lockPath: writeLock(root, asset),
download: copyAsset(asset),
};
installAgentHarness(options);
const result = installAgentHarness({
...options,
download: () => assert.fail('downloaded a cached release'),
});
assert.equal(result.cacheHit, true);
});
test('keeps the active plugin when verification fails', () => {
const root = fixtureDirectory();
const asset = makeAsset(root);
const paths = installerPaths(root);
mkdirSync(dirname(paths.pluginLink), { recursive: true });
const previous = join(root, 'previous-plugin.js');
writeFileSync(previous, 'previous plugin\n');
symlinkSync(previous, paths.pluginLink);
assert.throws(
() =>
installAgentHarness({
...paths,
lockPath: writeLock(root, asset, { sha256: '0'.repeat(64) }),
download: copyAsset(asset),
}),
/checksum mismatch/,
);
assert.equal(readlinkSync(paths.pluginLink), previous);
});
test('does not overwrite a user plugin file', () => {
const root = fixtureDirectory();
const asset = makeAsset(root);
const paths = installerPaths(root);
mkdirSync(dirname(paths.pluginLink), { recursive: true });
writeFileSync(paths.pluginLink, 'user plugin\n');
assert.throws(
() =>
installAgentHarness({
...paths,
lockPath: writeLock(root, asset),
download: copyAsset(asset),
}),
/non-symlink path/,
);
});
test('reports a clear GitHub access failure', () => {
assert.throws(
() =>
downloadReleaseAsset(
{ repository: 'n8n-io/cat-bot', releaseTag: 'harness-v1.2.3', assetName: 'asset.tgz' },
fixtureDirectory(),
{
run: () => {
throw new Error('release not found');
},
},
),
/Confirm that gh can read n8n-io\/cat-bot.*release not found/,
);
});
+1 -1
View File
@@ -77,7 +77,7 @@ function ensureCodespace() {
const SESSION_SECRETS =
'. /usr/local/lib/codespaces-env.sh 2>/dev/null || true; unset AGENT_WORKER_TOKEN N8N_DEQUEUE_URL SLACK_BOT_TOKEN';
const OPENCODE_CONFIG =
'export OPENCODE_CONFIG_CONTENT="{\\"provider\\":{\\"openrouter\\":{\\"options\\":{\\"apiKey\\":\\"{env:OPENROUTER_API_KEY}\\"}}}}"';
'export OPENCODE_CONFIG_CONTENT="{\\"provider\\":{\\"openrouter\\":{\\"options\\":{\\"apiKey\\":\\"{env:OPENROUTER_API_KEY}\\"}}}}"; export N8N_AGENT_RUNTIME=sandbox; unset N8N_AGENT_PROFILE';
// Worktrees share the pnpm store but not the turbo cache; a shared TURBO_CACHE_DIR
// (seeded from the main checkout) keeps new-worktree builds at cache-hit speed.
const CACHE =