fix(opencode): guard legacy cleanup and portable consent fixtures

Preserve contributor history and current-main behavior while resolving the exact reviewed follow-up.

Source-PR: https://github.com/affaan-m/ECC/pull/3008
Source-Parent: d0dc1fcb08
Review-Manifest-SHA256: 8f15febd33dfa9f9ac6e70cb41f75ffee7aca47dd21782e841c33d4874341d6e
This commit is contained in:
affaan-m
2026-09-28 00:54:21 -04:00
parent d0dc1fcb08
commit ef8c2d8b9c
11 changed files with 529 additions and 92 deletions
+3 -2
View File
@@ -1964,7 +1964,7 @@ function preflightOpenCodeHookDeactivation(record, context, options = {}) {
// Migration does not replay operations into the old root. Inspect existing
// activations there, without treating historical merge-json records as new
// writes; the canonical destination is validated separately below.
assertOpenCodeRepairHookDeactivation({ ...legacyPlan, operations: [] }, { requireInactive: true });
assertOpenCodeRepairHookDeactivation({ ...legacyPlan, operations: [] }, { allowVerifiedLegacyRemoval: true });
assertOpenCodeRepairHookDeactivation(rawPlan, options);
return;
}
@@ -2298,9 +2298,10 @@ function repairInstalledStates(options = {}) {
homeDir: context.homeDir, projectRoot: context.projectRoot,
repoRoot: context.projectRoot, env: context.env,
});
const { getOpenCodeInstallRoots } = require('./install/apply');
const { getOpenCodeInstallRoots, assertOpenCodeLeaseCoverage } = require('./install/apply');
const roots = getOpenCodeInstallRoots({ adapter, targetRoot, homeDir: context.homeDir });
return withOpenCodeInstallLocks(roots, lease => {
assertOpenCodeLeaseCoverage({ adapter, targetRoot, homeDir: context.homeDir }, lease);
// Discovery precedes acquisition. Never repair from that stale state.
record = buildDiscoveryRecord(adapter, context, record.legacyLayout === 'opencode'
? getLegacyOpencodeLocation(context.homeDir) : null);
+49 -20
View File
@@ -14,8 +14,8 @@ const {
disableOpenCodeHookPluginRegistration,
getDisabledOpenCodePluginContent,
getRecordedHookConsent,
isOpenCodeHookActivationOperation,
isOpenCodePluginEntrypoint,
getOpenCodeActivationPathKind,
getOpenCodeSourceActivationKind,
planMaterializesHookRuntime,
shouldDisableOpenCodeHooks,
} = require('./hook-consent');
@@ -42,7 +42,8 @@ const {
prepareUserOwnedFileGuard,
preserveUnwrittenFiles,
} = require('./ownership-guard');
const { cleanupLegacyOpencodeInstall, getLegacyLocationForPlan } = require('./opencode-legacy-migration');
const { cleanupLegacyOpencodeInstall, getLegacyLocationForPlan, inspectLegacyOpencodeState,
verifyManagedLegacyFile } = require('./opencode-legacy-migration');
const { writeFileNoFollow } = require('./guarded-write');
const { withOpenCodeInstallLocks } = require('./opencode-install-lock');
const {
@@ -313,12 +314,7 @@ function getOpenCodeActivationKind(plan, operation) {
const relative = operation.destinationPath
? path.relative(plan.targetRoot, operation.destinationPath).split(path.sep).join('/').toLowerCase()
: '';
if (/^plugins\/(?:[^/]+\.(?:[cm]?js|ts)|[^/]+\/(?:index\.(?:[cm]?js|ts)|package\.json))$/.test(relative)) {
return 'plugin';
}
if (relative === 'opencode.json') return 'config';
if (isOpenCodePluginEntrypoint(operation)) return 'plugin';
return isOpenCodeHookActivationOperation(operation) ? 'config' : null;
return getOpenCodeActivationPathKind(relative) || getOpenCodeSourceActivationKind(operation);
}
function readOpenCodeAliasForAttribution(plan, destinationPath) {
@@ -473,8 +469,11 @@ function assertOpenCodeHookDeactivationReady(plan, options = {}) {
const desired = new Map(plan.operations.filter(operation => getOpenCodeActivationKind(plan, operation))
.map(operation => [comparablePath(operation.destinationPath), operation]));
const snapshot = new Map();
for (const [key, operation] of openCodeActivationCandidates(plan, [...previous.values()])) {
for (const [key, operation] of openCodeActivationCandidates(plan, [...previous.values(), ...(options.legacyOperations || [])])) {
const kind = getOpenCodeActivationKind(plan, operation);
if (kind === 'package') {
throw new Error(`Unsupported OpenCode package metadata deactivation: ${operation.destinationPath}`);
}
const expectedTransform = kind === 'plugin'
? 'opencode-disable-plugin-entrypoint' : 'opencode-disable-ecc-hooks';
const replacement = desired.get(key);
@@ -498,8 +497,14 @@ function assertOpenCodeHookDeactivationReady(plan, options = {}) {
}
continue;
}
if (kind === 'plugin' && inactive) continue;
if (inactive && (kind === 'plugin' || options.allowVerifiedLegacyRemoval)) continue;
const recorded = previous.get(key);
if (options.allowVerifiedLegacyRemoval && recorded) {
const verified = verifyManagedLegacyFile(recorded, {
targetRoot: plan.targetRoot, installStatePath: plan.installStatePath,
}, plan.sourceRoot);
if (verified.destinationPath && verified.digest === digest) continue;
}
if (!replacement || replacement.kind !== 'copy-file'
|| replacement.contentTransform !== expectedTransform
|| !recorded || recorded.contentSha256 !== digest) {
@@ -530,17 +535,29 @@ function getOpenCodeActivationWriteOptions(operation, snapshot) {
function getOpenCodeInstallRoots(plan) {
const roots = [plan.targetRoot];
const legacy = getLegacyLocationForPlan(plan);
if (legacy) {
try {
fs.lstatSync(legacy.targetRoot);
roots.push(legacy.targetRoot);
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
}
const inspection = inspectLegacyOpencodeState(legacy);
if (inspection.status === 'unreadable') throw new Error(inspection.error);
if (inspection.status === 'valid') roots.push(legacy.targetRoot);
return roots;
}
function inspectLegacyOpenCodeDeactivation(plan) {
const location = getLegacyLocationForPlan(plan);
if (!location || comparablePath(location.targetRoot) === comparablePath(plan.targetRoot)) return null;
const inspection = inspectLegacyOpencodeState(location);
if (inspection.status === 'unreadable') throw new Error(inspection.error);
if (inspection.status !== 'valid') return null;
const legacyPlan = { ...plan, ...location, operations: [] };
assertOpenCodeHookDeactivationReady(legacyPlan, { allowVerifiedLegacyRemoval: true });
return { plan: legacyPlan, operations: inspection.state.operations.filter(operation => operation.ownership === 'managed') };
}
function assertOpenCodeLeaseCoverage(plan, lease) {
if (plan.adapter?.target !== 'opencode') return;
// Reuse checks opaque ownership without acquiring extra roots out of order.
withOpenCodeInstallLocks(getOpenCodeInstallRoots(plan), () => {}, lease);
}
function findPreviousManagedHooks(previousState, plan, operation) {
if (
!previousState
@@ -704,7 +721,7 @@ function applyInstallPlan(plan, dependencies = {}) {
assertSafeInstallOperation(plan, { destinationPath: plan.installStatePath });
return withOpenCodeInstallLocks(
getOpenCodeInstallRoots(plan),
() => applyInstallPlanLocked(plan, dependencies, false),
lease => applyInstallPlanLocked(plan, { ...dependencies, opencodeLease: lease }, false),
dependencies.opencodeLease
);
}
@@ -732,6 +749,8 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
if (typeof beforeInstallStateRead === 'function') {
beforeInstallStateRead({ plan });
}
assertOpenCodeLeaseCoverage(plan, dependencies.opencodeLease);
const legacyActivation = inspectLegacyOpenCodeDeactivation(plan);
const activationSnapshot = assertOpenCodeHookDeactivationReady(plan);
const migration = prepareExcludedPathsReconciliation(
plan,
@@ -953,6 +972,9 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
];
}
assertOpenCodeLeaseCoverage(appliedPlan, dependencies.opencodeLease);
// Recheck removable bytes after canonical writes, before legacy cleanup.
inspectLegacyOpenCodeDeactivation(appliedPlan);
let opencodeMigrationWarnings = [];
try {
const opencodeMigration = cleanupLegacyOpencodeInstall(appliedPlan);
@@ -968,6 +990,12 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
];
}
if (legacyActivation) {
assertOpenCodeHookDeactivationReady(legacyActivation.plan, {
requireInactive: true, legacyOperations: legacyActivation.operations,
});
}
let excludedPathsRemoved = [];
let excludedPathsWarnings = [];
try {
@@ -1001,6 +1029,7 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
module.exports = {
applyInstallPlan,
assertOpenCodeActivationUnchanged,
assertOpenCodeLeaseCoverage,
assertOpenCodeHookDeactivationReady,
getOpenCodeActivationKind,
getOpenCodeActivationWriteOptions,
+21 -5
View File
@@ -69,10 +69,22 @@ function disableOpenCodeHookPluginRegistration(content, sourceRelativePath) {
}, null, 2)}\n`;
}
function getOpenCodeActivationPathKind(value) {
const relative = normalizeOperationPath(value);
if (relative === 'opencode.json') return 'config';
if (/^plugins\/[^/]+\/package\.json$/.test(relative)) return 'package';
if (/^plugins\/(?:[^/]+\.(?:[cm]?js|ts)|[^/]+\/index\.(?:[cm]?js|ts))$/.test(relative)) return 'plugin';
return null;
}
function getOpenCodeSourceActivationKind(operation = {}) {
const source = normalizeOperationPath(operation.sourceRelativePath);
if (!source.startsWith('.opencode/')) return null;
return getOpenCodeActivationPathKind(source.replace(/^\.opencode\/(?:dist\/)?/, ''));
}
function isOpenCodePluginEntrypoint(operation = {}) {
return /^\.opencode\/(?:dist\/)?plugins\/[^/]+\.(?:[cm]?js|ts)$/.test(
normalizeOperationPath(operation.sourceRelativePath)
);
return getOpenCodeSourceActivationKind(operation) === 'plugin';
}
function getDisabledOpenCodePluginContent() {
@@ -82,8 +94,7 @@ function getDisabledOpenCodePluginContent() {
}
function isOpenCodeHookActivationOperation(operation = {}) {
return normalizeOperationPath(operation.sourceRelativePath) === '.opencode/opencode.json'
|| isOpenCodePluginEntrypoint(operation);
return getOpenCodeSourceActivationKind(operation) !== null;
}
function isHookRuntimeOperation(operation = {}) {
@@ -146,6 +157,9 @@ function withoutHookRuntimeId(values) {
}
function withoutOpenCodeHookActivation(operation) {
if (getOpenCodeSourceActivationKind(operation) === 'package') {
throw new Error(`Unsupported OpenCode package metadata deactivation: ${operation.sourceRelativePath}`);
}
if (
!isOpenCodeHookActivationOperation(operation)
|| operation.kind !== 'copy-file'
@@ -310,6 +324,8 @@ module.exports = {
disableUnselectedOpenCodeHooks,
disableOpenCodeHookPluginRegistration,
getDisabledOpenCodePluginContent,
getOpenCodeActivationPathKind,
getOpenCodeSourceActivationKind,
formatHookCapabilityDisclosure,
getRecordedHookConsent,
isHookRuntimeOperation,
@@ -4,7 +4,7 @@ const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { readInstallState } = require('../install-state');
const { readInstallState, validateInstallState } = require('../install-state');
const { assertWithinTrustedRoot } = require('../path-safety');
const OPENCODE_TARGET = 'opencode';
@@ -65,10 +65,14 @@ function inspectLegacyOpencodeState(location) {
return { status: 'absent', state: null, error: null };
}
try {
if (!pathExists(location.installStatePath)) {
if (!pathExists(location.targetRoot)) {
return { status: 'absent', state: null, error: null };
}
const rootStat = fs.lstatSync(location.targetRoot);
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
return { status: 'invalid', state: null, error: null };
}
if (!pathExists(location.installStatePath)) return { status: 'absent', state: null, error: null };
const stateStat = fs.lstatSync(location.installStatePath);
if (
!rootStat.isDirectory()
@@ -78,7 +82,11 @@ function inspectLegacyOpencodeState(location) {
) {
return { status: 'invalid', state: null, error: null };
}
const state = readInstallState(location.installStatePath);
const { content } = hashFileNoFollow(location.installStatePath);
let state;
try { state = JSON.parse(content.toString('utf8')); }
catch { return { status: 'invalid', state: null, error: null }; }
if (!validateInstallState(state).valid) return { status: 'invalid', state: null, error: null };
const isOpencode = state.target.target === OPENCODE_TARGET
|| state.target.id === 'opencode-home';
if (
@@ -98,17 +106,17 @@ function inspectLegacyOpencodeState(location) {
}
}
function hashFileNoFollow(filePath) {
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
const descriptor = fs.openSync(filePath, flags);
function hashFileNoFollow(filePath, fileSystem = fs) {
const flags = fileSystem.constants.O_RDONLY | (fileSystem.constants.O_NOFOLLOW || 0);
const descriptor = fileSystem.openSync(filePath, flags);
try {
const before = fs.fstatSync(descriptor, { bigint: true });
const before = fileSystem.fstatSync(descriptor, { bigint: true });
if (!before.isFile()) {
throw new Error(`Refusing to read a non-file at ${filePath}`);
}
const content = fs.readFileSync(descriptor);
const after = fs.fstatSync(descriptor, { bigint: true });
const finalPathStat = fs.lstatSync(filePath, { bigint: true });
const content = fileSystem.readFileSync(descriptor);
const after = fileSystem.fstatSync(descriptor, { bigint: true });
const finalPathStat = fileSystem.lstatSync(filePath, { bigint: true });
const unchanged = before.dev === after.dev
&& before.ino === after.ino
&& before.size === after.size
@@ -123,11 +131,12 @@ function hashFileNoFollow(filePath) {
throw new Error(`Refusing to read a file that changed during validation: ${filePath}`);
}
return {
content,
digest: crypto.createHash('sha256').update(content).digest('hex'),
stat: after,
};
} finally {
fs.closeSync(descriptor);
fileSystem.closeSync(descriptor);
}
}
@@ -202,7 +211,7 @@ function verifyManagedLegacyFile(operation, location, sourceRoot) {
if (source.digest !== destination.digest) {
return { retainedPath: destinationPath };
}
return { destinationPath, stat: destination.stat };
return { destinationPath, digest: destination.digest, stat: destination.stat };
}
function pathExistsWith(fileSystem, filePath) {
@@ -257,6 +266,13 @@ function removeVerifiedLegacyFile(entry, location, fileSystem = fs) {
identityError.code = 'ESTALE';
throw identityError;
}
// Recheck bytes after quarantine: an in-place edit retains the same inode.
// Production cleanup entries carry the digest verified against ledger/source.
if (entry.digest && hashFileNoFollow(quarantinePath, fileSystem).digest !== entry.digest) {
const changed = new Error(`Legacy OpenCode file changed during quarantine: ${safePath}`);
changed.code = 'ESTALE';
throw changed;
}
fileSystem.rmSync(quarantinePath);
fileSystem.rmdirSync(quarantineDir);
return true;
@@ -396,4 +412,5 @@ module.exports = {
getLegacyLocationForPlan,
inspectLegacyOpencodeState,
removeVerifiedLegacyFile,
verifyManagedLegacyFile,
};