fix(install): distinguish user plugins from ECC aliases

Attribute unrecorded OpenCode aliases only to exact trusted ECC source or build bytes while keeping planned and recorded destinations strict. Pass the trusted source root through repair plans and preserve unrelated user plugin files.
This commit is contained in:
affaan-m
2026-09-27 21:23:11 -04:00
parent 9f6c16fe02
commit 6dc706fe85
4 changed files with 137 additions and 7 deletions
+2
View File
@@ -1799,6 +1799,7 @@ function createRepairPlanFromRecord(record, context, options = {}) {
const statePreview = buildRecordedStatePreview(state, context, operations);
const recordedPlan = {
sourceRoot: context.repoRoot,
mode: state.request.legacyMode ? 'legacy' : 'recorded',
target: record.adapter.target,
adapter: record.adapter,
@@ -1950,6 +1951,7 @@ function preflightOpenCodeHookDeactivation(record, context, options = {}) {
if (record.legacyLayout === 'opencode') {
const state = record.state;
const legacyPlan = withHookConsent({
sourceRoot: context.repoRoot,
target: 'opencode',
adapter: record.adapter,
targetRoot: record.targetRoot,
+40 -6
View File
@@ -321,6 +321,35 @@ function getOpenCodeActivationKind(plan, operation) {
return isOpenCodeHookActivationOperation(operation) ? 'config' : null;
}
function readOpenCodeAliasForAttribution(plan, destinationPath) {
try {
const operation = { destinationPath };
assertSafeInstallOperation(plan, operation);
if (!fs.lstatSync(destinationPath).isFile()) return null;
return readInstalledFileNoFollow(plan, operation);
} catch {
// Optional, unrecorded aliases have no ECC ownership until their bytes
// prove it. Never follow an unsafe path or relax recorded/planned guards.
return null;
}
}
function knownOpenCodePluginDigests(plan) {
const digests = new Set();
if (typeof plan.sourceRoot !== 'string' || !path.isAbsolute(plan.sourceRoot)) return digests;
const sourcePlan = { ...plan, targetRoot: plan.sourceRoot };
for (const directory of ['.opencode/plugins', '.opencode/dist/plugins']) {
for (const name of ['ecc-hooks', 'index']) {
for (const extension of ['ts', 'js', 'mjs', 'cjs']) {
const content = readOpenCodeAliasForAttribution(sourcePlan,
path.join(plan.sourceRoot, directory, `${name}.${extension}`));
if (content !== null) digests.add(crypto.createHash('sha256').update(content).digest('hex'));
}
}
}
return digests;
}
function openCodeActivationCandidates(plan, previousOperations) {
const candidates = new Map();
for (const operation of [...previousOperations, ...plan.operations]) {
@@ -328,17 +357,22 @@ function openCodeActivationCandidates(plan, previousOperations) {
candidates.set(comparablePath(operation.destinationPath), operation);
}
}
// Old installs can leave aliases that are absent from the new source tree.
// Inspect only ECC's known entrypoint names, never unrelated user plugins.
// Old installs can leave unrecorded aliases, but names such as index.js
// are also used by unrelated plugins. Attribute only exact ECC artifacts.
const knownDigests = knownOpenCodePluginDigests(plan);
for (const name of ['ecc-hooks', 'index']) {
for (const extension of ['ts', 'js', 'mjs', 'cjs']) {
const destinationPath = path.join(plan.targetRoot, 'plugins', `${name}.${extension}`);
const key = comparablePath(destinationPath);
if (!candidates.has(key)) {
candidates.set(key, {
sourceRelativePath: `.opencode/plugins/${name}.${extension}`,
destinationPath,
});
const content = readOpenCodeAliasForAttribution(plan, destinationPath);
const digest = content === null ? null : crypto.createHash('sha256').update(content).digest('hex');
if (knownDigests.has(digest)) {
candidates.set(key, {
sourceRelativePath: `.opencode/plugins/${name}.${extension}`,
destinationPath,
});
}
}
}
}