fix(opencode): preserve CommonJS runtime scope and user script ownership

This commit is contained in:
haelyra
2026-10-10 18:53:22 -04:00
parent 6d813fe308
commit 74aace4071
8 changed files with 363 additions and 3 deletions
@@ -0,0 +1,3 @@
{
"type": "commonjs"
}
+16 -1
View File
@@ -1727,6 +1727,7 @@ function repairInstalledStates(options = {}) {
}
const rawPlan = createRepairPlanFromRecord(record, context);
require('./install/apply').assertOpenCodeScriptBoundary(rawPlan);
const {
migration,
plan: desiredPlan,
@@ -1811,6 +1812,9 @@ function repairInstalledStates(options = {}) {
}
for (const operation of repairOperations) {
if (operation.sourceRelativePath === 'manifests/install-assets/commonjs-scripts-package.json') {
require('./install/apply').assertOpenCodeScriptBoundary(desiredPlan, new Set(repairedPaths));
}
if (desiredPlan.target === 'opencode') {
const { assertOpenCodeActivationUnchanged } = require('./install/apply');
assertOpenCodeActivationUnchanged(desiredPlan, operation, activationSnapshot);
@@ -1996,7 +2000,18 @@ function uninstallInstalledStates(options = {}) {
);
}
for (const operation of operations) {
const { isScriptBoundary, retainScriptBoundary } = require('./install/opencode-script-boundary');
// Remove owned script bodies first. Keep their CommonJS scope while any
// retained or user-created JavaScript still relies on that boundary.
const orderedOperations = record.adapter.target === 'opencode'
? [...operations.filter(operation => !isScriptBoundary(operation, record.targetRoot)),
...operations.filter(operation => isScriptBoundary(operation, record.targetRoot))]
: operations;
for (const operation of orderedOperations) {
if (record.adapter.target === 'opencode' && retainScriptBoundary(operation, record.targetRoot)) {
retainedPaths.push(operation.destinationPath);
continue;
}
const outcome = executeUninstallOperation(operation, record.targetRoot, {
preserveDriftedCopies: true,
target: record.adapter.target,
+13 -1
View File
@@ -94,7 +94,7 @@ module.exports = createInstallTargetAdapter({
const modules = Array.isArray(input.modules)
? input.modules
: (input.module ? [input.module] : []);
return modules.flatMap(module => (Array.isArray(module.paths) ? module.paths : [])
const operations = modules.flatMap(module => (Array.isArray(module.paths) ? module.paths : [])
.filter(sourcePath => !isForeignPlatformPath(sourcePath, adapter.target))
.flatMap(sourcePath => {
const operation = adapter.createScaffoldOperation(module.id, sourcePath, input);
@@ -111,6 +111,18 @@ module.exports = createInstallTargetAdapter({
contentTransform: 'opencode-home-skills-path',
})];
}));
const scriptOperation = operations.find(operation => (
String(operation.sourceRelativePath).replace(/\\/g, '/').startsWith('scripts/')
));
if (!scriptOperation) return operations;
// OpenCode's root is ESM, while ECC runtime scripts remain CommonJS.
// Record this boundary so install, repair and uninstall share ownership.
return [...operations, createManagedOperation({
moduleId: scriptOperation.moduleId,
sourceRelativePath: 'manifests/install-assets/commonjs-scripts-package.json',
destinationPath: path.join(adapter.resolveRoot(input), 'scripts', 'package.json'),
strategy: 'preserve-relative-path',
})];
},
validate: defaultValidateOpencodeHome,
});
+23
View File
@@ -58,6 +58,7 @@ const {
} = require('./stale-operations-reconciliation');
const { buildInstallIndex, rewriteRelativeLinks } = require('./link-rewrite');
const { transformInstallContent } = require('./content-transform');
const { assertScriptBoundary } = require('./opencode-script-boundary');
function isMarkdownPath(filePath) {
return /\.(md|mdx|markdown)$/i.test(String(filePath || ''));
@@ -295,6 +296,22 @@ function readPreviousInstallState(plan) {
return readInstallState(plan.installStatePath);
}
function assertOpenCodeScriptBoundary(plan, writtenDestinations = new Set()) {
if (plan.adapter?.target !== 'opencode') return;
assertSafeInstallOperation(plan, { destinationPath: plan.installStatePath });
assertScriptBoundary(plan, {
readFile: destinationPath => readInstalledFileNoFollow(plan, { destinationPath }),
previousOperations: readPreviousInstallState(plan)?.operations || [],
writtenDestinations,
expectedContent(operation) {
if (!operation || operation.kind !== 'copy-file') return null;
const source = fs.readFileSync(operation.sourcePath);
return operation.contentTransform
? Buffer.from(transformInstallContent(operation, source.toString('utf8'))) : source;
},
});
}
function comparablePath(filePath) {
const resolved = path.resolve(filePath);
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
@@ -690,6 +707,7 @@ function preflightQwenAgentOperations(plan) {
function previewInstallPlan(plan) {
preflightQwenAgentOperations(plan);
assertOpenCodeScriptBoundary(plan);
assertOpenCodeHookDeactivationReady(plan);
const migration = prepareStaleOperationsReconciliation(
plan,
@@ -757,6 +775,7 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
if (typeof beforeInstallStateRead === 'function') {
beforeInstallStateRead({ plan });
}
assertOpenCodeScriptBoundary(plan);
assertOpenCodeLeaseCoverage(plan, dependencies.opencodeLease);
const legacyActivation = inspectLegacyOpenCodeDeactivation(plan);
const activationSnapshot = assertOpenCodeHookDeactivationReady(plan);
@@ -809,6 +828,9 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
beforeOperationWrite({ plan: appliedPlan, operation });
}
assertNoNewUserOwnedFile(migration, operation, appliedPlan);
if (operation.sourceRelativePath === 'manifests/install-assets/commonjs-scripts-package.json') {
assertOpenCodeScriptBoundary(appliedPlan, writtenDestinations);
}
assertOpenCodeActivationUnchanged(appliedPlan, operation, activationSnapshot);
if (
@@ -1059,6 +1081,7 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
module.exports = {
applyInstallPlan,
assertOpenCodeScriptBoundary,
assertOpenCodeActivationUnchanged,
assertOpenCodeLeaseCoverage,
assertOpenCodeHookDeactivationReady,
@@ -0,0 +1,72 @@
'use strict';
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const SOURCE = 'manifests/install-assets/commonjs-scripts-package.json';
const key = file => process.platform === 'win32' ? path.resolve(file).toLowerCase() : path.resolve(file);
const markerPath = root => path.join(root, 'scripts', 'package.json');
const isScriptBoundary = (operation, root) => operation.kind === 'copy-file'
&& operation.sourceRelativePath === SOURCE && key(operation.destinationPath) === key(markerPath(root));
// A nested package owns its own module scope. Symlinks and uncertain trees are
// preserved conservatively rather than followed while deciding scope changes.
function scopedJavaScriptFiles(directory, { nested = false, depth = 0 } = {}) {
if (depth > 40) throw new Error('OpenCode scripts tree exceeds the safe inspection depth');
const stat = fs.lstatSync(directory, { throwIfNoEntry: false });
if (!stat) return [];
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`Unsafe OpenCode scripts directory: ${directory}`);
const packageStat = fs.lstatSync(path.join(directory, 'package.json'), { throwIfNoEntry: false });
if (nested && packageStat?.isFile()) return [];
const files = [];
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const file = path.join(directory, entry.name);
if (entry.isSymbolicLink()) throw new Error(`Unsafe OpenCode scripts symlink: ${file}`);
if (entry.isDirectory()) files.push(...scopedJavaScriptFiles(file, { nested: true, depth: depth + 1 }));
else if (entry.isFile() && entry.name.toLowerCase().endsWith('.js')) files.push(file);
if (files.length > 12000) throw new Error('OpenCode scripts tree exceeds the safe inspection count');
}
return files;
}
function assertScriptBoundary(plan, { readFile, previousOperations = [], writtenDestinations = new Set(), expectedContent }) {
if (plan.adapter?.target !== 'opencode'
|| !plan.operations.some(operation => isScriptBoundary(operation, plan.targetRoot))) return;
const marker = markerPath(plan.targetRoot);
const content = readFile(marker);
if (content !== null) {
const metadata = JSON.parse(content.toString('utf8'));
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)
|| (metadata.type !== undefined && metadata.type !== 'commonjs')) {
throw new Error(`Refusing OpenCode CommonJS runtime migration: preserve the existing user module boundary at ${marker}.`);
}
// Existing user-owned CommonJS metadata is skipped by the ownership guard;
// retaining its bytes does not change anyone's module interpretation.
return;
}
const previous = new Map(previousOperations.filter(operation => operation.ownership === 'managed')
.map(operation => [key(operation.destinationPath), operation]));
const written = new Set([...writtenDestinations].map(key));
const planned = new Map(plan.operations.map(operation => [key(operation.destinationPath), operation]));
for (const file of scopedJavaScriptFiles(path.dirname(marker))) {
const operation = previous.get(key(file));
const bytes = readFile(file);
const expected = written.has(key(file))
? expectedContent(planned.get(key(file))) : null;
const digest = bytes === null ? null : crypto.createHash('sha256').update(bytes).digest('hex');
const expectedDigest = expected === null
? operation?.contentSha256 : crypto.createHash('sha256').update(expected).digest('hex');
if (!expectedDigest || digest !== expectedDigest) {
throw new Error(`Refusing OpenCode CommonJS runtime migration: unowned or edited JavaScript at ${file} would change module scope. Give user scripts their own package.json scope before retrying.`);
}
}
}
function retainScriptBoundary(operation, targetRoot) {
if (!isScriptBoundary(operation, targetRoot)) return false;
try { return scopedJavaScriptFiles(path.dirname(operation.destinationPath)).length > 0; }
catch { return true; }
}
module.exports = { assertScriptBoundary, isScriptBoundary, retainScriptBoundary };
+150
View File
@@ -0,0 +1,150 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const { createManifestInstallPlan, applyInstallPlan } = require('../../scripts/lib/install-executor');
const { withHookConsent } = require('../../scripts/lib/install/hook-consent');
const { repairInstalledStates, uninstallInstalledStates } = require('../../scripts/lib/install-lifecycle');
const { readInstallState } = require('../../scripts/lib/install-state');
const repo = path.resolve(__dirname, '../..');
const build = spawnSync(process.execPath, [path.join(repo, 'scripts/build-opencode.js')], { encoding: 'utf8' });
assert.strictEqual(build.status, 0, build.stderr || build.stdout);
let passed = 0;
let failed = 0;
function test(name, fn) {
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-opencode-boundary-')));
const home = path.join(dir, 'home');
const project = path.join(dir, 'project');
fs.mkdirSync(home);
fs.mkdirSync(project);
const root = path.join(home, '.config/opencode');
const scripts = path.join(root, 'scripts');
const marker = path.join(scripts, 'package.json');
const options = { repoRoot: repo, homeDir: home, projectRoot: project, env: {}, targets: ['opencode'] };
const plan = () => withHookConsent(createManifestInstallPlan({
sourceRoot: repo, homeDir: home, projectRoot: project, env: {}, target: 'opencode',
moduleIds: ['platform-configs', 'hooks-runtime'],
}), 'enabled');
const write = (file, bytes) => { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, bytes); };
const probe = file => {
const result = spawnSync(process.execPath, ['--input-type=module', '-e',
'const m=await import(requirePath); console.log(m.default ?? m.value);'.replace('requirePath', JSON.stringify(require('url').pathToFileURL(file).href))],
{ encoding: 'utf8', cwd: project });
assert.strictEqual(result.status, 0, result.stderr);
return result.stdout.trim();
};
try { fn({ home, project, root, scripts, marker, options, plan, write, probe }); passed++; console.log(`PASS ${name}`); }
catch (error) { failed++; console.error(`FAIL ${name}: ${error.stack}`); }
finally { fs.rmSync(dir, { recursive: true, force: true }); }
}
test('refuses unowned ESM scripts before copies or install-state writes', f => {
f.write(path.join(f.root, 'package.json'), '{"type":"module"}\n');
const sentinel = path.join(f.scripts, 'user.js');
f.write(sentinel, 'export const value = 42;\n');
assert.strictEqual(f.probe(sentinel), '42');
const plan = f.plan();
assert.throws(() => applyInstallPlan(plan), /unowned or edited JavaScript/);
assert.ok(!fs.existsSync(f.marker));
assert.ok(!fs.existsSync(plan.installStatePath));
assert.ok(!fs.existsSync(path.join(f.root, 'plugins')));
assert.strictEqual(f.probe(sentinel), '42');
});
test('preserves an unowned CommonJS boundary without repair or uninstall adoption', f => {
const bytes = '{"type":"commonjs","user":"keep"}\n';
f.write(f.marker, bytes);
const sentinel = path.join(f.scripts, 'user.js');
f.write(sentinel, 'module.exports = 43;\n');
const plan = f.plan();
applyInstallPlan(plan);
const owned = () => readInstallState(plan.installStatePath).operations.some(operation => operation.destinationPath === f.marker);
assert.strictEqual(owned(), false);
assert.strictEqual(repairInstalledStates(f.options).summary.errorCount, 0);
assert.strictEqual(owned(), false);
assert.strictEqual(fs.readFileSync(f.marker, 'utf8'), bytes);
assert.strictEqual(f.probe(sentinel), '43');
assert.strictEqual(uninstallInstalledStates(f.options).summary.errorCount, 0);
assert.strictEqual(fs.readFileSync(f.marker, 'utf8'), bytes);
assert.strictEqual(f.probe(sentinel), '43');
});
test('refuses an existing user ESM package boundary without changing it', f => {
const bytes = '{"type":"module","user":"keep"}\n';
f.write(f.marker, bytes);
const sentinel = path.join(f.scripts, 'user.js');
f.write(sentinel, 'export const value = 44;\n');
const plan = f.plan();
assert.throws(() => applyInstallPlan(plan), /existing user module boundary/);
assert.strictEqual(fs.readFileSync(f.marker, 'utf8'), bytes);
assert.ok(!fs.existsSync(plan.installStatePath));
assert.strictEqual(f.probe(sentinel), '44');
});
test('a nested user ESM scope remains unchanged across install and uninstall', f => {
f.write(path.join(f.scripts, 'user/package.json'), '{"type":"module"}\n');
const sentinel = path.join(f.scripts, 'user/index.js');
f.write(sentinel, 'export const value = 45;\n');
const explicitModule = path.join(f.scripts, 'user-module.mjs');
f.write(explicitModule, 'export const value = 48;\n');
applyInstallPlan(f.plan());
assert.strictEqual(f.probe(sentinel), '45');
assert.strictEqual(f.probe(explicitModule), '48');
assert.strictEqual(uninstallInstalledStates(f.options).summary.errorCount, 0);
assert.ok(!fs.existsSync(f.marker));
assert.strictEqual(f.probe(sentinel), '45');
assert.strictEqual(f.probe(explicitModule), '48');
});
test('partial uninstall retains an owned boundary while user JavaScript relies on it', f => {
const plan = f.plan();
applyInstallPlan(plan);
const sentinel = path.join(f.scripts, 'user.js');
f.write(sentinel, 'module.exports = 46;\n');
const result = uninstallInstalledStates(f.options);
assert.strictEqual(result.results[0].status, 'partial');
assert.ok(result.results[0].retainedPaths.includes(f.marker));
assert.strictEqual(f.probe(sentinel), '46');
assert.ok(readInstallState(plan.installStatePath).operations.some(operation => operation.destinationPath === f.marker));
fs.unlinkSync(sentinel);
assert.strictEqual(uninstallInstalledStates(f.options).summary.errorCount, 0);
assert.ok(!fs.existsSync(f.marker));
assert.ok(!fs.existsSync(plan.installStatePath));
});
test('rechecks newly introduced user JavaScript before boundary publication', f => {
const sentinel = path.join(f.scripts, 'user.js');
const plan = f.plan();
assert.throws(() => applyInstallPlan(plan, { beforeOperationWrite({ operation }) {
if (operation.destinationPath === f.marker) f.write(sentinel, 'export const value = 47;\n');
} }), /unowned or edited JavaScript/);
assert.ok(!fs.existsSync(f.marker));
assert.strictEqual(f.probe(sentinel), '47');
});
test('refuses a concurrent edit to a just-installed script before boundary publication', f => {
const plan = f.plan();
const sentinel = path.join(f.scripts, 'hooks/skill-run-tracker.js');
assert.throws(() => applyInstallPlan(plan, { beforeOperationWrite({ operation }) {
if (operation.destinationPath === f.marker) f.write(sentinel, 'export const value = 49;\n');
} }), /unowned or edited JavaScript/);
assert.ok(!fs.existsSync(f.marker));
assert.strictEqual(f.probe(sentinel), '49');
});
test('repair restores a missing owned boundary and preserves runtime script ownership', f => {
const plan = f.plan();
applyInstallPlan(plan);
fs.unlinkSync(f.marker);
assert.strictEqual(repairInstalledStates(f.options).summary.errorCount, 0);
assert.strictEqual(JSON.parse(fs.readFileSync(f.marker, 'utf8')).type, 'commonjs');
assert.ok(readInstallState(plan.installStatePath).operations.some(operation => operation.destinationPath === f.marker));
});
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exitCode = failed ? 1 : 0;
+2 -1
View File
@@ -111,8 +111,9 @@ function createModularInstallRoot(options = {}) {
fs.writeFileSync(
path.join(root, "package.json"),
JSON.stringify({ name: "ecc-universal", version: "0.0.0-test" })
JSON.stringify({ name: "ecc-universal", version: "0.0.0-test", type: "module" })
)
fs.writeFileSync(path.join(root, "scripts", "package.json"), JSON.stringify({ type: "commonjs" }))
// The plugin's tools import @opencode-ai/plugin, which resolves through
// node_modules — a real installation has it, an isolated temp dir would not.
@@ -0,0 +1,84 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const { pathToFileURL } = require('url');
const { createManifestInstallPlan, applyInstallPlan } = require('../../scripts/lib/install-executor');
const { withHookConsent } = require('../../scripts/lib/install/hook-consent');
const repo = path.resolve(__dirname, '../..');
const fixture = fs.mkdtempSync(path.join(repo, 'tests', '.opencode-modular-runtime-'));
const home = path.join(fixture, 'home');
const project = path.join(fixture, 'project');
fs.mkdirSync(home);
fs.mkdirSync(project);
let failed = 0;
try {
const build = spawnSync(process.execPath, [path.join(repo, 'scripts/build-opencode.js')], { encoding: 'utf8' });
assert.strictEqual(build.status, 0, build.stderr || build.stdout);
const plan = withHookConsent(createManifestInstallPlan({
sourceRoot: repo, homeDir: home, projectRoot: project, env: {},
target: 'opencode', moduleIds: ['platform-configs', 'hooks-runtime'],
}), 'enabled');
applyInstallPlan(plan);
assert.ok(!fs.existsSync(path.join(plan.targetRoot, 'skills')), 'Optional skills must stay absent');
const marker = path.join(plan.targetRoot, 'scripts/package.json');
assert.strictEqual(JSON.parse(fs.readFileSync(marker, 'utf8')).type, 'commonjs');
assert.ok(plan.statePreview.operations.some(operation => operation.destinationPath === marker));
const input = `
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
const root = process.argv[1];
const logs = [];
const context = { client: { app: { log: ({ body }) => { logs.push(body); return Promise.resolve(); } } },
$: () => { throw new Error('Offline fixture must not execute shell commands'); },
directory: process.cwd(), worktree: process.cwd() };
const loaded = [];
for (const name of fs.readdirSync(path.join(root, 'plugins')).filter(name => /\\.ts$/.test(name))) {
const mod = await import(pathToFileURL(path.join(root, 'plugins', name)).href);
for (const entry of new Set(Object.values(mod))) loaded.push(await entry(context));
}
const live = loaded.filter(hooks => typeof hooks['tool.execute.after'] === 'function');
assert.equal(live.length, 1, 'Local discovery must register exactly one live ECC plugin');
await live[0]['tool.execute.after']({ tool: 'skill', args: { name: 'code-review', arguments: 'private prompt sentinel' } }, {});
assert.ok(!logs.some(entry => /telemetry disabled/.test(entry.message)));
const runs = fs.readFileSync(path.join(process.env.ECC_AGENT_DATA_HOME, 'state/skill-runs.jsonl'), 'utf8');
assert.equal(runs.trim().split('\\n').length, 1);
assert.equal(JSON.parse(runs).skill_id, 'code-review');
assert.ok(!runs.includes('private prompt sentinel'));
const output = { env: {} };
await live[0]['shell.env']({ cwd: process.cwd() }, output);
assert.equal(output.env.CLAUDE_PLUGIN_ROOT, fs.realpathSync(root));
console.log('PASS: one live installed plugin, one private skill record, canonical root export');
`;
const result = spawnSync(process.execPath, ['--loader',
pathToFileURL(path.join(repo, 'tests/fixtures/opencode-ts-loader.mjs')).href,
'--input-type=module', '-e', input, plan.targetRoot], {
cwd: project, encoding: 'utf8', timeout: 30000,
env: { ...process.env, HOME: home, USERPROFILE: home,
CLAUDE_CONFIG_DIR: path.join(home, '.claude'), ECC_AGENT_DATA_HOME: path.join(home, '.claude'),
XDG_CONFIG_HOME: path.join(home, '.config'), OPENCODE_CONFIG_DIR: plan.targetRoot,
CLAUDE_PLUGIN_ROOT: '', CLAUDE_CODE_PACKAGE_MANAGER: 'bun', ECC_HOOK_PROFILE: 'standard', ECC_DISABLED_HOOKS: '' },
});
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
console.log(result.stdout.trim());
const standalone = spawnSync(process.execPath,
[path.join(plan.targetRoot, 'scripts/hooks/post-bash-command-log.js'), 'audit'], {
cwd: project, encoding: 'utf8', input: JSON.stringify({ tool_name: 'Bash', tool_input: { command: 'echo standalone-offline' } }),
env: { ...process.env, HOME: home, USERPROFILE: home, CLAUDE_CONFIG_DIR: path.join(home, '.claude') },
});
assert.strictEqual(standalone.status, 0, standalone.stderr);
assert.match(fs.readFileSync(path.join(home, '.claude/bash-commands.log'), 'utf8'), /standalone-offline/);
} catch (error) {
failed = 1;
console.error(error.stack);
} finally {
fs.rmSync(fixture, { recursive: true, force: true });
}
console.log(`Passed: ${1 - failed}`);
console.log(`Failed: ${failed}`);
process.exitCode = failed;