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

This commit is contained in:
haelyra committed 2026-10-10 18:53:22 -04:00
1 parent 6d813fe308
commit 74aace4071
8 files changed
+363 -3

No files matched your search

+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 };