mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-02 06:25:13 +02:00
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:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user