Merge reviewed contributor PR #3008 into backlog batch

Source-PR: https://github.com/affaan-m/ECC/pull/3008
Source-Head: e9911d6668

Local integration checkpoint; aggregate review and hosted acceptance pending.
This commit is contained in:
affaan-m
2026-09-27 23:35:42 -04:00
15 changed files with 3128 additions and 315 deletions
+374 -296
View File
@@ -10,8 +10,15 @@ const { readInstallState, validateInstallState } = require('./install-state');
const { assertWithinTrustedRoot } = require('./path-safety');
const { createInstallPlanFromRequest } = require('./install/runtime');
const { assertNoNewUserOwnedFile, prepareUserOwnedFileGuard } = require('./install/ownership-guard');
const { writeFileNoFollow: guardedWriteFile } = require('./install/guarded-write');
const { withOpenCodeInstallLocks } = require('./install/opencode-install-lock');
const { isCodexUserConfig } = require('./install/codex-user-config');
const { getRecordedHookConsent } = require('./install/hook-consent');
const {
disableOpenCodeHookPluginRegistration,
getDisabledOpenCodePluginContent,
getRecordedHookConsent,
withHookConsent,
} = require('./install/hook-consent');
const {
prepareClaudeSkillMigration,
} = require('./install/claude-skill-migration');
@@ -232,6 +239,12 @@ function transformCopyFileContent(operation, content) {
if (operation.contentTransform === 'antigravity-agent-frontmatter') {
return adaptAntigravityAgent(content, operation.sourceRelativePath);
}
if (operation.contentTransform === 'opencode-disable-ecc-hooks') {
return disableOpenCodeHookPluginRegistration(content, operation.sourceRelativePath);
}
if (operation.contentTransform === 'opencode-disable-plugin-entrypoint') {
return getDisabledOpenCodePluginContent();
}
throw new Error(`Unknown install content transform: ${operation.contentTransform}`);
}
@@ -450,66 +463,15 @@ function createChangedDestinationError(action) {
);
}
function getStableParentStat(filePath, action) {
const parentStat = fs.lstatSync(path.dirname(filePath));
if (!parentStat.isDirectory() || parentStat.isSymbolicLink()) {
throw createChangedDestinationError(action);
}
return parentStat;
}
function assertPinnedWriteDestination(
filePath,
fileDescriptor,
expectedParentStat,
trustedRoot,
action
) {
const liveDestination = getManagedDestination(filePath, trustedRoot, action);
if (path.resolve(liveDestination.managedPath) !== path.resolve(filePath)) {
throw createChangedDestinationError(action);
}
const liveParentStat = getStableParentStat(filePath, action);
if (!hasSameFileIdentity(expectedParentStat, liveParentStat)) {
throw createChangedDestinationError(action);
}
const descriptorStat = fs.fstatSync(fileDescriptor);
const livePathStat = fs.lstatSync(liveDestination.managedPath);
if (
!descriptorStat.isFile()
|| !livePathStat.isFile()
|| livePathStat.isSymbolicLink()
|| !hasSameFileIdentity(descriptorStat, livePathStat)
) {
throw createChangedDestinationError(action);
}
}
function writeFileNoFollow(filePath, content, mode, trustedRoot, action) {
const expectedParentStat = getStableParentStat(filePath, action);
const flags = fs.constants.O_WRONLY
| fs.constants.O_CREAT
| (fs.constants.O_NOFOLLOW || 0);
const fileDescriptor = fs.openSync(filePath, flags, mode);
try {
assertPinnedWriteDestination(
filePath,
fileDescriptor,
expectedParentStat,
trustedRoot,
action
);
fs.ftruncateSync(fileDescriptor, 0);
fs.writeFileSync(fileDescriptor, content);
if (mode !== undefined) {
fs.fchmodSync(fileDescriptor, mode);
}
} finally {
fs.closeSync(fileDescriptor);
}
function writeFileNoFollow(filePath, content, mode, trustedRoot, action, writeOptions = {}) {
return guardedWriteFile(filePath, content, {
...writeOptions,
mode,
action,
validateDestination(destinationPath) {
return getManagedDestination(destinationPath, trustedRoot, action).managedPath;
},
});
}
function readFileWithMetadataNoFollow(filePath, encoding) {
@@ -564,7 +526,7 @@ function assertClaudeSettingsDestination(operation, trustedRoot, target = null)
assertClaudeSettingsPath(operation.destinationPath, trustedRoot);
}
function writeContainedFile(destinationPath, content, trustedRoot, action, mode) {
function writeContainedFile(destinationPath, content, trustedRoot, action, mode, writeOptions) {
const preparedDestination = prepareContainedWriteDestination(destinationPath, trustedRoot, action);
const finalDestination = getManagedDestination(
preparedDestination,
@@ -576,7 +538,8 @@ function writeContainedFile(destinationPath, content, trustedRoot, action, mode)
content,
mode,
trustedRoot,
action
action,
writeOptions
);
return finalDestination;
}
@@ -782,7 +745,8 @@ function executeRepairOperation(
trustedRoot,
linkIndex = null,
target = null,
settingsLockHeld = false
settingsLockHeld = false,
writeOptions = {}
) {
// Install-state is attacker-controllable; never write/delete outside the
// adapter-derived trusted root, regardless of what the state file claims
@@ -800,7 +764,8 @@ function executeRepairOperation(
getExpectedCopyFileContent(operation, source.content, linkIndex),
trustedRoot,
'repair',
source.mode & 0o777
source.mode & 0o777,
writeOptions
);
} else {
copyContainedFile(sourcePath, operation.destinationPath, trustedRoot, 'repair');
@@ -1179,7 +1144,11 @@ function inspectManagedOperation(repoRoot, trustedRoot, operation, linkIndex = n
let contentMatches;
try {
contentMatches = hasRecordedContentDigest(operation)
// A deactivation transform changes the desired bytes. A historical
// active-file digest proves ownership, not completion of that transition.
const deactivatesOpenCode = operation.contentTransform === 'opencode-disable-ecc-hooks'
|| operation.contentTransform === 'opencode-disable-plugin-entrypoint';
contentMatches = hasRecordedContentDigest(operation) && !deactivatesOpenCode
? fileMatchesRecordedContent(inspectedPath, operation)
: operation.contentTransform || isMarkdownPath(operation.destinationPath)
? readFileNoFollow(inspectedPath, 'utf8') === getExpectedCopyFileContent(
@@ -1629,6 +1598,14 @@ function analyzeRecord(record, context) {
};
}
if (record.adapter.target === 'opencode') {
try {
preflightOpenCodeHookDeactivation(record, context, { requireInactive: true });
} catch (error) {
issues.push(buildIssue('error', 'opencode-hook-consent-violation', error.message));
}
}
if (!fs.existsSync(state.target.root)) {
issues.push(buildIssue('error', 'missing-target-root', `Target root does not exist: ${state.target.root}`));
}
@@ -1821,7 +1798,8 @@ function createRepairPlanFromRecord(record, context, options = {}) {
);
const statePreview = buildRecordedStatePreview(state, context, operations);
return {
const recordedPlan = {
sourceRoot: context.repoRoot,
mode: state.request.legacyMode ? 'legacy' : 'recorded',
target: record.adapter.target,
adapter: record.adapter,
@@ -1830,9 +1808,13 @@ function createRepairPlanFromRecord(record, context, options = {}) {
installStatePath: state.target.installStatePath,
warnings: [],
languages: Array.isArray(state.request.legacyLanguages) ? [...state.request.legacyLanguages] : [],
selectedModuleIds: [...(state.resolution.selectedModules || [])],
operations,
statePreview
};
return record.adapter.target === 'opencode'
? withHookConsent(recordedPlan, getRecordedHookConsent(state))
: recordedPlan;
}
const desiredPlan = resolveRecordedManifestPlan(record, context, options);
@@ -1882,8 +1864,11 @@ function writeRefreshedInstallState(record, statePreview, writtenPaths = []) {
return { ...operation };
}
// Refreshing a ledger is not a file write. Keep the last installed digest
// for untouched shared configs so a concurrent user edit is never claimed.
if (isCodexUserConfig(record, operation)
// for untouched shared configs and OpenCode activations so a concurrent
// user edit is never claimed, even by a partial repair checkpoint.
if ((isCodexUserConfig(record, operation)
|| (record.adapter.target === 'opencode'
&& require('./install/apply').getOpenCodeActivationKind(record, operation)))
&& !writtenPaths.some(writtenPath => path.relative(writtenPath, operation.destinationPath) === '')) {
const previousOperation = (record.state.operations || []).find(previous => (
previous.destinationPath
@@ -1923,10 +1908,13 @@ function prepareRepairMigration(plan, record) {
installStatePath: record.installStatePath,
statePreview: buildAdapterDerivedStatePreview(plan.statePreview, record),
};
const skillMigration = prepareClaudeSkillMigration(trustedPlan);
const migration = record.adapter.id === 'codex-home'
? prepareUserOwnedFileGuard(trustedPlan, skillMigration)
: skillMigration;
const initialMigration = prepareClaudeSkillMigration(trustedPlan);
const guardedMigration = record.adapter.id === 'codex-home'
? prepareUserOwnedFileGuard(trustedPlan, initialMigration)
: initialMigration;
const migration = trustedPlan.target === 'opencode'
? require('./install/apply').prepareHookConsentMigration(trustedPlan, guardedMigration)
: guardedMigration;
return {
migration,
plan: {
@@ -1941,6 +1929,49 @@ function prepareRepairMigration(plan, record) {
};
}
function assertOpenCodeRepairHookDeactivation(plan, options = {}) {
if (plan.target !== 'opencode') {
return new Map();
}
// The installer imports lifecycle helpers. Resolve this shared read-only
// guard lazily so both paths enforce the same discovery/ownership boundary.
const { assertOpenCodeHookDeactivationReady } = require('./install/apply');
return assertOpenCodeHookDeactivationReady(plan, options);
}
function preflightOpenCodeHookDeactivation(record, context, options = {}) {
if (record.adapter.target !== 'opencode') {
return;
}
const rawPlan = createRepairPlanFromRecord(record, context, {
// Planning must reject unsafe existing activations before a repair build
// writes compiled files. Missing payload is still validated/built later.
exemptValidationCodes: [OPENCODE_PLUGIN_NOT_BUILT_CODE],
});
if (record.legacyLayout === 'opencode') {
const state = record.state;
const legacyPlan = withHookConsent({
sourceRoot: context.repoRoot,
target: 'opencode',
adapter: record.adapter,
targetRoot: record.targetRoot,
installRoot: record.targetRoot,
installStatePath: record.installStatePath,
selectedModuleIds: [...(state.resolution.selectedModules || [])],
operations: getManagedOperations(state),
statePreview: buildAdapterDerivedStatePreview(state, record),
}, getRecordedHookConsent(state));
// 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(rawPlan, options);
return;
}
const { plan } = prepareRepairMigration(rawPlan, record);
assertOpenCodeRepairHookDeactivation(plan, options);
}
function repairInstalledStates(options = {}) {
const repoRoot = options.repoRoot || DEFAULT_REPO_ROOT;
const manifests = loadInstallManifests({ repoRoot });
@@ -1965,35 +1996,130 @@ function repairInstalledStates(options = {}) {
&& (!record.legacy || record.legacyLayout === 'opencode')
));
const results = records.map(record => {
if (record.error) {
return {
adapter: record.adapter,
status: 'error',
installStatePath: record.installStatePath,
repairedPaths: [],
plannedRepairs: [],
error: record.error
};
}
let releaseSettingsLock = null;
try {
const settingsPathToLock = !options.dryRun
&& getManagedOperations(record.state || {}).some(
operation => operation.kind === 'update-claude-settings'
)
? getClaudeSettingsPath(record.targetRoot)
: null;
if (settingsPathToLock) {
releaseSettingsLock = acquireSettingsLock(settingsPathToLock);
const results = records.map(initialRecord => {
let record = initialRecord;
const performRepair = (opencodeLease) => {
if (record.error) {
return {
adapter: record.adapter,
status: 'error',
installStatePath: record.installStatePath,
repairedPaths: [],
plannedRepairs: [],
error: record.error
};
}
const needsOpencodeBuild = record.adapter.target === 'opencode'
&& hasOpencodeBuildError(getOpencodeBuildValidationIssues(context));
const opencodeBuildRepairPath = path.join(context.repoRoot, OPENCODE_BUILD_ARTIFACT);
if (record.legacyLayout === 'opencode') {
if (needsOpencodeBuild && !options.dryRun) {
let releaseSettingsLock = null;
try {
preflightOpenCodeHookDeactivation(record, context);
const settingsPathToLock = !options.dryRun
&& getManagedOperations(record.state || {}).some(
operation => operation.kind === 'update-claude-settings'
)
? getClaudeSettingsPath(record.targetRoot)
: null;
if (settingsPathToLock) {
releaseSettingsLock = acquireSettingsLock(settingsPathToLock);
}
const needsOpencodeBuild = record.adapter.target === 'opencode'
&& hasOpencodeBuildError(getOpencodeBuildValidationIssues(context));
const opencodeBuildRepairPath = path.join(context.repoRoot, OPENCODE_BUILD_ARTIFACT);
if (record.legacyLayout === 'opencode') {
if (needsOpencodeBuild && !options.dryRun) {
try {
buildOpencodeRunner(context.repoRoot);
} catch (error) {
return {
adapter: record.adapter,
status: 'error',
installStatePath: record.installStatePath,
repairedPaths: [],
plannedRepairs: [],
error: formatBuildErrorMessage(error),
};
}
}
const canonicalPlan = createRepairPlanFromRecord(record, context, {
exemptValidationCodes: options.dryRun && needsOpencodeBuild
? [OPENCODE_PLUGIN_NOT_BUILT_CODE]
: [],
});
assertOpenCodeRepairHookDeactivation(canonicalPlan);
const plannedRepairs = [...new Set([
...(needsOpencodeBuild ? [opencodeBuildRepairPath] : []),
...canonicalPlan.operations.map(operation => operation.destinationPath),
...getManagedOperations(record.state).map(operation => operation.destinationPath),
record.installStatePath,
])];
if (options.dryRun) {
return {
adapter: record.adapter,
status: 'planned',
installStatePath: canonicalPlan.installStatePath,
repairedPaths: [],
plannedRepairs,
stateRefreshed: false,
warnings: canonicalPlan.warnings,
error: null,
};
}
// Load lazily to avoid a module cycle during install-lifecycle startup.
const { applyInstallPlan } = require('./install/apply');
const appliedPlan = applyInstallPlan(canonicalPlan, { opencodeLease });
return {
adapter: record.adapter,
status: 'repaired',
installStatePath: canonicalPlan.installStatePath,
repairedPaths: [
...(needsOpencodeBuild ? [opencodeBuildRepairPath] : []),
...canonicalPlan.operations.map(operation => operation.destinationPath),
],
plannedRepairs: [],
stateRefreshed: true,
warnings: appliedPlan.warnings,
error: null,
};
}
if (needsOpencodeBuild && options.dryRun) {
const rawPlan = createRepairPlanFromRecord(record, context, {
exemptValidationCodes: [OPENCODE_PLUGIN_NOT_BUILT_CODE],
});
const { plan: desiredPlan } = prepareRepairMigration(rawPlan, record);
const operationHealth = summarizeManagedOperationHealth(
context.repoRoot,
record.targetRoot,
desiredPlan.operations,
record.adapter.target
);
const unsafeOperationResult = getUnsafeOperationResult(
record,
operationHealth
);
if (unsafeOperationResult) {
return unsafeOperationResult;
}
const repairOperations = [...operationHealth.missing.map(entry => ({ ...entry.operation })), ...operationHealth.drifted.map(entry => ({ ...entry.operation }))];
const plannedRepairs = [opencodeBuildRepairPath, ...repairOperations.map(operation => operation.destinationPath)];
return {
adapter: record.adapter,
status: 'planned',
installStatePath: record.installStatePath,
repairedPaths: [],
plannedRepairs,
stateRefreshed: false,
warnings: desiredPlan.warnings,
error: null
};
}
if (needsOpencodeBuild) {
try {
buildOpencodeRunner(context.repoRoot);
} catch (error) {
@@ -2003,65 +2129,24 @@ function repairInstalledStates(options = {}) {
installStatePath: record.installStatePath,
repairedPaths: [],
plannedRepairs: [],
error: formatBuildErrorMessage(error),
error: formatBuildErrorMessage(error)
};
}
}
const canonicalPlan = createRepairPlanFromRecord(record, context, {
exemptValidationCodes: options.dryRun && needsOpencodeBuild
? [OPENCODE_PLUGIN_NOT_BUILT_CODE]
: [],
});
const plannedRepairs = [...new Set([
...(needsOpencodeBuild ? [opencodeBuildRepairPath] : []),
...canonicalPlan.operations.map(operation => operation.destinationPath),
...getManagedOperations(record.state).map(operation => operation.destinationPath),
record.installStatePath,
])];
if (options.dryRun) {
return {
adapter: record.adapter,
status: 'planned',
installStatePath: canonicalPlan.installStatePath,
repairedPaths: [],
plannedRepairs,
stateRefreshed: false,
warnings: canonicalPlan.warnings,
error: null,
};
}
// Load lazily to avoid a module cycle during install-lifecycle startup.
const { applyInstallPlan } = require('./install/apply');
const appliedPlan = applyInstallPlan(canonicalPlan);
return {
adapter: record.adapter,
status: 'repaired',
installStatePath: canonicalPlan.installStatePath,
repairedPaths: [
...(needsOpencodeBuild ? [opencodeBuildRepairPath] : []),
...canonicalPlan.operations.map(operation => operation.destinationPath),
],
plannedRepairs: [],
stateRefreshed: true,
warnings: appliedPlan.warnings,
error: null,
};
}
if (needsOpencodeBuild && options.dryRun) {
const rawPlan = createRepairPlanFromRecord(record, context, {
exemptValidationCodes: [OPENCODE_PLUGIN_NOT_BUILT_CODE],
});
const { plan: desiredPlan } = prepareRepairMigration(rawPlan, record);
const rawPlan = createRepairPlanFromRecord(record, context);
const {
migration,
plan: desiredPlan,
} = prepareRepairMigration(rawPlan, record);
const activationSnapshot = assertOpenCodeRepairHookDeactivation(desiredPlan);
const operationHealth = summarizeManagedOperationHealth(
context.repoRoot,
record.targetRoot,
desiredPlan.operations,
record.adapter.target
);
const unsafeOperationResult = getUnsafeOperationResult(
record,
operationHealth
@@ -2069,175 +2154,168 @@ function repairInstalledStates(options = {}) {
if (unsafeOperationResult) {
return unsafeOperationResult;
}
const repairOperations = [...operationHealth.missing.map(entry => ({ ...entry.operation })), ...operationHealth.drifted.map(entry => ({ ...entry.operation }))];
const plannedRepairs = [opencodeBuildRepairPath, ...repairOperations.map(operation => operation.destinationPath)];
return {
adapter: record.adapter,
status: 'planned',
installStatePath: record.installStatePath,
repairedPaths: [],
plannedRepairs,
stateRefreshed: false,
warnings: desiredPlan.warnings,
error: null
};
}
if (needsOpencodeBuild) {
try {
buildOpencodeRunner(context.repoRoot);
} catch (error) {
if (operationHealth.missingSource.length > 0) {
return {
adapter: record.adapter,
status: 'error',
installStatePath: record.installStatePath,
repairedPaths: [],
plannedRepairs: [],
error: formatBuildErrorMessage(error)
warnings: desiredPlan.warnings,
error: `Missing source file(s): ${operationHealth.missingSource.map(entry => entry.sourcePath).join(', ')}`
};
}
}
const rawPlan = createRepairPlanFromRecord(record, context);
const {
migration,
plan: desiredPlan,
} = prepareRepairMigration(rawPlan, record);
const operationHealth = summarizeManagedOperationHealth(
context.repoRoot,
record.targetRoot,
desiredPlan.operations,
record.adapter.target
);
const repairOperations = [
...operationHealth.missing.map(entry => ({ ...entry.operation })),
...operationHealth.drifted.map(entry => ({ ...entry.operation })),
...desiredPlan.operations
.filter(operation => (
operation.kind === 'update-claude-settings'
&& operation.previousManagedHooks
&& !isDeepStrictEqual(operation.previousManagedHooks, operation.managedHooks)
))
.map(operation => ({ ...operation })),
].filter((operation, index, items) => items.findIndex(candidate => (
candidate.kind === operation.kind
&& candidate.destinationPath === operation.destinationPath
)) === index);
const repairLinkIndex = buildLinkIndexForOperations(desiredPlan.operations, record.targetRoot);
const legacyMigrationPaths = migration.legacyOperationsToRemove.map(
operation => operation.destinationPath
);
const plannedRepairs = [...new Set([
...(needsOpencodeBuild ? [opencodeBuildRepairPath] : []),
...repairOperations.map(operation => operation.destinationPath),
...legacyMigrationPaths,
])];
const unsafeOperationResult = getUnsafeOperationResult(
record,
operationHealth
);
if (unsafeOperationResult) {
return unsafeOperationResult;
}
if (options.dryRun) {
return {
adapter: record.adapter,
status: plannedRepairs.length > 0 ? 'planned' : 'ok',
installStatePath: record.installStatePath,
repairedPaths: [],
plannedRepairs,
stateRefreshed: plannedRepairs.length === 0,
warnings: desiredPlan.warnings,
error: null
};
}
if (operationHealth.missingSource.length > 0) {
const hasLegacyMigration = migration.legacyOperationsToRemove.length > 0;
const repairedPaths = needsOpencodeBuild ? [opencodeBuildRepairPath] : [];
if (desiredPlan.target === 'opencode') {
const { assertOpenCodeActivationUnchanged } = require('./install/apply');
// Health inspection must not let a changed activation become owned by
// a bridge checkpoint before the per-write check can reject it.
for (const destinationPath of activationSnapshot.keys()) {
assertOpenCodeActivationUnchanged(desiredPlan, { destinationPath }, activationSnapshot);
}
}
if (migration.requiresBridgeState && (repairOperations.length > 0 || hasLegacyMigration)) {
writeRefreshedInstallState(record, migration.bridgeState);
}
for (const operation of repairOperations) {
if (desiredPlan.target === 'opencode') {
const { assertOpenCodeActivationUnchanged } = require('./install/apply');
assertOpenCodeActivationUnchanged(desiredPlan, operation, activationSnapshot);
}
if (record.adapter.id === 'codex-home') {
assertNoNewUserOwnedFile(migration, operation, desiredPlan);
}
const repairedPath = executeRepairOperation(
context.repoRoot,
operation,
record.targetRoot,
repairLinkIndex,
record.adapter.target,
Boolean(releaseSettingsLock),
require('./install/apply').getOpenCodeActivationWriteOptions(operation, activationSnapshot)
);
if (repairedPath) {
repairedPaths.push(repairedPath);
}
}
if (hasLegacyMigration) {
for (const operation of migration.legacyOperationsToRemove) {
const removedPath = removeContainedPath(
operation.destinationPath,
record.targetRoot,
'migrate managed Claude skill',
{ force: true }
);
if (removedPath) {
repairedPaths.push(removedPath);
}
}
}
const changedInstalledBytes = repairOperations.length > 0
|| needsOpencodeBuild
|| hasLegacyMigration;
const statePreviewToWrite = changedInstalledBytes
? desiredPlan.statePreview
: {
...desiredPlan.statePreview,
installedAt: record.state.installedAt,
source: { ...record.state.source },
};
assertOpenCodeRepairHookDeactivation(desiredPlan, { requireInactive: true });
writeRefreshedInstallState(record, statePreviewToWrite, repairedPaths);
return {
adapter: record.adapter,
status: (repairOperations.length > 0 || needsOpencodeBuild || hasLegacyMigration)
? 'repaired'
: 'ok',
installStatePath: record.installStatePath,
repairedPaths,
plannedRepairs: [],
stateRefreshed: true,
warnings: desiredPlan.warnings,
error: null
};
} catch (error) {
return {
adapter: record.adapter,
status: 'error',
installStatePath: record.installStatePath,
repairedPaths: [],
plannedRepairs: [],
warnings: desiredPlan.warnings,
error: `Missing source file(s): ${operationHealth.missingSource.map(entry => entry.sourcePath).join(', ')}`
error: error.message
};
} finally {
if (releaseSettingsLock) releaseSettingsLock();
}
const repairOperations = [
...operationHealth.missing.map(entry => ({ ...entry.operation })),
...operationHealth.drifted.map(entry => ({ ...entry.operation })),
...desiredPlan.operations
.filter(operation => (
operation.kind === 'update-claude-settings'
&& operation.previousManagedHooks
&& !isDeepStrictEqual(operation.previousManagedHooks, operation.managedHooks)
))
.map(operation => ({ ...operation })),
].filter((operation, index, items) => items.findIndex(candidate => (
candidate.kind === operation.kind
&& candidate.destinationPath === operation.destinationPath
)) === index);
const repairLinkIndex = buildLinkIndexForOperations(desiredPlan.operations, record.targetRoot);
const legacyMigrationPaths = migration.legacyOperationsToRemove.map(
operation => operation.destinationPath
);
const plannedRepairs = [...new Set([
...(needsOpencodeBuild ? [opencodeBuildRepairPath] : []),
...repairOperations.map(operation => operation.destinationPath),
...legacyMigrationPaths,
])];
if (options.dryRun) {
return {
adapter: record.adapter,
status: plannedRepairs.length > 0 ? 'planned' : 'ok',
installStatePath: record.installStatePath,
repairedPaths: [],
plannedRepairs,
stateRefreshed: plannedRepairs.length === 0,
warnings: desiredPlan.warnings,
error: null
};
}
const hasLegacyMigration = migration.legacyOperationsToRemove.length > 0;
const repairedPaths = needsOpencodeBuild ? [opencodeBuildRepairPath] : [];
if (migration.requiresBridgeState && (repairOperations.length > 0 || hasLegacyMigration)) {
writeRefreshedInstallState(record, migration.bridgeState);
}
for (const operation of repairOperations) {
if (record.adapter.id === 'codex-home') {
assertNoNewUserOwnedFile(migration, operation, desiredPlan);
};
if (record.adapter.target !== 'opencode' || options.dryRun) return performRepair();
let repairResult;
try {
const adapter = getInstallTargetAdapter('opencode');
const targetRoot = adapter.resolveRoot({
homeDir: context.homeDir, projectRoot: context.projectRoot,
repoRoot: context.projectRoot, env: context.env,
});
const { getOpenCodeInstallRoots } = require('./install/apply');
const roots = getOpenCodeInstallRoots({ adapter, targetRoot, homeDir: context.homeDir });
return withOpenCodeInstallLocks(roots, lease => {
// Discovery precedes acquisition. Never repair from that stale state.
record = buildDiscoveryRecord(adapter, context, record.legacyLayout === 'opencode'
? getLegacyOpencodeLocation(context.homeDir) : null);
if (!record.exists || record.error) {
throw new Error(record.error || 'OpenCode install-state disappeared before repair.');
}
const repairedPath = executeRepairOperation(
context.repoRoot,
operation,
record.targetRoot,
repairLinkIndex,
record.adapter.target,
Boolean(releaseSettingsLock)
);
if (repairedPath) {
repairedPaths.push(repairedPath);
}
}
if (hasLegacyMigration) {
for (const operation of migration.legacyOperationsToRemove) {
const removedPath = removeContainedPath(
operation.destinationPath,
record.targetRoot,
'migrate managed Claude skill',
{ force: true }
);
if (removedPath) {
repairedPaths.push(removedPath);
}
}
}
const changedInstalledBytes = repairOperations.length > 0
|| needsOpencodeBuild
|| hasLegacyMigration;
const statePreviewToWrite = changedInstalledBytes
? desiredPlan.statePreview
: {
...desiredPlan.statePreview,
installedAt: record.state.installedAt,
source: { ...record.state.source },
};
writeRefreshedInstallState(record, statePreviewToWrite, repairedPaths);
return {
adapter: record.adapter,
status: (repairOperations.length > 0 || needsOpencodeBuild || hasLegacyMigration)
? 'repaired'
: 'ok',
installStatePath: record.installStatePath,
repairedPaths,
plannedRepairs: [],
stateRefreshed: true,
warnings: desiredPlan.warnings,
error: null
};
repairResult = performRepair(lease);
return repairResult;
});
} catch (error) {
return {
adapter: record.adapter,
status: 'error',
installStatePath: record.installStatePath,
repairedPaths: [],
plannedRepairs: [],
error: error.message
};
} finally {
if (releaseSettingsLock) releaseSettingsLock();
if (repairResult?.status === 'error') {
return { ...repairResult, releaseError: error.message };
}
return { adapter: record.adapter, status: 'error', installStatePath: record.installStatePath,
repairedPaths: repairResult?.repairedPaths || [], plannedRepairs: [], error: error.message };
}
});
+303 -3
View File
@@ -9,7 +9,16 @@ const {
withCommitAttributionDisabled,
} = require('../claude-commit-attribution');
const { readInstallState, writeInstallState } = require('../install-state');
const { assertHookConsentReady, planMaterializesHookRuntime } = require('./hook-consent');
const {
assertHookConsentReady,
disableOpenCodeHookPluginRegistration,
getDisabledOpenCodePluginContent,
getRecordedHookConsent,
isOpenCodeHookActivationOperation,
isOpenCodePluginEntrypoint,
planMaterializesHookRuntime,
shouldDisableOpenCodeHooks,
} = require('./hook-consent');
const {
getClaudeSettingsPath,
mergeManagedHooks,
@@ -33,7 +42,9 @@ const {
prepareUserOwnedFileGuard,
preserveUnwrittenFiles,
} = require('./ownership-guard');
const { cleanupLegacyOpencodeInstall } = require('./opencode-legacy-migration');
const { cleanupLegacyOpencodeInstall, getLegacyLocationForPlan } = require('./opencode-legacy-migration');
const { writeFileNoFollow } = require('./guarded-write');
const { withOpenCodeInstallLocks } = require('./opencode-install-lock');
const {
completeExcludedPathsReconciliation,
prepareExcludedPathsReconciliation,
@@ -52,6 +63,12 @@ function transformInstallContent(operation, content) {
if (operation.contentTransform === 'antigravity-agent-frontmatter') {
return adaptAntigravityAgent(content, operation.sourceRelativePath);
}
if (operation.contentTransform === 'opencode-disable-ecc-hooks') {
return disableOpenCodeHookPluginRegistration(content, operation.sourceRelativePath);
}
if (operation.contentTransform === 'opencode-disable-plugin-entrypoint') {
return getDisabledOpenCodePluginContent();
}
throw new Error(`Unknown install content transform: ${operation.contentTransform}`);
}
@@ -292,6 +309,237 @@ function comparablePath(filePath) {
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
}
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;
}
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;
}
}
// Finite, exact public ECC entrypoint history reachable from d3b8a3e908904e242ed2dbe66af62cca71131419.
// Refusal evidence only: matching bytes never grant ownership or permission to
// adopt, rewrite or delete an unrecorded file. Unknown modified/compiled variants
// are not covered. No history lookup or plugin execution occurs at runtime.
const LEGACY_OPENCODE_PLUGIN_DIGESTS = Object.freeze([
// a0600a00fbe3a193a44584ad55800ce82cec62af:.opencode/plugins/index.ts (blob 3a98f0ba6510d436cc9cf3e2161f8f69771d968a)
'7dd2d255da5d4344eb38ca93cf1765e425b0c01943f6e45428614662ebee0d4b',
// a0600a00fbe3a193a44584ad55800ce82cec62af:.opencode/plugins/ecc-hooks.ts (blob bf06c03f8ff6bdd835c5266921758a6db85152bf)
'5db9b59434af0d5971538f0176779733b8146d7a71fbd9055ae0183c26754068',
// 91ba9b4cf6c47c8130829004f8bb64762a76ccbb:.opencode/plugins/ecc-hooks.ts (blob 4aabde61203d4473e04d5a10803b0560b8c596e4)
'c683b9321d8b5fbc6889b1740f4583c4f94c84554ee97e2072f61de45c661bda',
// 1a8beb71c5282ddfe77c72ab0290961a820e3d89:.opencode/plugins/ecc-hooks.ts (blob 69b59727e552991a79c196aab8ec128173329d9a)
'1e890ce162325c9d7c579b0716383b6c297179edb78084c4b59cc8bf766ceb71',
// e65f12bf7ea474a6f5ac96991a251673f474b445:.opencode/plugins/index.ts (blob c1e17a1595403080490fcec6820c1485bb6afba9)
'965c5fac76ce0c3ceb3836814f5eb9ede8c9db50373a508c734f949cb321a21a',
// e65f12bf7ea474a6f5ac96991a251673f474b445:.opencode/plugins/ecc-hooks.ts (blob 54881ad868c276ff0d50cc83d8ae938464ad02da)
'5c043b84693a654fffe4b407e87411b28c9af8b92f5ef49a03caab7b27f03102',
// 2cdc218c45a81ce46035832b13bf68d91137301e:.opencode/plugins/ecc-hooks.ts (blob d496e61a538131ff6f33b2e6d941544e3d94c5d8)
'73692e599d271bbb9b7aac59f97e193518af2b5db3a3505af0376c8d8657220a',
// 5929d246946eeb5d147612ba06d60c575c5a4e21:.opencode/plugins/ecc-hooks.ts (blob 472f80f5ae9500fa9a0a7885b6ce4dc4b409d3d6)
'd7a410380ed2e0bcb613110b2810221d03a8944e50766bc7a4db2eb1b44446b6',
// ca185ef5f7667078a1e70a763bd3a9c71c48acf0:.opencode/plugins/ecc-hooks.ts (blob 22b1132f0964bd4ba5c1a4ad1bafa605de99eb6a)
'0345093b34e537d350c5b5aa0296511f558aa767e5104fb5ef05069013f3b5b6',
// 28e53a0bc10e286f68b53bb1e3b3f049021e57b9:.opencode/plugins/ecc-hooks.ts (blob 47265c0ebd031168d8e3a18f30036864338cd22c)
'e20ecd53714b1fd55baeff796c6f4538ebd4bae71ca1e791b2b8e29575061846',
// 591ab5cbd3f2f65860ea91c226e410b1502c8e2e:.opencode/plugins/ecc-hooks.ts (blob 49124c255003eb5517178a8ecad7dd453303df33)
'b9c22c76ae2464c9410963579ee5ff49003e4d79e32b69c228539ae7b15f5104',
// 6f452d48d258b39f4f6e1171b7ca18c6f7f61ad5:.opencode/plugins/ecc-hooks.ts (blob 6336081e97c4345f02adfdc0b9ad9e27dccdec04)
'c7122565cf97b896cc3da9009bf06daca7513b3e9ba7448c26b8872591dbf3f7',
// 3a08b0c7a85bda69ee9922a103e077a04d538150:.opencode/plugins/ecc-hooks.ts (blob bad6a4cecf2270a7d8a919daeb6541c94a810c48)
'e438603c13206365068b400063df0af98bd587842b80f09b97fe57f7ddef56e0',
// 29edd57708bee26f16363c16a28fec7f6b09f53f:.opencode/plugins/ecc-hooks.ts (blob 05792ce9ae86a785b746bdb843572e4b5bc93130)
'6a9063b2f67334a78d269d53f95679c33d6d126260f8e5fc7cc941fea9b903e8',
// 8141f6904f14fa8a83131e1cb5b6507d687e25bb:.opencode/plugins/ecc-hooks.ts (blob 606bcb7c59aa5e459d2093ffb9cf9208d1184c30)
'a198b640fd1faf1c75e96909eabf4ae24899de127b2447490eed813766013836',
// 6d613f67dd24189a8bb7fb1a2f5e535957f46a58:.opencode/plugins/index.ts (blob ca58596901d816147ac4eff525f1a885d36bd094)
'e89aaa309b7a0578bb69af4a2425744fcf14c2556cd34a1036bbcba448a0b517',
// 6d613f67dd24189a8bb7fb1a2f5e535957f46a58:.opencode/plugins/ecc-hooks.ts (blob 31cfa8ac31ac3cbc5c51b4b275017ef18b8f9033)
'd66a43e43ef9669589de593e8f94e750ee11d3c75cb5fe79e81636ef738c3e45',
// affbd334858368518c5baf5f84f74034dea1ea6f:.opencode/plugins/ecc-hooks.ts (blob ff8628b5fd47181cbee54367dc4e32e5392cde1a)
'4874a12639fd58da59a54fe5b2461c0ddd2eeca6770111615caa402ff0e95693',
// 0a87323eda77ee412fa3a3bf028a577536966505:.opencode/plugins/ecc-hooks.ts (blob fa96b805685e3c3e6f86535debca5ab8a5bea1ef)
'4e2330f340e074208cd323c1a833667032ce8db4febc4eaeda354bc49cb58bc4',
// a0a1eda8fc4828e58dc8aabcec4e25f9ef038a0a:.opencode/plugins/ecc-hooks.ts (blob 51bde010b4d426676b52ffc9567b1da80f4ca510)
'ca9abadee5d072121677168752fb4f3b16ce9fc7eb55a3c9c7f517377e0bf69b',
// 05acc275307a09eea89080619a35d7dbd20b128b:.opencode/plugins/ecc-hooks.ts (blob 9e4ab3fcd50f6610cfdfa5a7d0d71c2374f65745)
'96998990d6aac0b9535ab6ab60a0be1c284ffcca7e4ba04f1cfa0e67409a8146',
// a2b3cc1600e9cab58147ef01c03f9889b5a8cc86:.opencode/plugins/ecc-hooks.ts (blob 58a209283f70efe1dbfef5d78b4d72764c67f027)
'0697bfed6e6ad887443a32810e83adb5316d2c9c0490b98f9fcb6ea52bea84e3',
// 0c7deb26a344db095c04a213eba5634d4ccce030:.opencode/plugins/ecc-hooks.ts (blob 9193bb412920a1f1d5af98fda0a66d1e3295f46f)
'd666a94e9d0ccbcfdeffd59b624938cd44706571eec3771974c57fdbe28577c1',
// 48b883d7412914b04c8b185d9a82685b105d1734:.opencode/plugins/ecc-hooks.ts (blob 3053314750a61dbcdb06a9cca39492304457f582)
'16fe21ca801a613a0ae2fc1f8dd5c8474138dc31c75ea35cd8695884397f1f15',
// d70bab85e33af7a03b78c70dba7a7ce3b01d1b17:.opencode/plugins/ecc-hooks.ts (blob 1f158d7999f5f100e386587a94a90a08d512e278)
'0354270a5fc26809d0795ecc7eef1dee91de4905d58f26d5828d43af24767b96',
// 0e9f613fd196f6d4157765b17d39c2c42ebbf564:.opencode/plugins/ecc-hooks.ts (blob 50d23bfde3607832446fda26b25a3ed3e527e5d1)
'513190b6c935dac472efd11818b20d7f2479ec1f7be06ef7f4d241c2493ad9e3',
// 6d440c036df2c1b2fec957627d1202c3708e0627:.opencode/plugins/index.ts (blob d19a91f1a686d6ed060d08eddeb5aa05a4be6b75)
'e42c733adb177f84cea813663aa34c7868dbaa98c96950d0ef91cd211b8aa169',
// 6d440c036df2c1b2fec957627d1202c3708e0627:.opencode/plugins/ecc-hooks.ts (blob b64ffae7ce10cab9e5ed9b04cec23d62db9036e7)
'503ea491cbeadff5bf59b936a75bff65caaf1d71e47a953a9b9b790be780efef',
]);
function knownOpenCodePluginDigests(plan) {
if (typeof plan.sourceRoot !== 'string' || !path.isAbsolute(plan.sourceRoot)) return new Set();
const digests = new Set(LEGACY_OPENCODE_PLUGIN_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]) {
if (getOpenCodeActivationKind(plan, operation) && operation.destinationPath) {
candidates.set(comparablePath(operation.destinationPath), operation);
}
}
// 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)) {
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,
});
}
}
}
}
return candidates;
}
function activationIsInactive(kind, operation, content) {
if (kind === 'plugin') {
return content.toString('utf8') === getDisabledOpenCodePluginContent();
}
const text = content.toString('utf8');
// Validate first so malformed JSON retains its source context.
disableOpenCodeHookPluginRegistration(text, operation.sourceRelativePath || operation.destinationPath);
const config = JSON.parse(text);
return !Array.isArray(config.plugin) || !config.plugin.includes('./plugins');
}
function assertOpenCodeHookDeactivationReady(plan, options = {}) {
if (!shouldDisableOpenCodeHooks(plan)) {
return new Map();
}
assertSafeInstallOperation(plan, { destinationPath: plan.installStatePath });
const previousState = readPreviousInstallState(plan);
if (previousState && (
previousState.target.id !== plan.adapter.id
|| comparablePath(previousState.target.root) !== comparablePath(plan.targetRoot)
|| comparablePath(previousState.target.installStatePath) !== comparablePath(plan.installStatePath)
)) {
throw new Error('Refusing OpenCode hook deactivation: install-state target mismatch.');
}
const previous = new Map(((previousState && previousState.operations) || [])
.filter(operation => operation.ownership === 'managed' && operation.destinationPath)
.map(operation => [comparablePath(operation.destinationPath), operation]));
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()])) {
const kind = getOpenCodeActivationKind(plan, operation);
const expectedTransform = kind === 'plugin'
? 'opencode-disable-plugin-entrypoint' : 'opencode-disable-ecc-hooks';
const replacement = desired.get(key);
// Validate planned activation even when its destination does not yet exist.
// Recorded operations may name an unrelated source or use render-template.
if (replacement && (replacement.kind !== 'copy-file'
|| replacement.contentTransform !== expectedTransform)) {
throw new Error(`Refusing OpenCode hook deactivation: unsupported activation operation at ${operation.destinationPath}`);
}
const content = readInstalledFileNoFollow(plan, operation);
if (content === null && fs.existsSync(operation.destinationPath)) {
throw new Error(`Refusing OpenCode hook deactivation: non-file activation at ${operation.destinationPath}`);
}
const digest = content === null ? null : crypto.createHash('sha256').update(content).digest('hex');
snapshot.set(key, digest);
if (content === null) continue;
const inactive = activationIsInactive(kind, operation, content);
if (options.requireInactive) {
if (!inactive) {
throw new Error(`OpenCode hook activation remains active at ${operation.destinationPath}`);
}
continue;
}
if (kind === 'plugin' && inactive) continue;
const recorded = previous.get(key);
if (!replacement || replacement.kind !== 'copy-file'
|| replacement.contentTransform !== expectedTransform
|| !recorded || recorded.contentSha256 !== digest) {
throw new Error(`Refusing OpenCode hook deactivation: user-owned, modified, unverifiable or stale activation at ${operation.destinationPath}`);
}
}
return snapshot;
}
function assertOpenCodeActivationUnchanged(plan, operation, snapshot) {
const key = comparablePath(operation.destinationPath);
if (!snapshot.has(key)) return;
const content = readInstalledFileNoFollow(plan, operation);
const digest = content === null ? null : crypto.createHash('sha256').update(content).digest('hex');
if (digest !== snapshot.get(key)) {
throw new Error(`Refusing OpenCode hook deactivation: activation changed after preflight at ${operation.destinationPath}`);
}
}
function getOpenCodeActivationWriteOptions(operation, snapshot) {
const key = comparablePath(operation.destinationPath);
if (!snapshot.has(key)) return {};
const digest = snapshot.get(key);
return { expectedContent: Object.freeze(digest === null
? { kind: 'absent' } : { kind: 'sha256', digest }) };
}
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;
}
}
return roots;
}
function findPreviousManagedHooks(previousState, plan, operation) {
if (
!previousState
@@ -347,6 +595,27 @@ function preflightClaudeSettingsOperations(plan) {
}
function prepareHookConsentMigration(plan, migration) {
if (shouldDisableOpenCodeHooks(plan) && migration.requiresBridgeState) {
const previousState = readPreviousInstallState(plan);
if (previousState) {
const previousConsent = getRecordedHookConsent(previousState);
return {
...migration,
// A checkpoint is not a completed consent transition. On failure,
// retain the previous decision until every activation is inactive.
bridgeState: {
...migration.bridgeState,
request: { ...migration.bridgeState.request, hookConsent: previousConsent },
resolution: {
...migration.bridgeState.resolution,
selectedModules: previousConsent === 'enabled'
? [...new Set([...migration.bridgeState.resolution.selectedModules, 'hooks-runtime'])]
: migration.bridgeState.resolution.selectedModules,
},
},
};
}
}
if (plan.hookConsent !== 'declined') {
return migration;
}
@@ -400,6 +669,7 @@ function prepareHookConsentMigration(plan, migration) {
}
function previewInstallPlan(plan) {
assertOpenCodeHookDeactivationReady(plan);
const migration = prepareHookConsentMigration(
plan,
prepareUserOwnedFileGuard(plan, prepareClaudeSkillMigration(plan))
@@ -429,6 +699,14 @@ function previewInstallPlan(plan) {
function applyInstallPlan(plan, dependencies = {}) {
assertHookConsentReady(plan);
if (plan.adapter?.target === 'opencode') {
assertSafeInstallOperation(plan, { destinationPath: plan.installStatePath });
return withOpenCodeInstallLocks(
getOpenCodeInstallRoots(plan),
() => applyInstallPlanLocked(plan, dependencies, false),
dependencies.opencodeLease
);
}
const isClaudeManualTarget = plan.adapter
&& (plan.adapter.target === 'claude' || plan.adapter.target === 'claude-project');
const settingsPathToLock = isClaudeManualTarget
@@ -453,6 +731,7 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
if (typeof beforeInstallStateRead === 'function') {
beforeInstallStateRead({ plan });
}
const activationSnapshot = assertOpenCodeHookDeactivationReady(plan);
const migration = prepareExcludedPathsReconciliation(
plan,
prepareHookConsentMigration(
@@ -499,6 +778,7 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
beforeOperationWrite({ plan: appliedPlan, operation });
}
assertNoNewUserOwnedFile(migration, operation, appliedPlan);
assertOpenCodeActivationUnchanged(appliedPlan, operation, activationSnapshot);
if (
operation.kind === 'update-claude-settings'
@@ -585,7 +865,20 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
index: linkIndex,
})
: transformed;
fs.writeFileSync(operation.destinationPath, installedContent, 'utf8');
const writeOptions = getOpenCodeActivationWriteOptions(operation, activationSnapshot);
if (writeOptions.expectedContent) {
writeFileNoFollow(operation.destinationPath, installedContent, {
...writeOptions,
action: 'install OpenCode activation',
validateDestination(destinationPath) {
assertSafeInstallOperation(appliedPlan, { destinationPath });
assertSafeClaudeSkillOperation(appliedPlan, { destinationPath });
return destinationPath;
},
});
} else {
fs.writeFileSync(operation.destinationPath, installedContent, 'utf8');
}
writtenDestinations.add(operation.destinationPath);
continue;
}
@@ -605,6 +898,7 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
);
}
assertOpenCodeHookDeactivationReady(appliedPlan, { requireInactive: true });
finalState = stateWithContentDigests(migration.finalState, appliedPlan);
if (typeof beforeInstallStateWrite === 'function') {
beforeInstallStateWrite({ plan: appliedPlan, state: finalState });
@@ -705,6 +999,12 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
module.exports = {
applyInstallPlan,
assertOpenCodeActivationUnchanged,
assertOpenCodeHookDeactivationReady,
getOpenCodeActivationKind,
getOpenCodeActivationWriteOptions,
getOpenCodeInstallRoots,
assertSafeInstallOperation,
prepareHookConsentMigration,
previewInstallPlan,
};
+22 -10
View File
@@ -5,6 +5,7 @@ const fs = require('fs');
const path = require('path');
const INVALID_LOCK_STALE_MS = 5 * 60 * 1000;
const acquiredLockIdentities = new WeakMap();
function sameFileIdentity(left, right) {
if (left.ino !== right.ino) {
@@ -19,7 +20,7 @@ function sameFileIdentity(left, right) {
return left.dev === right.dev;
}
function createSettingsLock(lockPath) {
function createSettingsLock(lockPath, label = 'Claude settings') {
const tempPath = `${lockPath}.create-${process.pid}-${crypto.randomBytes(8).toString('hex')}`;
let descriptor;
let ownedStats;
@@ -43,18 +44,28 @@ function createSettingsLock(lockPath) {
fs.rmSync(tempPath, { force: true });
let released = false;
return () => {
const release = () => {
if (released) return;
const quarantinePath = `${lockPath}.release-${process.pid}-${crypto.randomBytes(8).toString('hex')}`;
fs.renameSync(lockPath, quarantinePath);
const quarantinedStats = fs.lstatSync(quarantinePath, { bigint: true });
if (!sameFileIdentity(quarantinedStats, ownedStats)) {
if (!fs.existsSync(lockPath)) fs.renameSync(quarantinePath, lockPath);
throw new Error(`Refusing to release a changed Claude settings lock: ${lockPath}`);
throw new Error(`Refusing to release a changed ${label} lock: ${lockPath}`);
}
released = true;
fs.rmSync(quarantinePath, { force: true });
};
// Retain the identity observed through the creation descriptor. A pathname
// sampled after publication may already refer to a replacement lock.
acquiredLockIdentities.set(release, Object.freeze({ dev: ownedStats.dev, ino: ownedStats.ino }));
return release;
}
function getSettingsLockIdentity(release) {
const identity = acquiredLockIdentities.get(release);
if (!identity) throw new Error('No acquired settings lock identity for this release function.');
return identity;
}
function inspectSettingsLock(lockPath) {
@@ -91,7 +102,7 @@ function processIsAlive(pid) {
}
}
function recoverSettingsLock(lockPath) {
function recoverSettingsLock(lockPath, label = 'Claude settings') {
const recoveryPath = `${lockPath}.recover`;
try {
fs.mkdirSync(recoveryPath, { mode: 0o700 });
@@ -106,7 +117,7 @@ function recoverSettingsLock(lockPath) {
try {
inspected = inspectSettingsLock(lockPath);
} catch (error) {
if (error && error.code === 'ENOENT') return createSettingsLock(lockPath);
if (error && error.code === 'ENOENT') return createSettingsLock(lockPath, label);
throw error;
}
const validOwner = Number.isSafeInteger(inspected.metadata && inspected.metadata.pid)
@@ -123,27 +134,27 @@ function recoverSettingsLock(lockPath) {
return null;
}
fs.rmSync(quarantinePath, { force: true });
return createSettingsLock(lockPath);
return createSettingsLock(lockPath, label);
} finally {
fs.rmSync(recoveryPath, { recursive: true, force: true });
fs.rmSync(quarantinePath, { force: true });
}
}
function acquireSettingsLock(settingsPath) {
function acquireSettingsLock(settingsPath, { label = 'Claude settings' } = {}) {
const lockPath = `${settingsPath}.ecc.lock`;
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
try {
return createSettingsLock(lockPath);
return createSettingsLock(lockPath, label);
} catch (error) {
if (!error || error.code !== 'EEXIST') {
throw error;
}
}
const recovered = recoverSettingsLock(lockPath);
const recovered = recoverSettingsLock(lockPath, label);
if (recovered) return recovered;
throw new Error(
`Another ECC process is updating Claude settings: ${settingsPath}. `
`Another ECC process is updating ${label}: ${settingsPath}. `
+ `If no ECC process is active, inspect and remove ${lockPath}.`
);
}
@@ -175,6 +186,7 @@ function runWithSettingsLock(settingsPath, callback) {
module.exports = {
acquireSettingsLock,
getSettingsLockIdentity,
runWithSettingsLock,
sameFileIdentity,
};
+115
View File
@@ -0,0 +1,115 @@
'use strict';
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { sameFileIdentity } = require('./claude-settings-lock');
function snapshotExpectedContent(expectedContent) {
if (expectedContent === undefined) return undefined;
if (expectedContent && expectedContent.kind === 'absent') {
return Object.freeze({ kind: 'absent' });
}
if (expectedContent && expectedContent.kind === 'sha256'
&& typeof expectedContent.digest === 'string' && /^[a-f0-9]{64}$/i.test(expectedContent.digest)) {
return Object.freeze({ kind: 'sha256', digest: expectedContent.digest.toLowerCase() });
}
throw new TypeError('Invalid expectedContent for guarded write.');
}
function changedDestination(action, filePath, expectedContent, cause) {
const message = expectedContent
? `Refusing OpenCode hook deactivation: activation changed after preflight at ${filePath}`
: `Refusing to ${action}: managed destination changed during the write.`;
const error = new Error(message);
if (cause) {
error.cause = cause;
if (cause.code) error.code = cause.code;
}
return error;
}
function writeFileNoFollow(filePath, content, {
mode,
action = 'write managed file',
validateDestination,
expectedContent: requestedContent,
} = {}) {
if (typeof validateDestination !== 'function') {
throw new TypeError('writeFileNoFollow requires validateDestination.');
}
const destination = path.resolve(filePath);
const expectedContent = snapshotExpectedContent(requestedContent);
const bytes = typeof content === 'string' ? Buffer.from(content) : content;
if (!Buffer.isBuffer(bytes)) throw new TypeError('Managed file content must be a string or Buffer.');
const changed = cause => changedDestination(action, destination, expectedContent, cause);
function validatePath() {
const validated = validateDestination(destination);
if (typeof validated !== 'string' || path.resolve(validated) !== destination) throw changed();
}
function parentStats() {
const stats = fs.lstatSync(path.dirname(destination), { bigint: true });
if (!stats.isDirectory() || stats.isSymbolicLink()) throw changed();
return stats;
}
validatePath();
const parent = parentStats();
const flags = (expectedContent ? fs.constants.O_RDWR : fs.constants.O_WRONLY)
| (!expectedContent || expectedContent.kind === 'absent' ? fs.constants.O_CREAT : 0)
| (expectedContent && expectedContent.kind === 'absent' ? fs.constants.O_EXCL : 0)
| (fs.constants.O_NOFOLLOW || 0);
let descriptor;
try {
descriptor = fs.openSync(destination, flags, mode);
} catch (error) {
if (expectedContent) throw changed(error);
throw error;
}
function assertPinned() {
validatePath();
const descriptorStat = fs.fstatSync(descriptor, { bigint: true });
const liveStat = fs.lstatSync(destination, { bigint: true });
if (!sameFileIdentity(parent, parentStats()) || !descriptorStat.isFile()
|| !liveStat.isFile() || liveStat.isSymbolicLink() || !sameFileIdentity(descriptorStat, liveStat)) {
throw changed();
}
return descriptorStat;
}
let primaryError;
try {
const before = assertPinned();
if (expectedContent && expectedContent.kind === 'absent' && before.size !== 0n) throw changed();
if (expectedContent && expectedContent.kind === 'sha256') {
const digest = crypto.createHash('sha256').update(fs.readFileSync(descriptor)).digest('hex');
const after = assertPinned();
if (!sameFileIdentity(before, after) || before.size !== after.size
|| before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs
|| digest !== expectedContent.digest) throw changed();
}
// Observed changes are rejected before truncation. This is not a CAS against
// arbitrary editors: callers coordinate participating writers with a lease.
fs.ftruncateSync(descriptor, 0);
for (let offset = 0; offset < bytes.length;) {
const written = fs.writeSync(descriptor, bytes, offset, bytes.length - offset, offset);
if (!Number.isInteger(written) || written <= 0 || written > bytes.length - offset) {
throw new Error(`Refusing to ${action}: file write made no valid progress.`);
}
offset += written;
}
if (mode !== undefined) fs.fchmodSync(descriptor, mode);
} catch (error) {
primaryError = error;
} finally {
try {
fs.closeSync(descriptor);
} catch (error) {
if (primaryError) primaryError.closeError = error;
else primaryError = error;
}
}
// An exclusive creation that later fails is not unlinked: the pathname may
// already belong to another writer. Keep the refusal visible to the caller.
if (primaryError) throw primaryError;
}
module.exports = { writeFileNoFollow };
+123 -3
View File
@@ -38,11 +38,54 @@ const HOOK_CAPABILITY_GROUPS = Object.freeze([
const HOOK_CONSENT_DECISIONS = Object.freeze(['enabled', 'declined']);
const HOOK_RUNTIME_MODULE_ID = 'hooks-runtime';
const OPENCODE_DISABLE_ECC_HOOKS_TRANSFORM = 'opencode-disable-ecc-hooks';
const OPENCODE_DISABLE_PLUGIN_TRANSFORM = 'opencode-disable-plugin-entrypoint';
function normalizeOperationPath(value) {
return String(value || '').replace(/\\/g, '/').toLowerCase();
}
function disableOpenCodeHookPluginRegistration(content, sourceRelativePath) {
let config;
try {
config = JSON.parse(content);
} catch (error) {
throw new Error(`Failed to parse ${sourceRelativePath}: ${error.message}`);
}
if (!config || typeof config !== 'object' || Array.isArray(config)) {
throw new Error(`Invalid ${sourceRelativePath}: expected a JSON object`);
}
if (config.plugin !== undefined && !Array.isArray(config.plugin)) {
throw new Error(`Invalid ${sourceRelativePath}: plugin must be an array`);
}
if (!Array.isArray(config.plugin)) {
return `${JSON.stringify(config, null, 2)}\n`;
}
return `${JSON.stringify({
...config,
plugin: config.plugin.filter(plugin => plugin !== './plugins'),
}, null, 2)}\n`;
}
function isOpenCodePluginEntrypoint(operation = {}) {
return /^\.opencode\/(?:dist\/)?plugins\/[^/]+\.(?:[cm]?js|ts)$/.test(
normalizeOperationPath(operation.sourceRelativePath)
);
}
function getDisabledOpenCodePluginContent() {
// OpenCode discovers plugins independently of opencode.json registration.
// Do not import the original module: even module initialization has effects.
return 'export default async () => ({});\n';
}
function isOpenCodeHookActivationOperation(operation = {}) {
return normalizeOperationPath(operation.sourceRelativePath) === '.opencode/opencode.json'
|| isOpenCodePluginEntrypoint(operation);
}
function isHookRuntimeOperation(operation = {}) {
if (
operation.kind === 'update-claude-settings'
@@ -51,6 +94,15 @@ function isHookRuntimeOperation(operation = {}) {
return true;
}
if (isOpenCodeHookActivationOperation(operation)) {
return !(
operation.kind === 'copy-file'
&& operation.contentTransform === (isOpenCodePluginEntrypoint(operation)
? OPENCODE_DISABLE_PLUGIN_TRANSFORM
: OPENCODE_DISABLE_ECC_HOOKS_TRANSFORM)
);
}
const source = normalizeOperationPath(operation.sourceRelativePath);
const destination = normalizeOperationPath(operation.destinationPath);
return (
@@ -93,6 +145,57 @@ function withoutHookRuntimeId(values) {
return (Array.isArray(values) ? values : []).filter(value => value !== HOOK_RUNTIME_MODULE_ID);
}
function withoutOpenCodeHookActivation(operation) {
if (
!isOpenCodeHookActivationOperation(operation)
|| operation.kind !== 'copy-file'
) {
return operation;
}
return {
...operation,
contentTransform: isOpenCodePluginEntrypoint(operation)
? OPENCODE_DISABLE_PLUGIN_TRANSFORM
: OPENCODE_DISABLE_ECC_HOOKS_TRANSFORM,
};
}
function transformOpenCodeHookActivationOperations(operations) {
return (Array.isArray(operations) ? operations : []).map(withoutOpenCodeHookActivation);
}
function planSelectsHookRuntime(plan = {}) {
return (
Array.isArray(plan.selectedModuleIds)
&& plan.selectedModuleIds.includes(HOOK_RUNTIME_MODULE_ID)
) || (
Array.isArray(plan.operations)
&& plan.operations.some(operation => operation.moduleId === HOOK_RUNTIME_MODULE_ID)
);
}
function disableUnselectedOpenCodeHooks(plan) {
if (plan.target !== 'opencode' || planSelectsHookRuntime(plan)) {
return plan;
}
return {
...plan,
operations: transformOpenCodeHookActivationOperations(plan.operations),
statePreview: plan.statePreview
? {
...plan.statePreview,
operations: transformOpenCodeHookActivationOperations(plan.statePreview.operations),
}
: plan.statePreview,
};
}
function shouldDisableOpenCodeHooks(plan = {}) {
return plan.target === 'opencode'
&& (plan.hookConsent === 'declined' || !planSelectsHookRuntime(plan));
}
function setStatePreviewHookConsent(statePreview, hookConsent) {
if (!statePreview || !statePreview.request) {
return statePreview;
@@ -123,7 +226,11 @@ function getRecordedHookConsent(state = {}) {
return 'enabled';
}
if (planMaterializesHookRuntime(state)) {
// Older OpenCode installs copied activation files without asking for consent.
// Their presence cannot establish that the user opted in to automatic hooks.
if ((Array.isArray(state.operations) ? state.operations : []).some(operation => (
!isOpenCodeHookActivationOperation(operation) && isHookRuntimeOperation(operation)
))) {
return 'enabled';
}
@@ -134,11 +241,17 @@ function stripHookRuntimeFromPlan(plan) {
const hadHookRuntimeModule = Array.isArray(plan.selectedModuleIds)
&& plan.selectedModuleIds.includes('hooks-runtime');
const operations = (Array.isArray(plan.operations) ? plan.operations : [])
.map(operation => (
plan.target === 'opencode' ? withoutOpenCodeHookActivation(operation) : operation
))
.filter(operation => !isHookRuntimeOperation(operation));
const statePreview = plan.statePreview
? {
...plan.statePreview,
operations: (Array.isArray(plan.statePreview.operations) ? plan.statePreview.operations : [])
.map(operation => (
plan.target === 'opencode' ? withoutOpenCodeHookActivation(operation) : operation
))
.filter(operation => !isHookRuntimeOperation(operation)),
resolution: plan.statePreview.resolution
? {
@@ -167,10 +280,11 @@ function withHookConsent(plan, hookConsent = null) {
if (hookConsent === 'declined') {
return { ...stripHookRuntimeFromPlan(plan), hookConsent };
}
const effectivePlan = disableUnselectedOpenCodeHooks(plan);
return {
...plan,
...effectivePlan,
hookConsent,
statePreview: setStatePreviewHookConsent(plan.statePreview, hookConsent),
statePreview: setStatePreviewHookConsent(effectivePlan.statePreview, hookConsent),
};
}
@@ -193,10 +307,16 @@ function assertHookConsentReady(plan = {}) {
module.exports = {
HOOK_CAPABILITY_GROUPS,
assertHookConsentReady,
disableUnselectedOpenCodeHooks,
disableOpenCodeHookPluginRegistration,
getDisabledOpenCodePluginContent,
formatHookCapabilityDisclosure,
getRecordedHookConsent,
isHookRuntimeOperation,
isOpenCodeHookActivationOperation,
isOpenCodePluginEntrypoint,
planMaterializesHookRuntime,
resolveHookConsentFlags,
shouldDisableOpenCodeHooks,
withHookConsent,
};
@@ -0,0 +1,145 @@
'use strict';
const fs = require('fs');
const path = require('path');
const { realpathNearestExisting } = require('../path-safety');
const { acquireSettingsLock, getSettingsLockIdentity, sameFileIdentity } = require('./claude-settings-lock');
const activeLeases = new WeakMap();
const STATE_FILENAME = 'ecc-install-state.json';
const comparablePath = value => process.platform === 'win32' ? value.toLowerCase() : value;
function inspectEntry(filePath) {
try { return fs.lstatSync(filePath, { bigint: true }); }
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
}
function canonicalRoots(roots) {
if (!Array.isArray(roots)) throw new TypeError('OpenCode install roots must be an array.');
const deduplicated = new Map();
for (const root of roots) {
if (typeof root !== 'string' || !path.isAbsolute(root)) {
throw new TypeError('OpenCode install root must be an absolute trusted path.');
}
const stats = inspectEntry(root);
if (stats && (stats.isSymbolicLink() || !stats.isDirectory())) {
throw new Error(`Refusing OpenCode install lock through a non-directory or symlink root: ${root}`);
}
const canonical = realpathNearestExisting(root);
deduplicated.set(comparablePath(canonical), canonical);
}
return [...deduplicated.values()].sort((left, right) => {
const a = comparablePath(left);
const b = comparablePath(right);
return a < b ? -1 : a > b ? 1 : 0;
});
}
function assertLockLocation(root, expectedRoot) {
const stats = inspectEntry(root);
if (!stats || !stats.isDirectory() || stats.isSymbolicLink()
|| comparablePath(fs.realpathSync(root)) !== comparablePath(root)
|| (expectedRoot && !sameFileIdentity(stats, expectedRoot))) {
throw new Error(`Refusing changed OpenCode install lock root: ${root}`);
}
const lock = inspectEntry(path.join(root, `${STATE_FILENAME}.ecc.lock`));
if (lock && (!lock.isFile() || lock.isSymbolicLink())) {
throw new Error(`Refusing non-file or symlink OpenCode install lock: ${root}`);
}
return { root: stats, lock };
}
function assertOwnedLock(owned) {
const live = assertLockLocation(owned.root, owned.rootStats);
if (!live.lock || !sameFileIdentity(live.lock, owned.lockStats)) {
throw new Error(`Refusing changed OpenCode install lock: ${owned.root}`);
}
}
function annotateCleanupFailure(primary, property, value) {
try {
// Own data properties avoid invoking caller getters/setters. Frozen values,
// primitives and rejecting proxy traps simply retain no extra diagnostic.
Object.defineProperty(primary, property, { value, configurable: true, enumerable: true, writable: true });
} catch {
// Diagnostics must never replace the exact primary thrown value.
}
}
function releaseOwned(ownedLocks) {
const failures = [];
for (const owned of [...ownedLocks].reverse()) {
try {
assertOwnedLock(owned);
owned.release();
} catch (error) {
failures.push(error);
}
}
// Finish every safe release before touching any caller-owned error object.
if (failures.length > 0) {
if (failures.length > 1) annotateCleanupFailure(failures[0], 'releaseErrors', failures.slice(1));
throw failures[0];
}
}
function acquireOpenCodeInstallLocks(roots, existingLease) {
const orderedRoots = canonicalRoots(roots);
if (existingLease !== undefined) {
const existing = existingLease && typeof existingLease === 'object' && activeLeases.get(existingLease);
if (!existing) throw new Error('Invalid or inactive OpenCode install lease.');
const covered = new Set(existing.map(owned => comparablePath(owned.root)));
if (orderedRoots.some(root => !covered.has(comparablePath(root)))) {
throw new Error('OpenCode install lease does not cover every required root.');
}
existing.forEach(assertOwnedLock);
return { lease: existingLease, release() {} };
}
const ownedLocks = [];
try {
for (const root of orderedRoots) {
fs.mkdirSync(root, { recursive: true });
const before = assertLockLocation(root);
const release = acquireSettingsLock(path.join(root, STATE_FILENAME), { label: 'OpenCode installation' });
// Bind descriptor-derived ownership before any path revalidation. Never
// adopt the identity of a replacement published at the lock pathname.
const owned = { root, rootStats: before.root, release,
lockStats: getSettingsLockIdentity(release) };
ownedLocks.push(owned);
assertOwnedLock(owned);
}
} catch (error) {
try { releaseOwned(ownedLocks); }
catch (releaseError) { annotateCleanupFailure(error, 'releaseError', releaseError); }
throw error;
}
const lease = Object.freeze({});
activeLeases.set(lease, ownedLocks);
return {
lease,
release() {
if (!activeLeases.has(lease)) return;
activeLeases.delete(lease);
releaseOwned(ownedLocks);
},
};
}
function withOpenCodeInstallLocks(roots, callback, existingLease) {
if (typeof callback !== 'function') throw new TypeError('OpenCode install lock callback must be a function.');
const holder = acquireOpenCodeInstallLocks(roots, existingLease);
let didThrow = false;
let primaryError;
let result;
try { result = callback(holder.lease); }
catch (error) { didThrow = true; primaryError = error; }
try { holder.release(); }
catch (error) {
if (didThrow) annotateCleanupFailure(primaryError, 'releaseError', error);
else { didThrow = true; primaryError = error; }
}
if (didThrow) throw primaryError;
return result;
}
module.exports = { acquireOpenCodeInstallLocks, withOpenCodeInstallLocks };
@@ -393,6 +393,7 @@ function cleanupLegacyOpencodeInstall(plan) {
module.exports = {
cleanupLegacyOpencodeInstall,
getLegacyOpencodeLocation,
getLegacyLocationForPlan,
inspectLegacyOpencodeState,
removeVerifiedLegacyFile,
};
+3 -2
View File
@@ -7,6 +7,7 @@ const { execFileSync } = require('child_process');
const { resolveInstallPlan } = require('../install-manifests');
const { getInstallTargetAdapter } = require('../install-targets/registry');
const { resolveInvocationEnvironment } = require('../invocation-environment');
const { disableUnselectedOpenCodeHooks } = require('./hook-consent');
const { readHooksConfig } = require('../hooks-config');
const {
materializeManagedHooks,
@@ -315,7 +316,7 @@ function createManifestInstallPlan(options = {}) {
source
});
return {
return disableUnselectedOpenCodeHooks({
mode: options.mode || 'manifest',
sourceRoot,
target,
@@ -341,7 +342,7 @@ function createManifestInstallPlan(options = {}) {
excludedModuleIds: plan.excludedModuleIds,
operations,
statePreview
};
});
}
module.exports = {
+283
View File
@@ -0,0 +1,283 @@
/** Focused descriptor-bound writes; every filesystem fixture is private. */
'use strict';
const assert = require('assert');
const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { writeFileNoFollow } = require('../../scripts/lib/install/guarded-write');
const digest = value => crypto.createHash('sha256').update(value).digest('hex');
let passed = 0;
let failed = 0;
function test(name, callback) {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-guarded-')));
const file = path.join(root, 'entry.js');
const options = { action: 'replace activation', validateDestination: value => value };
try { callback({ root, file, options }); passed++; console.log(` PASS ${name}`); }
catch (error) { failed++; console.error(` FAIL ${name}: ${error.stack}`); }
finally { fs.rmSync(root, { recursive: true, force: true }); }
}
function replaceMethod(name, replacement, callback) {
const original = fs[name];
fs[name] = replacement(original);
try { callback(); } finally { fs[name] = original; }
}
function existing(options, content = 'old activation bytes') {
return { ...options, expectedContent: Object.freeze({ kind: 'sha256', digest: digest(content) }) };
}
test('hashing then writing shorter bytes starts at zero and preserves the requested mode', ({ file, options }) => {
fs.writeFileSync(file, 'old activation bytes');
writeFileNoFollow(file, 'off\n', { ...existing(options), mode: 0o600 });
assert.deepStrictEqual(fs.readFileSync(file), Buffer.from('off\n'));
assert.strictEqual(fs.statSync(file).size, 4);
if (process.platform !== 'win32') assert.strictEqual(fs.statSync(file).mode & 0o777, 0o600);
});
test('short writes are completed at explicit positions', ({ file, options }) => {
const positions = [];
replaceMethod('writeSync', original => (fd, buffer, offset, length, position) => {
positions.push(position);
return original(fd, buffer, offset, Math.min(length, 2), position);
}, () => writeFileNoFollow(file, 'abcdef', options));
assert.deepStrictEqual(positions, [0, 2, 4]);
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'abcdef');
});
test('expected absence creates a new exact file', ({ file, options }) => {
writeFileNoFollow(file, Buffer.from('inert'), { ...options, expectedContent: { kind: 'absent' } });
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'inert');
});
test('a competing file before exclusive open is preserved', ({ file, options }) => {
replaceMethod('openSync', original => (...args) => {
if (args[0] === file && typeof args[1] === 'number') fs.writeFileSync(file, 'user bytes');
return original(...args);
}, () => assert.throws(() => writeFileNoFollow(file, 'off', {
...options, expectedContent: { kind: 'absent' },
}), /changed after preflight/));
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'user bytes');
});
test('existing content removed before open is never recreated', ({ file, options }) => {
fs.writeFileSync(file, 'old activation bytes');
replaceMethod('openSync', original => (...args) => {
if (args[0] === file) fs.unlinkSync(file);
return original(...args);
}, () => assert.throws(() => writeFileNoFollow(file, 'off', existing(options)), /changed after preflight/));
assert.strictEqual(fs.existsSync(file), false);
});
test('same-inode edit at the writable-open boundary refuses before truncate and closes once', ({ file, options }) => {
fs.writeFileSync(file, 'old activation bytes');
let descriptor;
let closes = 0;
let truncates = 0;
const originalOpen = fs.openSync;
const originalClose = fs.closeSync;
const originalTruncate = fs.ftruncateSync;
fs.openSync = (...args) => {
const fd = originalOpen(...args);
if (args[0] === file && typeof args[1] === 'number') {
descriptor = fd;
fs.writeFileSync(file, 'user edited activation');
}
return fd;
};
fs.closeSync = fd => { if (fd === descriptor) closes++; return originalClose(fd); };
fs.ftruncateSync = (...args) => { truncates++; return originalTruncate(...args); };
try { assert.throws(() => writeFileNoFollow(file, 'off', existing(options)), /changed after preflight/); }
finally { fs.openSync = originalOpen; fs.closeSync = originalClose; fs.ftruncateSync = originalTruncate; }
assert.strictEqual(closes, 1);
assert.strictEqual(truncates, 0);
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'user edited activation');
});
test('a descriptor read concurrent edit is observed before truncation', ({ file, options }) => {
fs.writeFileSync(file, 'old activation bytes');
replaceMethod('readFileSync', original => (...args) => {
const result = original(...args);
if (typeof args[0] === 'number') fs.writeFileSync(file, 'raced during hash');
return result;
}, () => assert.throws(() => writeFileNoFollow(file, 'off', existing(options)), /changed after preflight/));
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'raced during hash');
});
test('a pathname replacement after opening preserves both original and replacement', ({ file, options }) => {
fs.writeFileSync(file, 'old activation bytes');
replaceMethod('openSync', original => (...args) => {
const fd = original(...args);
if (args[0] === file && typeof args[1] === 'number') {
fs.renameSync(file, `${file}.old`);
fs.writeFileSync(file, 'replacement');
}
return fd;
}, () => assert.throws(() => writeFileNoFollow(file, 'off', existing(options)), /changed/));
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'replacement');
assert.strictEqual(fs.readFileSync(`${file}.old`, 'utf8'), 'old activation bytes');
});
test('a directory destination is refused without writes', ({ file, options }) => {
fs.mkdirSync(file);
assert.throws(() => writeFileNoFollow(file, 'off', options));
assert.ok(fs.statSync(file).isDirectory());
});
test('a parent replacement before native open is refused even when the file identity is unchanged', ({ root, options }) => {
const parent = path.join(root, 'plugins');
fs.mkdirSync(parent);
const file = path.join(parent, 'entry.js');
fs.writeFileSync(file, 'old activation bytes');
const fileIdentity = fs.statSync(file, { bigint: true });
const originalOpen = fs.openSync;
const originalClose = fs.closeSync;
const originalTruncate = fs.ftruncateSync;
let descriptor;
let swaps = 0;
let closes = 0;
let truncates = 0;
fs.openSync = (...args) => {
if (args[0] === file && typeof args[1] === 'number') {
// The writer has pinned the parent, but has not opened the file yet.
// Moving an open file's parent is not portable to Windows. Keep the
// same file inode and bytes so only the parent identity rejects this.
fs.renameSync(parent, `${parent}.old`);
fs.mkdirSync(parent);
fs.renameSync(path.join(`${parent}.old`, 'entry.js'), file);
swaps++;
descriptor = originalOpen(...args);
return descriptor;
}
return originalOpen(...args);
};
fs.closeSync = fd => { if (fd === descriptor) closes++; return originalClose(fd); };
fs.ftruncateSync = (...args) => { truncates++; return originalTruncate(...args); };
try {
assert.throws(() => writeFileNoFollow(file, 'off', existing(options)), error => {
assert.match(error.message, /changed after preflight/);
assert.strictEqual(error.cause, undefined, 'the parent guard, not an open error, refuses');
return true;
});
} finally {
fs.openSync = originalOpen;
fs.closeSync = originalClose;
fs.ftruncateSync = originalTruncate;
}
assert.strictEqual(swaps, 1);
assert.strictEqual(typeof descriptor, 'number');
assert.strictEqual(closes, 1);
assert.strictEqual(truncates, 0);
const replacementIdentity = fs.statSync(file, { bigint: true });
assert.strictEqual(replacementIdentity.dev, fileIdentity.dev);
assert.strictEqual(replacementIdentity.ino, fileIdentity.ino);
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'old activation bytes');
assert.ok(fs.statSync(`${parent}.old`).isDirectory());
});
test('a denied native open preserves its cause without closing an unallocated descriptor', ({ file, options }) => {
fs.writeFileSync(file, 'old activation bytes');
const denied = Object.assign(new Error('fixture open denied'), { code: 'EPERM' });
const originalClose = fs.closeSync;
const originalTruncate = fs.ftruncateSync;
let closes = 0;
let truncates = 0;
fs.closeSync = fd => { closes++; return originalClose(fd); };
fs.ftruncateSync = (...args) => { truncates++; return originalTruncate(...args); };
try {
replaceMethod('openSync', original => (...args) => {
if (args[0] === file && typeof args[1] === 'number') throw denied;
return original(...args);
}, () => assert.throws(() => writeFileNoFollow(file, 'off', existing(options)), error => {
assert.strictEqual(error.cause, denied);
assert.strictEqual(error.code, 'EPERM');
return true;
}));
} finally {
fs.closeSync = originalClose;
fs.ftruncateSync = originalTruncate;
}
assert.strictEqual(closes, 0);
assert.strictEqual(truncates, 0);
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'old activation bytes');
});
test('destination validator is mandatory and must return the same path', ({ file, options }) => {
assert.throws(() => writeFileNoFollow(file, 'off', {}), /validateDestination/);
assert.throws(() => writeFileNoFollow(file, 'off', { ...options, validateDestination: () => `${file}.other` }), /changed/);
assert.strictEqual(fs.existsSync(file), false);
});
test('destination validation runs again after the descriptor hash', ({ file, options }) => {
fs.writeFileSync(file, 'old activation bytes');
let validations = 0;
assert.throws(() => writeFileNoFollow(file, 'off', {
...existing(options), validateDestination: value => {
validations++;
if (validations === 3) throw new Error('containment changed');
return value;
},
}), /containment changed/);
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'old activation bytes');
});
for (const method of ['readFileSync', 'ftruncateSync', 'writeSync', 'fchmodSync']) {
test(`${method} failure closes once and preserves the primary error if close also fails`, ({ file, options }) => {
fs.writeFileSync(file, 'old activation bytes');
const primary = new Error(`${method} primary`);
const secondary = new Error('close secondary');
const original = fs[method];
const close = fs.closeSync;
let closes = 0;
fs[method] = (...args) => { if (typeof args[0] === 'number') throw primary; return original(...args); };
fs.closeSync = fd => { closes++; close(fd); throw secondary; };
try {
assert.throws(() => writeFileNoFollow(file, 'off', { ...existing(options), mode: 0o600 }), error => {
assert.strictEqual(error, primary);
assert.strictEqual(error.closeError, secondary);
return true;
});
} finally { fs[method] = original; fs.closeSync = close; }
assert.strictEqual(closes, 1);
});
}
test('close failure after a successful write is reported', ({ file, options }) => {
replaceMethod('closeSync', original => fd => { original(fd); throw new Error('close failed'); }, () => {
assert.throws(() => writeFileNoFollow(file, 'off', options), /close failed/);
});
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'off');
});
test('zero-progress writes refuse instead of spinning', ({ file, options }) => {
replaceMethod('writeSync', () => () => 0, () => assert.throws(() => writeFileNoFollow(file, 'off', options), /progress/));
});
test('malformed expected-content guards fail before opening a destination', ({ file, options }) => {
for (const expectedContent of [null, true, { kind: 'sha256', digest: 'bad' }, { kind: 'missing' }]) {
assert.throws(() => writeFileNoFollow(file, 'off', { ...options, expectedContent }), /expectedContent/);
}
assert.strictEqual(fs.existsSync(file), false);
});
test('a new exclusive file changed through the owned descriptor is preserved', ({ file, options }) => {
replaceMethod('openSync', original => (...args) => {
const fd = original(...args);
if (args[0] === file && typeof args[1] === 'number') fs.writeSync(fd, Buffer.from('raced creation'), 0, 14, 0);
return fd;
}, () => assert.throws(() => writeFileNoFollow(file, 'off', {
...options, expectedContent: { kind: 'absent' },
}), /changed after preflight/));
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'raced creation');
});
test('the final pathname must remain a regular non-symlink even without O_NOFOLLOW support', ({ file, options }) => {
fs.writeFileSync(file, 'old activation bytes');
replaceMethod('lstatSync', original => (...args) => {
const stats = original(...args);
if (args[0] === file) {
const simulatedSymlink = Object.create(stats);
simulatedSymlink.isSymbolicLink = () => true;
return simulatedSymlink;
}
return stats;
}, () => assert.throws(() => writeFileNoFollow(file, 'off', existing(options)), /changed/));
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'old activation bytes');
});
test('the immutable preflight digest cannot be changed by a validator callback', ({ file, options }) => {
fs.writeFileSync(file, 'user bytes');
const expectedContent = { kind: 'sha256', digest: digest('old bytes') };
assert.throws(() => writeFileNoFollow(file, 'off', {
...options, expectedContent, validateDestination: value => {
expectedContent.digest = digest('user bytes');
return value;
},
}), /changed after preflight/);
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'user bytes');
});
test('unexpected input fails without creating a file', ({ file, options }) => {
assert.throws(() => writeFileNoFollow(file, {}, options), /content/);
assert.strictEqual(fs.existsSync(file), false);
});
console.log(`Results: Passed: ${passed}, Failed: ${failed}`);
process.exitCode = failed ? 1 : 0;
+87 -1
View File
@@ -7,7 +7,9 @@ const assert = require('assert');
const {
HOOK_CAPABILITY_GROUPS,
assertHookConsentReady,
disableOpenCodeHookPluginRegistration,
formatHookCapabilityDisclosure,
getRecordedHookConsent,
isHookRuntimeOperation,
planMaterializesHookRuntime,
resolveHookConsentFlags,
@@ -98,8 +100,25 @@ function runTests() {
sourceRelativePath: '.opencode/plugins/ecc-hooks.ts',
destinationPath: '/root/.config/opencode/plugins/ecc-hooks.ts',
}),
false
true
);
assert.strictEqual(isHookRuntimeOperation({
kind: 'copy-file',
moduleId: 'platform-configs',
sourceRelativePath: '.opencode/opencode.json',
}), true);
assert.strictEqual(isHookRuntimeOperation({
kind: 'copy-file',
moduleId: 'platform-configs',
sourceRelativePath: '.opencode/opencode.json',
contentTransform: 'opencode-disable-ecc-hooks',
}), false);
assert.strictEqual(isHookRuntimeOperation({
kind: 'merge-json',
moduleId: 'platform-configs',
sourceRelativePath: '.opencode/opencode.json',
contentTransform: 'opencode-disable-ecc-hooks',
}), true);
assert.strictEqual(isHookRuntimeOperation({ sourceRelativePath: 'rules/common.md' }), false);
assert.strictEqual(
isHookRuntimeOperation({ sourceRelativePath: 'skills/webhooks-guide.md' }),
@@ -107,6 +126,41 @@ function runTests() {
);
})) passed++; else failed++;
if (test('OpenCode auto-discovered entrypoints require selected runtime and consent', () => {
const entrypoints = ['.opencode/plugins/ecc-hooks.ts', '.opencode/plugins/index.ts'];
const operations = entrypoints.map(sourceRelativePath => ({
kind: 'copy-file', moduleId: 'platform-configs', sourceRelativePath,
}));
const base = {
target: 'opencode', operations, selectedModuleIds: ['platform-configs'],
statePreview: { operations, request: {}, resolution: { selectedModules: ['platform-configs'] } },
};
for (const decision of [null, 'enabled', 'declined']) {
const plan = withHookConsent(base, decision);
for (const operation of [...plan.operations, ...plan.statePreview.operations]) {
assert.strictEqual(operation.contentTransform, 'opencode-disable-plugin-entrypoint');
assert.strictEqual(isHookRuntimeOperation(operation), false);
}
assert.doesNotThrow(() => assertHookConsentReady(plan));
}
const selected = { ...base, selectedModuleIds: ['platform-configs', 'hooks-runtime'] };
const pending = withHookConsent(selected, null);
assert.throws(() => assertHookConsentReady(pending), /automatic hook runtime/);
const enabled = withHookConsent(selected, 'enabled');
assert.ok(enabled.operations.every(operation => operation.contentTransform === undefined));
assert.doesNotThrow(() => assertHookConsentReady(enabled));
const declined = withHookConsent(selected, 'declined');
assert.strictEqual(declined.operations.length, 2);
assert.ok(declined.operations.every(operation => (
operation.contentTransform === 'opencode-disable-plugin-entrypoint'
)));
assert.ok(operations.every(operation => operation.contentTransform === undefined), 'Input plan stays unchanged');
assert.strictEqual(isHookRuntimeOperation({
kind: 'copy-file', moduleId: 'platform-configs',
sourceRelativePath: '.opencode/plugins/helpers/readme.md',
}), false);
})) passed++; else failed++;
if (test('detects hook materialization from plan operations only', () => {
assert.strictEqual(planMaterializesHookRuntime(buildHookPlan()), true);
assert.strictEqual(planMaterializesHookRuntime({
@@ -116,6 +170,38 @@ function runTests() {
assert.strictEqual(planMaterializesHookRuntime({}), false);
})) passed++; else failed++;
if (test('removes only ECC hook activation from OpenCode config', () => {
const transformed = disableOpenCodeHookPluginRegistration(JSON.stringify({
plugin: ['./plugins', 'example-plugin'],
instructions: ['AGENTS.md'],
}), '.opencode/opencode.json');
assert.deepStrictEqual(JSON.parse(transformed), {
plugin: ['example-plugin'],
instructions: ['AGENTS.md'],
});
assert.deepStrictEqual(JSON.parse(disableOpenCodeHookPluginRegistration(
JSON.stringify({ instructions: ['AGENTS.md'] }),
'.opencode/opencode.json'
)), {
instructions: ['AGENTS.md'],
});
})) passed++; else failed++;
if (test('historical OpenCode activation bytes alone do not imply hook consent', () => {
const state = {
request: {}, resolution: { selectedModules: ['platform-configs'] },
operations: [
{ kind: 'copy-file', moduleId: 'platform-configs', sourceRelativePath: '.opencode/opencode.json' },
{ kind: 'copy-file', moduleId: 'platform-configs', sourceRelativePath: '.opencode/plugins/ecc-hooks.ts' },
],
};
assert.strictEqual(getRecordedHookConsent(state), null);
assert.strictEqual(getRecordedHookConsent({ ...state, request: { hookConsent: 'declined' } }), 'declined');
assert.strictEqual(getRecordedHookConsent({ ...state, request: { hookConsent: 'enabled' } }), 'enabled');
assert.strictEqual(getRecordedHookConsent({ ...state, resolution: { selectedModules: ['hooks-runtime'] } }), 'enabled');
assert.strictEqual(getRecordedHookConsent({ operations: [{ kind: 'update-claude-settings' }] }), 'enabled');
})) passed++; else failed++;
if (test('formats one numbered disclosure line per capability group', () => {
const disclosure = formatHookCapabilityDisclosure();
const lines = disclosure.split('\n');
+250
View File
@@ -20,6 +20,8 @@ const {
listAvailableLanguages,
} = require('../../scripts/lib/install-executor');
const { applyInstallPlan: applyInstallPlanDirect } = require('../../scripts/lib/install/apply');
const { normalizeInstallRequest } = require('../../scripts/lib/install/request');
const { createInstallPlanFromRequest } = require('../../scripts/lib/install/runtime');
const { withHookConsent } = require('../../scripts/lib/install/hook-consent');
const REPO_ROOT = path.resolve(__dirname, '..', '..');
@@ -50,6 +52,40 @@ function operationFor(plan, suffix) {
));
}
const INERT_OPENCODE_PLUGIN = 'export default async () => ({});\n';
const OPENCODE_ENTRYPOINTS = ['ecc-hooks.ts', 'index.ts'];
function snapshotFiles(root) {
const entries = [];
function visit(directory, prefix = '') {
if (!fs.existsSync(directory)) return;
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
const relativePath = path.join(prefix, entry.name);
const fullPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
entries.push([relativePath, 'directory']);
visit(fullPath, relativePath);
} else {
entries.push([relativePath, fs.readFileSync(fullPath).toString('base64')]);
}
}
}
visit(root);
return entries;
}
function createOpenCodePlan(homeDir, hookConsent = null, enabledRuntime = false) {
return createInstallPlanFromRequest(normalizeInstallRequest({
target: 'opencode',
moduleIds: enabledRuntime ? ['platform-configs', 'hooks-runtime'] : ['platform-configs'],
enableHooks: hookConsent === 'enabled',
noHooks: hookConsent === 'declined',
}), {
sourceRoot: REPO_ROOT, homeDir, projectRoot: homeDir,
exemptValidationCodes: ['opencode-plugin-not-built'],
});
}
function writeLegacySourceFixture(root) {
writeJson(root, 'package.json', { version: '9.8.7' });
writeFile(root, path.join('rules', 'common', 'coding-style.md'), '# Common\n');
@@ -843,6 +879,220 @@ function runTests() {
}
})) passed++; else failed++;
if (test('OpenCode profile keeps plugin source dormant until hook opt-in is consented', () => {
const homeDir = createTempDir('install-executor-opencode-boundary-');
try {
const planOptions = {
sourceRoot: REPO_ROOT,
homeDir,
projectRoot: homeDir,
exemptValidationCodes: ['opencode-plugin-not-built'],
};
const rawDefaultPlan = createManifestInstallPlan({
...planOptions,
target: 'opencode',
profileId: 'opencode',
});
assert.strictEqual(
rawDefaultPlan.operations.find(operation => (
operation.sourceRelativePath.split(path.sep).join('/') === '.opencode/opencode.json'
)).contentTransform,
'opencode-disable-ecc-hooks'
);
const defaultPlan = createInstallPlanFromRequest(
normalizeInstallRequest({ target: 'opencode', profileId: 'opencode' }),
planOptions
);
const defaultConfig = defaultPlan.operations.find(operation => (
operation.sourceRelativePath.split(path.sep).join('/') === '.opencode/opencode.json'
));
const defaultStateConfig = defaultPlan.statePreview.operations.find(operation => (
operation.sourceRelativePath.split(path.sep).join('/') === '.opencode/opencode.json'
));
assert.strictEqual(defaultConfig.contentTransform, 'opencode-disable-ecc-hooks');
assert.strictEqual(defaultStateConfig.contentTransform, 'opencode-disable-ecc-hooks');
assert.ok(defaultPlan.operations.some(operation => (
operation.sourceRelativePath.split(path.sep).join('/') === '.opencode/plugins/ecc-hooks.ts'
)), 'Default plan should still copy dormant plugin source');
for (const name of OPENCODE_ENTRYPOINTS) {
for (const operations of [defaultPlan.operations, defaultPlan.statePreview.operations]) {
const operation = operations.find(candidate => (
candidate.sourceRelativePath.split(path.sep).join('/') === `.opencode/plugins/${name}`
));
assert.ok(operation, `Plan includes ${name}`);
assert.strictEqual(operation.contentTransform, 'opencode-disable-plugin-entrypoint');
}
}
applyInstallPlanDirect(defaultPlan);
for (const name of OPENCODE_ENTRYPOINTS) {
assert.strictEqual(fs.readFileSync(path.join(homeDir, '.config', 'opencode', 'plugins', name), 'utf8'), INERT_OPENCODE_PLUGIN);
}
const installedConfig = JSON.parse(fs.readFileSync(
path.join(homeDir, '.config', 'opencode', 'opencode.json'),
'utf8'
));
assert.ok(!installedConfig.plugin.includes('./plugins'));
const enabledWithoutRuntime = createInstallPlanFromRequest(
normalizeInstallRequest({
target: 'opencode',
profileId: 'opencode',
enableHooks: true,
}),
planOptions
);
assert.strictEqual(
enabledWithoutRuntime.operations.find(operation => (
operation.sourceRelativePath.split(path.sep).join('/') === '.opencode/opencode.json'
)).contentTransform,
'opencode-disable-ecc-hooks'
);
const declinedPlan = createInstallPlanFromRequest(
normalizeInstallRequest({ target: 'opencode', profileId: 'core', noHooks: true }),
planOptions
);
assert.strictEqual(
declinedPlan.operations.find(operation => (
operation.sourceRelativePath.split(path.sep).join('/') === '.opencode/opencode.json'
)).contentTransform,
'opencode-disable-ecc-hooks'
);
assert.ok(!declinedPlan.operations.some(operation => operation.moduleId === 'hooks-runtime'));
const pendingPlan = createInstallPlanFromRequest(
normalizeInstallRequest({
target: 'opencode',
moduleIds: ['platform-configs', 'hooks-runtime'],
}),
planOptions
);
assert.throws(
() => applyInstallPlanDirect(pendingPlan, { writeInstallState() {} }),
/automatic hook runtime/
);
const enabledPlan = createInstallPlanFromRequest(
normalizeInstallRequest({
target: 'opencode',
moduleIds: ['platform-configs', 'hooks-runtime'],
enableHooks: true,
}),
planOptions
);
const enabledConfig = enabledPlan.operations.find(operation => (
operation.sourceRelativePath.split(path.sep).join('/') === '.opencode/opencode.json'
));
assert.strictEqual(enabledConfig.contentTransform, undefined);
applyInstallPlanDirect(enabledPlan);
for (const name of OPENCODE_ENTRYPOINTS) {
assert.strictEqual(
fs.readFileSync(path.join(homeDir, '.config', 'opencode', 'plugins', name), 'utf8'),
fs.readFileSync(path.join(REPO_ROOT, '.opencode', 'plugins', name), 'utf8')
);
}
applyInstallPlanDirect(declinedPlan);
const declinedState = JSON.parse(fs.readFileSync(declinedPlan.installStatePath, 'utf8'));
assert.strictEqual(declinedState.request.hookConsent, 'declined');
assert.ok(!declinedState.resolution.selectedModules.includes('hooks-runtime'));
assert.ok(!JSON.parse(fs.readFileSync(path.join(declinedPlan.targetRoot, 'opencode.json'), 'utf8')).plugin.includes('./plugins'));
for (const name of OPENCODE_ENTRYPOINTS) {
const destinationPath = path.join(homeDir, '.config', 'opencode', 'plugins', name);
assert.strictEqual(fs.readFileSync(destinationPath, 'utf8'), INERT_OPENCODE_PLUGIN);
const operation = declinedState.operations.find(candidate => candidate.destinationPath === destinationPath);
assert.strictEqual(operation.contentTransform, 'opencode-disable-plugin-entrypoint');
assert.strictEqual(operation.contentSha256, crypto.createHash('sha256').update(INERT_OPENCODE_PLUGIN).digest('hex'));
}
} finally {
cleanup(homeDir);
}
})) passed++; else failed++;
if (test('OpenCode disabled install refuses user-owned activation files before any writes', () => {
for (const relativePath of ['opencode.json', path.join('plugins', 'ecc-hooks.ts'), path.join('plugins', 'index.ts')]) {
const homeDir = createTempDir('install-opencode-user-owned-');
try {
const plan = createOpenCodePlan(homeDir, 'declined');
writeFile(plan.targetRoot, relativePath, relativePath === 'opencode.json'
? '{"plugin":["./plugins"],"theme":"user-owned"}\n'
: 'globalThis.userOwnedPlugin = true; export default async () => ({});\n');
const before = snapshotFiles(homeDir);
assert.throws(() => applyInstallPlanDirect(plan), /OpenCode hook|OpenCode.*activation|OpenCode.*plugin/i);
assert.deepStrictEqual(snapshotFiles(homeDir), before, `No writes when ${relativePath} is user-owned`);
} finally {
cleanup(homeDir);
}
}
})) passed++; else failed++;
if (test('OpenCode disabled install refuses modified or unverifiable managed activation files before writes', () => {
for (const scenario of ['modified-plugin', 'modified-config', 'missing-digest', 'stale-entrypoint']) {
const homeDir = createTempDir('install-opencode-managed-guard-');
try {
const enabledPlan = createOpenCodePlan(homeDir, 'enabled', true);
applyInstallPlanDirect(enabledPlan);
const state = JSON.parse(fs.readFileSync(enabledPlan.installStatePath, 'utf8'));
const pluginPath = path.join(enabledPlan.targetRoot, 'plugins', 'ecc-hooks.ts');
if (scenario === 'modified-plugin') {
fs.appendFileSync(pluginPath, '// user modification\n');
} else if (scenario === 'modified-config') {
fs.appendFileSync(path.join(enabledPlan.targetRoot, 'opencode.json'), ' \n');
} else if (scenario === 'missing-digest') {
delete state.operations.find(operation => operation.destinationPath === pluginPath).contentSha256;
writeJson(homeDir, path.relative(homeDir, enabledPlan.installStatePath), state);
} else {
const stalePath = writeFile(enabledPlan.targetRoot, path.join('plugins', 'legacy-hooks.js'), 'globalThis.legacyHook = true;\n');
state.operations.push({
kind: 'copy-file', moduleId: 'platform-configs', ownership: 'managed', scaffoldOnly: false,
sourceRelativePath: '.opencode/plugins/legacy-hooks.js', destinationPath: stalePath,
strategy: 'preserve-relative-path',
contentSha256: crypto.createHash('sha256').update(fs.readFileSync(stalePath)).digest('hex'),
});
writeJson(homeDir, path.relative(homeDir, enabledPlan.installStatePath), state);
}
const declinedPlan = createOpenCodePlan(homeDir, 'declined');
const before = snapshotFiles(homeDir);
assert.throws(() => applyInstallPlanDirect(declinedPlan), /OpenCode hook|OpenCode.*activation|OpenCode.*plugin/i);
assert.deepStrictEqual(snapshotFiles(homeDir), before, `No writes for ${scenario}`);
} finally {
cleanup(homeDir);
}
}
})) passed++; else failed++;
if (test('OpenCode decline preserves activation changed at the install write boundary', () => {
const homeDir = createTempDir('install-opencode-write-race-');
try {
const enabledPlan = createOpenCodePlan(homeDir, 'enabled', true);
applyInstallPlanDirect(enabledPlan);
const pluginPath = path.join(enabledPlan.targetRoot, 'plugins', 'ecc-hooks.ts');
const changedContent = 'globalThis.userChangedHook = true;\n';
const stateBefore = fs.readFileSync(enabledPlan.installStatePath);
let changed = false;
const declinedPlan = createOpenCodePlan(homeDir, 'declined');
assert.throws(() => applyInstallPlanDirect(declinedPlan, {
beforeOperationWrite({ operation }) {
if (operation.destinationPath === pluginPath) {
changed = true;
fs.writeFileSync(pluginPath, changedContent);
}
},
}), /OpenCode hook.*changed after preflight/i);
assert.strictEqual(changed, true);
assert.strictEqual(fs.readFileSync(pluginPath, 'utf8'), changedContent);
const previousState = JSON.parse(stateBefore.toString('utf8'));
const checkpointState = JSON.parse(fs.readFileSync(enabledPlan.installStatePath, 'utf8'));
const priorOperation = previousState.operations.find(operation => operation.destinationPath === pluginPath);
const checkpointOperation = checkpointState.operations.find(operation => operation.destinationPath === pluginPath);
assert.strictEqual(checkpointOperation.contentSha256, priorOperation.contentSha256, 'Failure checkpoint must not adopt raced activation bytes');
assert.strictEqual(checkpointState.request.hookConsent, 'enabled', 'Failed decline must retain the prior enabled decision');
} finally {
cleanup(homeDir);
}
})) passed++; else failed++;
if (test('Claude hooks install refuses a symlinked hooks destination', () => {
if (process.platform === 'win32') return;
+357
View File
@@ -206,6 +206,27 @@ function writeOpencodeState(homeDir, overrides = {}) {
};
}
function writeRecordedOpenCodeActivation(homeDir) {
const targetRoot = path.join(homeDir, '.config', 'opencode');
const operations = ['opencode.json', 'plugins/ecc-hooks.ts', 'plugins/index.ts'].map(relativePath => {
const sourceRelativePath = `.opencode/${relativePath}`;
const content = fs.readFileSync(path.join(REPO_ROOT, sourceRelativePath));
const destinationPath = path.join(targetRoot, relativePath);
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, content);
return {
kind: 'copy-file', moduleId: 'platform-configs', sourceRelativePath, destinationPath,
ownership: 'managed', scaffoldOnly: false, strategy: 'preserve-relative-path',
contentSha256: crypto.createHash('sha256').update(content).digest('hex'),
};
});
return writeOpencodeState(homeDir, {
request: { modules: ['platform-configs'], hookConsent: null },
resolution: { selectedModules: ['platform-configs'] },
operations,
});
}
function withTemporarilyMovedPath(filePath, callback) {
if (!fs.existsSync(filePath)) {
try {
@@ -1935,6 +1956,342 @@ function runTests() {
}
})) passed++; else failed++;
if (test('doctor dispatches the OpenCode hook-disable content transform consistently', () => {
const projectRoot = createTempDir('install-lifecycle-opencode-transform-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const installStatePath = path.join(targetRoot, 'ecc-install-state.json');
const destinationPath = path.join(targetRoot, 'opencode.json');
const sourceConfig = JSON.parse(fs.readFileSync(
path.join(REPO_ROOT, '.opencode', 'opencode.json'),
'utf8'
));
const expectedContent = `${JSON.stringify({
...sourceConfig,
plugin: sourceConfig.plugin.filter(plugin => plugin !== './plugins'),
}, null, 2)}\n`;
fs.mkdirSync(targetRoot, { recursive: true });
fs.writeFileSync(destinationPath, expectedContent, 'utf8');
writeState(installStatePath, createCursorStateOptions(projectRoot, {
targetRoot,
installStatePath,
operations: [{
kind: 'copy-file',
moduleId: 'platform-configs',
sourceRelativePath: '.opencode/opencode.json',
destinationPath,
strategy: 'preserve-relative-path',
ownership: 'managed',
scaffoldOnly: false,
contentTransform: 'opencode-disable-ecc-hooks',
}],
}));
const report = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir: projectRoot,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(report.results.length, 1);
assert.ok(!report.results[0].issues.some(issue => (
issue.code === 'drifted-managed-files'
|| issue.code === 'unverified-managed-operations'
)));
} finally {
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('OpenCode doctor and repair keep auto-discovered plugin entrypoints inert after declined consent', () => {
const homeDir = createTempDir('install-lifecycle-opencode-inert-');
try {
withTemporarilyMovedPath(path.join(REPO_ROOT, '.opencode', 'dist'), () => {
const plan = createInstallPlanFromRequest({
mode: 'manifest', target: 'opencode', profileId: null,
moduleIds: ['platform-configs'], includeComponentIds: [], excludeComponentIds: [],
legacyLanguages: [], hookConsent: 'declined',
}, {
sourceRoot: REPO_ROOT, homeDir, projectRoot: homeDir,
exemptValidationCodes: ['opencode-plugin-not-built'],
});
applyInstallPlan(plan);
const pluginPaths = ['ecc-hooks.ts', 'index.ts'].map(name => path.join(plan.targetRoot, 'plugins', name));
for (const pluginPath of pluginPaths) {
assert.strictEqual(fs.readFileSync(pluginPath, 'utf8'), 'export default async () => ({});\n');
fs.unlinkSync(pluginPath);
}
const before = buildDoctorReport({ repoRoot: REPO_ROOT, homeDir, projectRoot: homeDir, targets: ['opencode'] });
assert.ok(before.results[0].issues.some(issue => issue.code === 'missing-managed-files'));
let buildCalls = 0;
const repaired = repairInstalledStates({
repoRoot: REPO_ROOT, homeDir, projectRoot: homeDir, targets: ['opencode'],
buildOpencodePayload(repoRoot) {
buildCalls += 1;
const distDir = path.join(repoRoot, '.opencode', 'dist');
fs.mkdirSync(path.join(distDir, 'plugins'), { recursive: true });
fs.mkdirSync(path.join(distDir, 'tools'), { recursive: true });
fs.writeFileSync(path.join(distDir, 'index.js'), 'module.exports = {};\n');
},
});
assert.strictEqual(buildCalls, 1, 'Only the injected inert builder is used');
assert.strictEqual(repaired.results[0].status, 'repaired');
for (const pluginPath of pluginPaths) {
assert.strictEqual(fs.readFileSync(pluginPath, 'utf8'), 'export default async () => ({});\n');
}
const state = readInstallState(plan.installStatePath);
assert.strictEqual(state.request.hookConsent, 'declined');
for (const pluginPath of pluginPaths) {
const operation = state.operations.find(candidate => candidate.destinationPath === pluginPath);
assert.strictEqual(operation.contentTransform, 'opencode-disable-plugin-entrypoint');
}
const after = buildDoctorReport({ repoRoot: REPO_ROOT, homeDir, projectRoot: homeDir, targets: ['opencode'] });
assert.ok(!after.results[0].issues.some(issue => (
['missing-managed-files', 'drifted-managed-files', 'unverified-managed-operations'].includes(issue.code)
)));
});
} finally {
cleanup(homeDir);
}
})) passed++; else failed++;
if (test('OpenCode repair disables older auto-discovered activation without inferring consent', () => {
const homeDir = createTempDir('install-lifecycle-opencode-old-activation-');
try {
withTemporarilyMovedPath(path.join(REPO_ROOT, '.opencode', 'dist'), () => {
const recorded = writeRecordedOpenCodeActivation(homeDir);
const result = repairInstalledStates({
repoRoot: REPO_ROOT, homeDir, projectRoot: homeDir, targets: ['opencode'],
buildOpencodePayload(repoRoot) {
const distDir = path.join(repoRoot, '.opencode', 'dist');
fs.mkdirSync(path.join(distDir, 'plugins'), { recursive: true });
fs.mkdirSync(path.join(distDir, 'tools'), { recursive: true });
fs.writeFileSync(path.join(distDir, 'index.js'), 'module.exports = {};\n');
},
});
assert.strictEqual(result.results[0].status, 'repaired');
for (const name of ['ecc-hooks.ts', 'index.ts']) {
assert.strictEqual(fs.readFileSync(path.join(recorded.targetRoot, 'plugins', name), 'utf8'), 'export default async () => ({});\n');
}
assert.ok(!JSON.parse(fs.readFileSync(path.join(recorded.targetRoot, 'opencode.json'), 'utf8')).plugin.includes('./plugins'));
const state = readInstallState(recorded.installStatePath);
assert.notStrictEqual(state.request.hookConsent, 'enabled');
assert.ok(!state.resolution.selectedModules.includes('hooks-runtime'));
for (const operation of state.operations.filter(operation => (
['.opencode/plugins/ecc-hooks.ts', '.opencode/plugins/index.ts'].includes(operation.sourceRelativePath)
))) {
assert.strictEqual(operation.contentTransform, 'opencode-disable-plugin-entrypoint');
}
});
} finally {
cleanup(homeDir);
}
})) passed++; else failed++;
if (test('OpenCode repair refuses modified activation before building or restoring missing files', () => {
const homeDir = createTempDir('install-lifecycle-opencode-modified-activation-');
try {
withTemporarilyMovedPath(path.join(REPO_ROOT, '.opencode', 'dist'), () => {
const recorded = writeRecordedOpenCodeActivation(homeDir);
const pluginPath = path.join(recorded.targetRoot, 'plugins', 'ecc-hooks.ts');
const missingPath = path.join(recorded.targetRoot, 'plugins', 'index.ts');
const configPath = path.join(recorded.targetRoot, 'opencode.json');
fs.appendFileSync(pluginPath, '// user custom hook\n');
fs.unlinkSync(missingPath);
const before = [pluginPath, configPath, recorded.installStatePath].map(filePath => fs.readFileSync(filePath));
let buildCalls = 0;
const result = repairInstalledStates({
repoRoot: REPO_ROOT, homeDir, projectRoot: homeDir, targets: ['opencode'],
buildOpencodePayload() {
buildCalls += 1;
throw new Error('Guard must run before the builder');
},
});
assert.strictEqual(result.results[0].status, 'error');
assert.match(result.results[0].error, /OpenCode hook|OpenCode.*activation|OpenCode.*plugin/i);
assert.strictEqual(buildCalls, 0);
assert.strictEqual(fs.existsSync(missingPath), false);
assert.deepStrictEqual([pluginPath, configPath, recorded.installStatePath].map(filePath => fs.readFileSync(filePath)), before);
assert.strictEqual(fs.existsSync(path.join(REPO_ROOT, '.opencode', 'dist')), false);
});
} finally {
cleanup(homeDir);
}
})) passed++; else failed++;
if (test('OpenCode repair rejects recorded non-plugin sources at missing activation destinations before building', () => {
for (const kind of ['render-template', 'copy-file']) {
const homeDir = createTempDir('install-lifecycle-opencode-disguised-');
try {
withTemporarilyMovedPath(path.join(REPO_ROOT, '.opencode', 'dist'), () => {
const targetRoot = path.join(homeDir, '.config', 'opencode');
const destinationPath = path.join(targetRoot, 'plugins', 'index.ts');
const recorded = writeOpencodeState(homeDir, {
request: { modules: ['platform-configs'], hookConsent: 'declined', legacyMode: true },
resolution: { selectedModules: ['platform-configs'] },
operations: [managedOperation(kind, destinationPath, {
moduleId: 'platform-configs',
sourceRelativePath: '.claude-plugin/plugin.json.template',
...(kind === 'render-template' ? { renderedContent: 'globalThis.unconsentedHook = true;\n' } : {}),
})],
});
const stateBefore = fs.readFileSync(recorded.installStatePath);
let buildCalls = 0;
const result = repairInstalledStates({
repoRoot: REPO_ROOT, homeDir, projectRoot: homeDir, targets: ['opencode'],
buildOpencodePayload() { buildCalls += 1; throw new Error('Unsafe activation must fail before building'); },
});
assert.strictEqual(result.results[0].status, 'error', kind);
assert.match(result.results[0].error, /OpenCode hook.*unsupported activation operation/i);
assert.strictEqual(buildCalls, 0);
assert.strictEqual(fs.existsSync(destinationPath), false);
assert.strictEqual(fs.existsSync(path.dirname(destinationPath)), false);
assert.strictEqual(fs.existsSync(path.join(REPO_ROOT, '.opencode', 'dist')), false);
assert.strictEqual(crypto.createHash('sha256').update(fs.readFileSync(recorded.installStatePath)).digest('hex'), crypto.createHash('sha256').update(stateBefore).digest('hex'), 'Failed repair must not refresh install-state');
});
} finally {
cleanup(homeDir);
}
}
})) passed++; else failed++;
if (test('OpenCode repair preserves activation bytes changed between health inspection and write', () => {
const homeDir = createTempDir('install-lifecycle-opencode-health-race-');
const originalOpenSync = fs.openSync;
const originalReadFileSync = fs.readFileSync;
const originalCloseSync = fs.closeSync;
const descriptors = new Set();
try {
withTemporarilyMovedPath(path.join(REPO_ROOT, '.opencode', 'dist'), () => {
const recorded = writeRecordedOpenCodeActivation(homeDir);
const pluginPath = path.join(recorded.targetRoot, 'plugins', 'ecc-hooks.ts');
const canonicalPluginPath = fs.realpathSync(pluginPath);
const changedContent = 'globalThis.userChangedHook = true;\n';
const stateBefore = fs.readFileSync(recorded.installStatePath);
let changed = false;
fs.openSync = function trackActivationDescriptor(candidate, ...args) {
const descriptor = originalOpenSync.call(fs, candidate, ...args);
if (typeof candidate === 'string' && [pluginPath, canonicalPluginPath].includes(path.resolve(candidate))) descriptors.add(descriptor);
return descriptor;
};
fs.closeSync = function releaseActivationDescriptor(descriptor) {
descriptors.delete(descriptor);
return originalCloseSync.call(fs, descriptor);
};
fs.readFileSync = function mutateAfterHealthRead(candidate, ...args) {
const content = originalReadFileSync.call(fs, candidate, ...args);
// Lifecycle reads pass an encoding argument; the consent snapshot
// reads the descriptor as raw bytes with no second argument.
if (!changed && descriptors.has(candidate) && args.length > 0) {
changed = true;
fs.writeFileSync(pluginPath, changedContent);
}
return content;
};
let result;
try {
result = repairInstalledStates({
repoRoot: REPO_ROOT, homeDir, projectRoot: homeDir, targets: ['opencode'],
buildOpencodePayload(repoRoot) {
const distDir = path.join(repoRoot, '.opencode', 'dist');
fs.mkdirSync(path.join(distDir, 'plugins'), { recursive: true });
fs.mkdirSync(path.join(distDir, 'tools'), { recursive: true });
fs.writeFileSync(path.join(distDir, 'index.js'), 'module.exports = {};\n');
},
});
} finally {
fs.openSync = originalOpenSync;
fs.readFileSync = originalReadFileSync;
fs.closeSync = originalCloseSync;
}
assert.strictEqual(changed, true, 'The health-read boundary was exercised');
assert.strictEqual(result.results[0].status, 'error');
assert.match(result.results[0].error, /OpenCode hook.*changed after preflight/i);
assert.strictEqual(fs.readFileSync(pluginPath, 'utf8'), changedContent);
const previousState = JSON.parse(stateBefore.toString('utf8'));
const checkpointState = readInstallState(recorded.installStatePath);
const priorOperation = previousState.operations.find(operation => operation.destinationPath === pluginPath);
const checkpointOperation = checkpointState.operations.find(operation => operation.destinationPath === pluginPath);
assert.strictEqual(checkpointOperation.contentSha256, priorOperation.contentSha256, 'Failure checkpoint must not adopt raced activation bytes');
assert.strictEqual(checkpointState.request.hookConsent, previousState.request.hookConsent);
assert.notStrictEqual(result.results[0].stateRefreshed, true);
});
} finally {
fs.openSync = originalOpenSync;
fs.readFileSync = originalReadFileSync;
fs.closeSync = originalCloseSync;
cleanup(homeDir);
}
})) passed++; else failed++;
if (test('OpenCode repair rejects an active alias introduced during writes before refreshing state', () => {
const homeDir = createTempDir('install-lifecycle-opencode-alias-race-');
const originalOpenSync = fs.openSync;
const originalWriteFileSync = fs.writeFileSync;
const originalWriteSync = fs.writeSync;
const originalCloseSync = fs.closeSync;
const descriptors = new Set();
try {
withTemporarilyMovedPath(path.join(REPO_ROOT, '.opencode', 'dist'), () => {
const recorded = writeRecordedOpenCodeActivation(homeDir);
const pluginPath = path.join(recorded.targetRoot, 'plugins', 'index.ts');
const canonicalPluginPath = fs.realpathSync(pluginPath);
const aliasPath = path.join(recorded.targetRoot, 'plugins', 'index.js');
const aliasContent = fs.readFileSync(path.join(REPO_ROOT, '.opencode', 'plugins', 'index.ts'), 'utf8');
const stateBefore = fs.readFileSync(recorded.installStatePath);
let inserted = false;
fs.openSync = function trackPluginWriteDescriptor(candidate, ...args) {
const descriptor = originalOpenSync.call(fs, candidate, ...args);
if (typeof candidate === 'string' && [pluginPath, canonicalPluginPath].includes(path.resolve(candidate))) descriptors.add(descriptor);
return descriptor;
};
fs.closeSync = function releasePluginWriteDescriptor(descriptor) {
descriptors.delete(descriptor);
return originalCloseSync.call(fs, descriptor);
};
fs.writeSync = function insertAliasAfterPluginWrite(candidate, ...args) {
const result = originalWriteSync.call(fs, candidate, ...args);
if (!inserted && descriptors.has(candidate)) {
inserted = true;
originalWriteFileSync.call(fs, aliasPath, aliasContent);
}
return result;
};
let result;
try {
result = repairInstalledStates({
repoRoot: REPO_ROOT, homeDir, projectRoot: homeDir, targets: ['opencode'],
buildOpencodePayload(repoRoot) {
const distDir = path.join(repoRoot, '.opencode', 'dist');
fs.mkdirSync(path.join(distDir, 'plugins'), { recursive: true });
fs.mkdirSync(path.join(distDir, 'tools'), { recursive: true });
fs.writeFileSync(path.join(distDir, 'index.js'), 'module.exports = {};\n');
},
});
} finally {
fs.openSync = originalOpenSync;
fs.writeSync = originalWriteSync;
fs.closeSync = originalCloseSync;
}
assert.strictEqual(inserted, true, 'The plugin-write boundary was exercised');
assert.strictEqual(result.results[0].status, 'error');
assert.match(result.results[0].error, /OpenCode hook activation remains active/i);
assert.strictEqual(fs.readFileSync(aliasPath, 'utf8'), aliasContent);
const checkpointState = readInstallState(recorded.installStatePath);
assert.ok(!checkpointState.operations.some(operation => operation.destinationPath === aliasPath), 'Failed repair must not adopt the raced alias');
assert.strictEqual(checkpointState.request.hookConsent, JSON.parse(stateBefore.toString('utf8')).request.hookConsent);
assert.notStrictEqual(result.results[0].stateRefreshed, true);
});
} finally {
fs.openSync = originalOpenSync;
fs.writeSync = originalWriteSync;
fs.closeSync = originalCloseSync;
cleanup(homeDir);
}
})) passed++; else failed++;
if (test('doctor infers enabled hooks from older manifest install-state records', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
@@ -0,0 +1,196 @@
/** Synthetic legacy migration: both install roots stay locked across build/apply. */
'use strict';
const assert = require('assert');
const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { applyInstallPlan } = require('../../scripts/lib/install/apply');
const { repairInstalledStates } = require('../../scripts/lib/install-lifecycle');
const { createInstallState, readInstallState, writeInstallState } = require('../../scripts/lib/install-state');
const { withOpenCodeInstallLocks } = require('../../scripts/lib/install/opencode-install-lock');
const SOURCE_RELATIVE_PATH = path.join('skills', 'skill-comply', 'SKILL.md');
const SKILL_CONTENT = '---\nname: skill-comply\ndescription: Synthetic migration fixture.\n---\n\n# Inert fixture\n';
const sha256 = value => crypto.createHash('sha256').update(value).digest('hex');
const lockPath = root => path.join(root, 'ecc-install-state.json.ecc.lock');
function writeJson(filePath, value) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
}
function privateFixture(callback) {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-legacy-consent-lock-')));
const sourceRoot = path.join(root, 'source');
const homeDir = path.join(root, 'home');
const legacyRoot = path.join(homeDir, '.opencode');
const canonicalRoot = path.join(homeDir, '.config', 'opencode');
const sourcePath = path.join(sourceRoot, SOURCE_RELATIVE_PATH);
const legacyFile = path.join(legacyRoot, SOURCE_RELATIVE_PATH);
const legacyStatePath = path.join(legacyRoot, 'ecc-install-state.json');
const canonicalStatePath = path.join(canonicalRoot, 'ecc-install-state.json');
const adapter = { id: 'opencode-home', target: 'opencode', kind: 'home' };
try {
writeJson(path.join(sourceRoot, 'package.json'), { name: 'synthetic-ecc-lock-fixture', version: '2.2.2' });
writeJson(path.join(sourceRoot, 'manifests', 'install-modules.json'), {
version: 1,
modules: [{ id: 'workflow-quality', kind: 'skills', description: 'Inert test skill.',
paths: [SOURCE_RELATIVE_PATH], targets: ['opencode'], dependencies: [],
defaultInstall: false, cost: 'light', stability: 'stable' }],
});
writeJson(path.join(sourceRoot, 'manifests', 'install-profiles.json'), { version: 1, profiles: {} });
writeJson(path.join(sourceRoot, 'manifests', 'install-components.json'), { version: 1, components: [] });
for (const filePath of [sourcePath, legacyFile]) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, SKILL_CONTENT);
}
const operation = { kind: 'copy-file', moduleId: 'workflow-quality',
sourceRelativePath: SOURCE_RELATIVE_PATH, destinationPath: legacyFile,
strategy: 'preserve-relative-path', ownership: 'managed', scaffoldOnly: false,
contentSha256: sha256(SKILL_CONTENT) };
const stateOptions = {
adapter,
request: { profile: null, modules: ['workflow-quality'], includeComponents: [],
excludeComponents: [], legacyLanguages: [], legacyMode: false, hookConsent: null },
resolution: { selectedModules: ['workflow-quality'], skippedModules: [] },
source: { repoVersion: '2.2.2', repoCommit: 'synthetic-legacy-lock-fixture', manifestVersion: 1 },
};
writeInstallState(legacyStatePath, createInstallState({ ...stateOptions,
targetRoot: legacyRoot, installStatePath: legacyStatePath, operations: [operation] }));
const canonicalOperation = { ...operation, sourcePath,
destinationPath: path.join(canonicalRoot, SOURCE_RELATIVE_PATH) };
const canonicalPlan = {
target: 'opencode', adapter, sourceRoot, homeDir, targetRoot: canonicalRoot,
installRoot: canonicalRoot, installStatePath: canonicalStatePath,
selectedModuleIds: ['workflow-quality'], operations: [canonicalOperation], warnings: [],
statePreview: createInstallState({ ...stateOptions, targetRoot: canonicalRoot,
installStatePath: canonicalStatePath, operations: [canonicalOperation] }),
};
const sourceFiles = ['package.json', 'manifests/install-modules.json',
'manifests/install-profiles.json', 'manifests/install-components.json', SOURCE_RELATIVE_PATH];
const sourceSnapshot = new Map(sourceFiles.map(relative => [relative,
fs.readFileSync(path.join(sourceRoot, relative))]));
callback({ root, homeDir, sourceRoot, sourcePath, sourceSnapshot, legacyRoot, canonicalRoot,
legacyFile, legacyStatePath, canonicalStatePath, canonicalPlan });
} finally {
// The generated payload and all install paths are inside this fixture.
// No repository assets, compiler output, real user home or providers are used.
fs.rmSync(root, { recursive: true, force: true });
}
}
function repair(value, buildOpencodePayload) {
return repairInstalledStates({ repoRoot: value.sourceRoot, homeDir: value.homeDir,
projectRoot: value.homeDir, targets: ['opencode'], env: {}, buildOpencodePayload });
}
function assertSourceUnchanged(value) {
for (const [relative, bytes] of value.sourceSnapshot) {
assert.deepStrictEqual(fs.readFileSync(path.join(value.sourceRoot, relative)), bytes, relative);
}
}
function assertHeldAndIndependentWritersRefused(value) {
const lockBytes = [value.canonicalRoot, value.legacyRoot].map(targetRoot => {
const bytes = fs.readFileSync(lockPath(targetRoot));
assert.strictEqual(JSON.parse(bytes).pid, process.pid);
return bytes;
});
const legacyBytes = fs.readFileSync(value.legacyStatePath);
assert.throws(() => applyInstallPlan(value.canonicalPlan, {
beforeInstallStateRead() { assert.fail('Independent apply reached state read while migration held both locks'); },
}), /Another ECC process.*OpenCode/);
let nestedBuilds = 0;
const nested = repair(value, () => {
nestedBuilds++;
assert.fail('Independent repair reached build while migration held both locks');
});
assert.strictEqual(nested.results.length, 1, JSON.stringify(nested));
assert.strictEqual(nested.results[0].status, 'error');
assert.match(nested.results[0].error, /Another ECC process.*OpenCode/);
assert.strictEqual(nestedBuilds, 0);
assert.strictEqual(fs.existsSync(value.canonicalStatePath), false);
assert.deepStrictEqual(fs.readFileSync(value.legacyStatePath), legacyBytes);
[value.canonicalRoot, value.legacyRoot].forEach((targetRoot, index) => {
assert.deepStrictEqual(fs.readFileSync(lockPath(targetRoot)), lockBytes[index]);
});
}
function assertBothLocksReleased(value) {
for (const targetRoot of [value.canonicalRoot, value.legacyRoot]) {
assert.strictEqual(fs.existsSync(lockPath(targetRoot)), false);
}
withOpenCodeInstallLocks([value.canonicalRoot, value.legacyRoot], () => {
assert.ok(fs.existsSync(lockPath(value.canonicalRoot)));
assert.ok(fs.existsSync(lockPath(value.legacyRoot)));
});
for (const targetRoot of [value.canonicalRoot, value.legacyRoot]) {
assert.strictEqual(fs.existsSync(lockPath(targetRoot)), false);
}
}
function runTests() {
let passed = 0;
let failed = 0;
function test(name, callback) {
try { privateFixture(callback); passed++; console.log(` PASS ${name}`); }
catch (error) { failed++; console.error(` FAIL ${name}: ${error.stack}`); }
}
test('legacy repair holds both roots before build and releases them after a throwing builder', value => {
const beforeState = fs.readFileSync(value.legacyStatePath);
let builds = 0;
const result = repair(value, sourceRoot => {
builds++;
assert.strictEqual(sourceRoot, value.sourceRoot);
assertHeldAndIndependentWritersRefused(value);
throw new Error('Synthetic builder failure before any source mutation');
});
assert.strictEqual(builds, 1);
assert.strictEqual(result.results.length, 1, JSON.stringify(result));
assert.strictEqual(result.results[0].status, 'error');
assert.match(result.results[0].error, /Synthetic builder failure before any source mutation/);
assert.strictEqual(result.results[0].stateRefreshed, undefined);
assert.deepStrictEqual(fs.readFileSync(value.legacyStatePath), beforeState);
assert.strictEqual(fs.readFileSync(value.legacyFile, 'utf8'), SKILL_CONTENT);
assert.strictEqual(fs.existsSync(value.canonicalStatePath), false);
assert.strictEqual(fs.existsSync(path.join(value.sourceRoot, '.opencode', 'dist')), false);
assertSourceUnchanged(value);
assertBothLocksReleased(value);
});
test('legacy repair reuses its opaque lease for canonical apply after an inert synthetic build', value => {
let builds = 0;
const result = repair(value, sourceRoot => {
builds++;
assert.strictEqual(sourceRoot, value.sourceRoot);
assertHeldAndIndependentWritersRefused(value);
const dist = path.join(sourceRoot, '.opencode', 'dist');
fs.mkdirSync(path.join(dist, 'plugins'), { recursive: true });
fs.mkdirSync(path.join(dist, 'tools'), { recursive: true });
fs.writeFileSync(path.join(dist, 'index.js'), 'module.exports = {};\n');
});
assert.strictEqual(builds, 1);
assert.strictEqual(result.summary.errorCount, 0, JSON.stringify(result));
assert.strictEqual(result.results.length, 1, JSON.stringify(result));
assert.strictEqual(result.results[0].status, 'repaired');
assert.strictEqual(result.results[0].stateRefreshed, true);
assert.strictEqual(result.results[0].installStatePath, value.canonicalStatePath);
const state = readInstallState(value.canonicalStatePath);
assert.strictEqual(state.target.root, value.canonicalRoot);
assert.deepStrictEqual(state.resolution.selectedModules, ['workflow-quality']);
assert.notStrictEqual(state.request.hookConsent, 'enabled');
assert.strictEqual(state.operations.length, 1);
assert.strictEqual(state.operations[0].contentSha256, sha256(SKILL_CONTENT));
assert.strictEqual(fs.readFileSync(path.join(value.canonicalRoot, SOURCE_RELATIVE_PATH), 'utf8'), SKILL_CONTENT);
assert.strictEqual(fs.existsSync(value.legacyStatePath), false);
assert.strictEqual(fs.existsSync(value.legacyFile), false);
assertSourceUnchanged(value);
assertBothLocksReleased(value);
});
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
return { passed, failed };
}
if (require.main === module) process.exitCode = runTests().failed ? 1 : 0;
module.exports = { runTests };
@@ -0,0 +1,445 @@
'use strict';
const assert = require('assert');
const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { applyInstallPlan } = require('../../scripts/lib/install/apply');
const { withHookConsent } = require('../../scripts/lib/install/hook-consent');
const { createInstallState, readInstallState, writeInstallState } = require('../../scripts/lib/install-state');
const { buildDoctorReport, repairInstalledStates } = require('../../scripts/lib/install-lifecycle');
const REPO_ROOT = path.resolve(__dirname, '../..');
const sha256 = value => crypto.createHash('sha256').update(value).digest('hex');
// Authentic public ECC source fixtures, kept as inert bytes (never imported).
// Copying these bytes to an alias does not claim they were published build output.
const legacyPluginFixtures = [
{
name: "2.2.1 barrel",
// https://github.com/affaan-m/ECC/blob/ca185ef5f7667078a1e70a763bd3a9c71c48acf0/.opencode/plugins/index.ts
sha256: '965c5fac76ce0c3ceb3836814f5eb9ede8c9db50373a508c734f949cb321a21a',
content: `/**
* ECC Plugins for OpenCode
*
* This module exports all ECC plugins for OpenCode integration.
* Plugins provide hook-based automation that mirrors Claude Code's hook system
* while taking advantage of OpenCode's more sophisticated 20+ event types.
*/
export { ECCHooksPlugin, default } from "./ecc-hooks.js"
// Re-export for named imports
export * from "./ecc-hooks.js"
`,
},
{
name: "early barrel",
// https://github.com/affaan-m/ECC/blob/6d440c036df2c1b2fec957627d1202c3708e0627/.opencode/plugins/index.ts
sha256: 'e42c733adb177f84cea813663aa34c7868dbaa98c96950d0ef91cd211b8aa169',
content: `/**
* Everything Claude Code (ECC) Plugins for OpenCode
*
* This module exports all ECC plugins for OpenCode integration.
* Plugins provide hook-based automation that mirrors Claude Code's hook system
* while taking advantage of OpenCode's more sophisticated 20+ event types.
*/
export { ECCHooksPlugin, default } from "./ecc-hooks"
// Re-export for named imports
export * from "./ecc-hooks"
`,
},
];
function fixture(callback, enabled = false) {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-opencode-write-')));
const homeDir = path.join(root, 'home');
const sourceRoot = path.join(root, 'source');
const targetRoot = path.join(homeDir, '.config', 'opencode');
const adapter = { id: 'opencode-home', target: 'opencode', kind: 'home' };
try {
fs.mkdirSync(path.join(sourceRoot, 'manifests'), { recursive: true });
for (const name of ['install-modules.json', 'install-components.json', 'install-profiles.json']) {
fs.copyFileSync(path.join(REPO_ROOT, 'manifests', name), path.join(sourceRoot, 'manifests', name));
}
fs.copyFileSync(path.join(REPO_ROOT, 'package.json'), path.join(sourceRoot, 'package.json'));
const operations = ['opencode.json', 'plugins/ecc-hooks.ts'].map(relativePath => {
const sourceRelativePath = `.opencode/${relativePath}`;
const sourcePath = path.join(sourceRoot, sourceRelativePath);
const destinationPath = path.join(targetRoot, relativePath);
const content = relativePath === 'opencode.json'
? '{"plugin":["./plugins"],"userSetting":true}\n'
: 'export default async () => ({ "session.created": () => {} });\n';
fs.mkdirSync(path.dirname(sourcePath), { recursive: true });
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(sourcePath, content);
fs.writeFileSync(destinationPath, content);
return { kind: 'copy-file', moduleId: 'platform-configs', sourceRelativePath, sourcePath,
destinationPath, ownership: 'managed', scaffoldOnly: false,
strategy: 'preserve-relative-path', contentSha256: sha256(content) };
});
const dist = path.join(sourceRoot, '.opencode', 'dist');
fs.mkdirSync(path.join(dist, 'plugins'), { recursive: true });
fs.mkdirSync(path.join(dist, 'tools'), { recursive: true });
fs.writeFileSync(path.join(dist, 'index.js'), 'module.exports = {};\n');
const installStatePath = path.join(targetRoot, 'ecc-install-state.json');
const state = createInstallState({
adapter, targetRoot, installStatePath,
request: { modules: ['platform-configs'], legacyMode: true,
hookConsent: enabled ? 'enabled' : null },
resolution: { selectedModules: enabled ? ['platform-configs', 'hooks-runtime'] : ['platform-configs'],
skippedModules: [] },
operations, source: { repoVersion: '2.2.2', manifestVersion: 1 },
});
writeInstallState(installStatePath, state);
const basePlan = { target: 'opencode', adapter, homeDir, sourceRoot, targetRoot,
installRoot: targetRoot, installStatePath, operations, warnings: [],
selectedModuleIds: state.resolution.selectedModules, statePreview: state };
callback({ root, homeDir, sourceRoot, targetRoot, installStatePath, state, basePlan,
declinePlan: withHookConsent(basePlan, 'declined') });
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
}
function repair(value, extra = {}) {
return repairInstalledStates({ repoRoot: value.sourceRoot, homeDir: value.homeDir,
projectRoot: value.homeDir, targets: ['opencode'],
buildOpencodePayload() { throw new Error('Unexpected compiler execution'); }, ...extra });
}
function withWritableOpenMutation(filePath, mutate, callback) {
const originalOpen = fs.openSync;
const originalClose = fs.closeSync;
let injected = false;
const descriptors = new Set();
fs.openSync = function (candidate, flags, ...rest) {
const writable = typeof flags === 'number'
? Boolean(flags & (fs.constants.O_WRONLY | fs.constants.O_RDWR))
: /[wa+]/.test(flags);
const matches = typeof candidate === 'string' && path.resolve(candidate) === path.resolve(filePath);
if (matches && writable && !injected) {
injected = true;
mutate();
}
const fd = originalOpen.call(fs, candidate, flags, ...rest);
if (matches && writable) descriptors.add(fd);
return fd;
};
fs.closeSync = function (fd) {
descriptors.delete(fd);
return originalClose.call(fs, fd);
};
try {
callback();
assert.ok(injected, 'The destination writable-open boundary must be exercised');
assert.strictEqual(descriptors.size, 0, 'Every owned writable descriptor must close');
} finally {
fs.openSync = originalOpen;
fs.closeSync = originalClose;
}
}
function assertPriorOwnership(value, filePath) {
const after = readInstallState(value.installStatePath);
const prior = value.state.operations.find(operation => operation.destinationPath === filePath);
const current = after.operations.find(operation => operation.destinationPath === filePath);
assert.strictEqual(current.contentSha256, prior.contentSha256, 'Do not adopt raced bytes');
assert.strictEqual(after.request.hookConsent, value.state.request.hookConsent);
}
function runTests() {
let passed = 0;
let failed = 0;
const test = (name, callback) => {
try { callback(); passed++; console.log(` PASS ${name}`); }
catch (error) { failed++; console.error(` FAIL ${name}: ${error.stack}`); }
};
for (const relativePath of ['plugins/ecc-hooks.ts', 'opencode.json']) {
for (const mode of ['apply', 'repair']) {
test(`${mode} preserves a same-inode ${relativePath} edit at writable open`, () => fixture(value => {
const destination = path.join(value.targetRoot, relativePath);
const content = relativePath === 'opencode.json'
? '{"plugin":["./plugins"],"userEdit":"keep"}\n' : '// user edit: preserve this\n';
withWritableOpenMutation(destination, () => fs.writeFileSync(destination, content), () => {
if (mode === 'apply') {
assert.throws(() => applyInstallPlan(value.declinePlan), /changed after preflight/i);
} else {
const result = repair(value).results[0];
assert.strictEqual(result.status, 'error');
assert.match(result.error, /changed after preflight/i);
assert.notStrictEqual(result.stateRefreshed, true);
}
assert.strictEqual(fs.readFileSync(destination, 'utf8'), content);
assertPriorOwnership(value, destination);
});
}, mode === 'apply'));
}
}
for (const mode of ['apply', 'repair']) {
test(`${mode} preserves a file created after expected absence`, () => fixture(value => {
const destination = path.join(value.targetRoot, 'plugins/ecc-hooks.ts');
fs.unlinkSync(destination);
const content = '// newly created user file\n';
withWritableOpenMutation(destination, () => fs.writeFileSync(destination, content), () => {
if (mode === 'apply') assert.throws(() => applyInstallPlan(value.declinePlan), /EEXIST|changed after preflight/i);
else assert.strictEqual(repair(value).results[0].status, 'error');
assert.strictEqual(fs.readFileSync(destination, 'utf8'), content);
assertPriorOwnership(value, destination);
});
}, mode === 'apply'));
test(`${mode} does not recreate an expected existing file removed at open`, () => fixture(value => {
const destination = path.join(value.targetRoot, 'opencode.json');
withWritableOpenMutation(destination, () => fs.unlinkSync(destination), () => {
if (mode === 'apply') assert.throws(() => applyInstallPlan(value.declinePlan), /ENOENT|changed after preflight/i);
else assert.strictEqual(repair(value).results[0].status, 'error');
assert.strictEqual(fs.existsSync(destination), false);
assertPriorOwnership(value, destination);
});
}, mode === 'apply'));
}
for (const mode of ['apply', 'doctor', 'repair']) {
test(`${mode} reports malformed activation config with source context`, () => fixture(value => {
const destination = path.join(value.targetRoot, 'opencode.json');
fs.writeFileSync(destination, '{ malformed');
const before = fs.readFileSync(value.installStatePath);
if (mode === 'apply') assert.throws(() => applyInstallPlan(value.declinePlan), /Failed to parse .*opencode\.json/);
else if (mode === 'repair') assert.match(repair(value).results[0].error, /Failed to parse .*opencode\.json/);
else {
const result = buildDoctorReport({ repoRoot: value.sourceRoot, homeDir: value.homeDir,
projectRoot: value.homeDir, targets: ['opencode'] }).results[0];
assert.match(JSON.stringify(result.issues), /Failed to parse .*opencode\.json/);
}
assert.strictEqual(fs.readFileSync(destination, 'utf8'), '{ malformed');
assert.deepStrictEqual(fs.readFileSync(value.installStatePath), before);
}));
}
test('apply rejects another enabled writer and repair while holding its target lease', () => fixture(value => {
let checked = false;
applyInstallPlan(value.declinePlan, {
beforeOperationWrite() {
if (checked) return;
checked = true;
assert.throws(() => applyInstallPlan(withHookConsent(value.basePlan, 'enabled')), /Another ECC process|OpenCode.*lock/i);
assert.strictEqual(repair(value).results[0].status, 'error');
},
});
assert.ok(checked);
assert.strictEqual(fs.existsSync(`${value.installStatePath}.ecc.lock`), false);
assert.strictEqual(applyInstallPlan(value.declinePlan).applied, true);
}));
test('repair preserves ownership for a destination-classified activation with a recorded transform', () => fixture(value => {
const operation = value.state.operations.find(entry => entry.sourceRelativePath.endsWith('ecc-hooks.ts'));
const oldDestination = operation.destinationPath;
operation.sourceRelativePath = '.opencode/tools/fixture.js';
operation.contentTransform = 'opencode-disable-plugin-entrypoint';
operation.destinationPath = path.join(value.targetRoot, 'plugins', 'custom.js');
const source = path.join(value.sourceRoot, operation.sourceRelativePath);
fs.mkdirSync(path.dirname(source), { recursive: true });
fs.copyFileSync(oldDestination, source);
fs.unlinkSync(oldDestination);
writeInstallState(value.installStatePath, value.state);
const content = '// preserve unowned new custom plugin\n';
withWritableOpenMutation(operation.destinationPath, () => fs.writeFileSync(operation.destinationPath, content), () => {
const result = repair(value).results[0];
assert.strictEqual(result.status, 'error');
assert.match(result.error, /changed after preflight/i);
assert.strictEqual(fs.readFileSync(operation.destinationPath, 'utf8'), content);
assertPriorOwnership(value, operation.destinationPath);
});
}));
test('repair completes historical deactivation with exact inert bytes and releases its lock', () => fixture(value => {
const result = repair(value).results[0];
assert.strictEqual(result.status, 'repaired', result.error);
assert.strictEqual(result.stateRefreshed, true);
assert.strictEqual(fs.readFileSync(path.join(value.targetRoot, 'plugins/ecc-hooks.ts'), 'utf8'),
'export default async () => ({});\n');
assert.deepStrictEqual(JSON.parse(fs.readFileSync(path.join(value.targetRoot, 'opencode.json'))),
{ plugin: [], userSetting: true });
assert.strictEqual(fs.existsSync(`${value.installStatePath}.ecc.lock`), false);
}));
for (const consent of [null, 'declined']) {
test(`fresh ${consent || 'default'} apply preserves unrelated unrecorded plugin aliases`, () => fixture(value => {
fs.unlinkSync(value.installStatePath);
for (const operation of value.basePlan.operations) fs.unlinkSync(operation.destinationPath);
const aliases = ['index.ts', 'index.js', 'index.mjs', 'index.cjs', 'ecc-hooks.js'];
const content = 'export default async () => ({ "user.plugin": () => {} });\n';
for (const alias of aliases) fs.writeFileSync(path.join(value.targetRoot, 'plugins', alias), content);
const result = applyInstallPlan(withHookConsent(value.basePlan, consent));
assert.strictEqual(result.applied, true);
const state = readInstallState(value.installStatePath);
for (const alias of aliases) {
const destination = path.join(value.targetRoot, 'plugins', alias);
assert.strictEqual(fs.readFileSync(destination, 'utf8'), content);
assert.ok(!state.operations.some(operation => operation.destinationPath === destination), 'Do not adopt a user plugin');
}
}));
}
for (const mode of ['doctor', 'repair']) {
test(`${mode} preserves an unrelated plugin without reporting ECC activation`, () => fixture(value => {
applyInstallPlan(value.declinePlan);
const destination = path.join(value.targetRoot, 'plugins', 'index.js');
const content = 'export default async () => ({ "user.plugin": () => {} });\n';
fs.writeFileSync(destination, content);
const before = fs.readFileSync(value.installStatePath);
if (mode === 'doctor') {
const result = buildDoctorReport({ repoRoot: value.sourceRoot, homeDir: value.homeDir,
projectRoot: value.homeDir, targets: ['opencode'] }).results[0];
assert.ok(!result.issues.some(issue => issue.code === 'opencode-hook-consent-violation'), JSON.stringify(result.issues));
} else {
const result = repair(value).results[0];
assert.notStrictEqual(result.status, 'error', result.error);
}
assert.strictEqual(fs.readFileSync(destination, 'utf8'), content);
const { lastValidatedAt: _beforeValidation, ...priorState } = JSON.parse(before);
const { lastValidatedAt: _afterValidation, ...afterState } = readInstallState(value.installStatePath);
assert.deepStrictEqual(afterState, priorState, 'Only the legitimate validation timestamp may change');
assert.ok(!afterState.operations.some(operation => operation.destinationPath === destination), 'Do not adopt a user plugin');
}));
}
for (const legacy of legacyPluginFixtures) {
for (const consent of [null, 'declined']) {
test(`fresh ${consent || 'default'} apply refuses the unrecorded historical ${legacy.name}`, () => fixture(value => {
assert.strictEqual(sha256(legacy.content), legacy.sha256, 'Preserve exact public-source fixture bytes');
assert.notStrictEqual(sha256(fs.readFileSync(path.join(value.sourceRoot, '.opencode/plugins/ecc-hooks.ts'))), legacy.sha256);
assert.notStrictEqual(sha256(fs.readFileSync(path.join(REPO_ROOT, '.opencode/plugins/index.ts'))), legacy.sha256);
fs.unlinkSync(value.installStatePath);
for (const operation of value.basePlan.operations) fs.unlinkSync(operation.destinationPath);
const alias = path.join(value.targetRoot, 'plugins', 'index.js');
assert.ok(!value.basePlan.operations.some(operation => operation.destinationPath === alias));
fs.writeFileSync(alias, legacy.content);
assert.throws(() => applyInstallPlan(withHookConsent(value.basePlan, consent)), /Refusing OpenCode hook deactivation/);
assert.strictEqual(fs.readFileSync(alias, 'utf8'), legacy.content);
assert.strictEqual(fs.existsSync(value.installStatePath), false, 'Do not adopt an unrecorded historical alias');
for (const operation of value.basePlan.operations) assert.strictEqual(fs.existsSync(operation.destinationPath), false);
assert.strictEqual(fs.existsSync(`${value.installStatePath}.ecc.lock`), false);
}));
}
for (const mode of ['doctor', 'repair']) {
test(`${mode} refuses the unrecorded historical ${legacy.name} without changing ownership`, () => fixture(value => {
applyInstallPlan(value.declinePlan);
const alias = path.join(value.targetRoot, 'plugins', 'index.js');
fs.writeFileSync(alias, legacy.content);
const before = fs.readFileSync(value.installStatePath);
const operationsBefore = value.basePlan.operations.map(operation => fs.readFileSync(operation.destinationPath));
assert.ok(!readInstallState(value.installStatePath).operations.some(operation => operation.destinationPath === alias));
if (mode === 'doctor') {
const result = buildDoctorReport({ repoRoot: value.sourceRoot, homeDir: value.homeDir,
projectRoot: value.homeDir, targets: ['opencode'] }).results[0];
const issue = result.issues.find(entry => entry.code === 'opencode-hook-consent-violation');
assert.ok(issue, JSON.stringify(result.issues));
assert.match(issue.message, /OpenCode hook activation remains active/);
} else {
const result = repair(value).results[0];
assert.strictEqual(result.status, 'error');
assert.match(result.error, /Refusing OpenCode hook deactivation/);
assert.notStrictEqual(result.stateRefreshed, true);
}
assert.strictEqual(fs.readFileSync(alias, 'utf8'), legacy.content);
assert.deepStrictEqual(fs.readFileSync(value.installStatePath), before);
assert.deepStrictEqual(value.basePlan.operations.map(operation => fs.readFileSync(operation.destinationPath)), operationsBefore);
assert.strictEqual(fs.existsSync(`${value.installStatePath}.ecc.lock`), false);
}));
}
}
for (const mode of ['apply', 'repair']) {
test(`${mode} refuses a historical alias inserted after preflight before state refresh`, () => fixture(value => {
const destination = path.join(value.targetRoot, 'plugins', 'ecc-hooks.ts');
const alias = path.join(value.targetRoot, 'plugins', 'index.js');
const content = legacyPluginFixtures[0].content;
withWritableOpenMutation(destination, () => fs.writeFileSync(alias, content), () => {
if (mode === 'apply') assert.throws(() => applyInstallPlan(value.declinePlan), /OpenCode hook activation remains active/);
else {
const result = repair(value).results[0];
assert.strictEqual(result.status, 'error');
assert.match(result.error, /OpenCode hook activation remains active/);
assert.notStrictEqual(result.stateRefreshed, true);
}
assert.strictEqual(fs.readFileSync(alias, 'utf8'), content);
const state = readInstallState(value.installStatePath);
assert.ok(!state.operations.some(operation => operation.destinationPath === alias));
assert.strictEqual(state.request.hookConsent, value.state.request.hookConsent);
assert.strictEqual(fs.existsSync(`${value.installStatePath}.ecc.lock`), false);
});
}));
}
for (const artifact of ['source', 'build']) {
test(`unrecorded ${artifact}-identical ECC alias still fails closed`, () => fixture(value => {
applyInstallPlan(value.declinePlan);
const source = artifact === 'source'
? path.join(value.sourceRoot, '.opencode', 'plugins', 'ecc-hooks.ts')
: path.join(value.sourceRoot, '.opencode', 'dist', 'plugins', 'index.js');
if (artifact === 'build') fs.writeFileSync(source, 'module.exports = { eccHook: true };\n');
const content = fs.readFileSync(source);
const alias = path.join(value.targetRoot, 'plugins', 'index.js');
fs.writeFileSync(alias, content);
const before = fs.readFileSync(value.installStatePath);
assert.throws(() => applyInstallPlan(value.declinePlan), /OpenCode hook deactivation/);
const doctor = buildDoctorReport({ repoRoot: value.sourceRoot, homeDir: value.homeDir,
projectRoot: value.homeDir, targets: ['opencode'] }).results[0];
assert.ok(doctor.issues.some(issue => issue.code === 'opencode-hook-consent-violation'));
assert.strictEqual(repair(value).results[0].status, 'error');
assert.deepStrictEqual(fs.readFileSync(alias), content);
assert.deepStrictEqual(fs.readFileSync(value.installStatePath), before);
}));
}
test('an unrecorded collision at a planned ECC plugin destination still fails closed', () => fixture(value => {
fs.unlinkSync(value.installStatePath);
fs.unlinkSync(path.join(value.targetRoot, 'opencode.json'));
const destination = path.join(value.targetRoot, 'plugins', 'ecc-hooks.ts');
const content = '// user-owned file at an ECC destination\n';
fs.writeFileSync(destination, content);
assert.throws(() => applyInstallPlan(value.declinePlan), /user-owned|unverifiable/i);
assert.strictEqual(fs.readFileSync(destination, 'utf8'), content);
assert.strictEqual(fs.existsSync(value.installStatePath), false);
}));
for (const planned of [false, true]) {
test(`${planned ? 'planned ECC' : 'unrecorded user'} plugin read failures respect the attribution boundary`, () => fixture(value => {
const destination = path.join(value.targetRoot, 'plugins', planned ? 'ecc-hooks.ts' : 'index.js');
const content = planned ? fs.readFileSync(destination) : Buffer.from('// unreadable user plugin\n');
if (!planned) fs.writeFileSync(destination, content);
const originalOpen = fs.openSync;
let refusedReads = 0;
fs.openSync = function (candidate, ...args) {
if (typeof candidate === 'string' && path.resolve(candidate) === destination) {
refusedReads++;
throw Object.assign(new Error('Synthetic plugin read permission denied'), { code: 'EACCES' });
}
return originalOpen.call(fs, candidate, ...args);
};
try {
if (planned) assert.throws(() => applyInstallPlan(value.declinePlan), /permission denied/);
else assert.strictEqual(applyInstallPlan(value.declinePlan).applied, true);
assert.ok(refusedReads > 0, 'The permission boundary must be exercised');
} finally {
fs.openSync = originalOpen;
}
assert.deepStrictEqual(fs.readFileSync(destination), content);
if (!planned) assert.ok(!readInstallState(value.installStatePath).operations.some(operation => operation.destinationPath === destination));
}));
}
test('repair preserves the primary failure and replacement lock when release also fails', () => fixture(value => {
const destination = path.join(value.targetRoot, 'opencode.json');
const lock = `${value.installStatePath}.ecc.lock`;
withWritableOpenMutation(destination, () => {
fs.writeFileSync(destination, '{"userEdit":true}');
fs.renameSync(lock, `${lock}.owned`);
fs.writeFileSync(lock, 'replacement lock');
}, () => {
const result = repair(value).results[0];
assert.strictEqual(result.status, 'error');
assert.match(result.error, /changed after preflight/i);
assert.match(result.releaseError, /changed OpenCode install lock/);
assert.strictEqual(fs.readFileSync(lock, 'utf8'), 'replacement lock');
assertPriorOwnership(value, destination);
});
}));
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
return { passed, failed };
}
if (require.main === module) process.exitCode = runTests().failed ? 1 : 0;
module.exports = { runTests };
+424
View File
@@ -0,0 +1,424 @@
/** Cooperative OpenCode installation lock scopes, without child processes. */
'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { acquireOpenCodeInstallLocks, withOpenCodeInstallLocks } = require('../../scripts/lib/install/opencode-install-lock');
const { acquireSettingsLock, getSettingsLockIdentity, sameFileIdentity } = require('../../scripts/lib/install/claude-settings-lock');
let passed = 0;
let failed = 0;
const lockPath = root => path.join(root, 'ecc-install-state.json.ecc.lock');
function test(name, callback) {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-opencode-lock-')));
try { callback(root); passed++; console.log(` PASS ${name}`); }
catch (error) { failed++; console.error(` FAIL ${name}: ${error.stack}`); }
finally { fs.rmSync(root, { recursive: true, force: true }); }
}
test('same target refuses an independent writer and releases on success', root => {
assert.strictEqual(withOpenCodeInstallLocks([root], lease => {
assert.ok(Object.isFrozen(lease));
assert.ok(fs.existsSync(lockPath(root)));
assert.throws(() => acquireOpenCodeInstallLocks([root]), /Another ECC process.*OpenCode/);
return 'done';
}), 'done');
assert.strictEqual(fs.existsSync(lockPath(root)), false);
});
test('different roots can be held independently', root => {
const other = path.join(root, 'other');
withOpenCodeInstallLocks([root], () => withOpenCodeInstallLocks([other], () => {
assert.ok(fs.existsSync(lockPath(root)) && fs.existsSync(lockPath(other)));
}));
assert.strictEqual(fs.existsSync(lockPath(other)), false);
});
test('roots are deduplicated, sorted and released in reverse order', root => {
const a = path.join(root, 'a');
const b = path.join(root, 'b');
const linked = [];
const renamed = [];
const originalLink = fs.linkSync;
const originalRename = fs.renameSync;
fs.linkSync = (from, to) => { linked.push(to); return originalLink(from, to); };
fs.renameSync = (from, to) => { renamed.push(from); return originalRename(from, to); };
try { withOpenCodeInstallLocks([b, a, b], () => {}); }
finally { fs.linkSync = originalLink; fs.renameSync = originalRename; }
assert.deepStrictEqual(linked, [lockPath(a), lockPath(b)]);
assert.deepStrictEqual(renamed, [lockPath(b), lockPath(a)]);
});
test('nested use requires a real active lease with full coverage', root => {
const holder = acquireOpenCodeInstallLocks([root]);
const nested = acquireOpenCodeInstallLocks([root], holder.lease);
assert.strictEqual(nested.lease, holder.lease);
nested.release();
assert.ok(fs.existsSync(lockPath(root)));
assert.throws(() => acquireOpenCodeInstallLocks([path.join(root, 'missing')], holder.lease), /cover/);
assert.throws(() => acquireOpenCodeInstallLocks([root], true), /lease/);
assert.throws(() => acquireOpenCodeInstallLocks([root], {}), /lease/);
holder.release();
holder.release();
assert.throws(() => acquireOpenCodeInstallLocks([root], holder.lease), /lease/);
});
test('callback error retains identity and releases', root => {
const primary = new Error('callback failure');
assert.throws(() => withOpenCodeInstallLocks([root], () => { throw primary; }), error => error === primary);
assert.strictEqual(fs.existsSync(lockPath(root)), false);
});
test('second lock failure releases the first without disturbing the other owner', root => {
const a = path.join(root, 'a');
const b = path.join(root, 'b');
const other = acquireOpenCodeInstallLocks([b]);
const bytes = fs.readFileSync(lockPath(b));
try { assert.throws(() => acquireOpenCodeInstallLocks([b, a]), /Another ECC process/); }
finally { assert.deepStrictEqual(fs.readFileSync(lockPath(b)), bytes); other.release(); }
assert.strictEqual(fs.existsSync(lockPath(a)), false);
});
test('a replaced lock invalidates nested reuse and is preserved on release', root => {
const holder = acquireOpenCodeInstallLocks([root]);
fs.renameSync(lockPath(root), `${lockPath(root)}.owned`);
fs.writeFileSync(lockPath(root), 'replacement');
assert.throws(() => acquireOpenCodeInstallLocks([root], holder.lease), /changed/);
assert.throws(() => holder.release(), /changed/);
assert.strictEqual(fs.readFileSync(lockPath(root), 'utf8'), 'replacement');
});
test('release failure is attached without hiding the callback failure', root => {
const primary = new Error('primary');
assert.throws(() => withOpenCodeInstallLocks([root], () => {
fs.renameSync(lockPath(root), `${lockPath(root)}.owned`);
fs.writeFileSync(lockPath(root), 'replacement');
throw primary;
}), error => error === primary && /changed/.test(error.releaseError.message));
assert.strictEqual(fs.readFileSync(lockPath(root), 'utf8'), 'replacement');
});
test('invalid root and non-file lock paths refuse before callback', root => {
for (const roots of [null, ['relative'], [true], ['']]) {
assert.throws(() => acquireOpenCodeInstallLocks(roots), /root/);
}
fs.mkdirSync(lockPath(root));
assert.throws(() => withOpenCodeInstallLocks([root], () => assert.fail('must refuse')), /lock/);
});
test('lock engine still recovers a stale malformed lock', root => {
fs.writeFileSync(lockPath(root), 'stale');
const old = new Date(Date.now() - 600000);
fs.utimesSync(lockPath(root), old, old);
withOpenCodeInstallLocks([root], () => assert.match(fs.readFileSync(lockPath(root), 'utf8'), /"pid"/));
assert.strictEqual(fs.existsSync(lockPath(root)), false);
});
test('a root or lock reported as a symlink refuses without callback or outside access', root => {
for (const target of [root, lockPath(root)]) {
if (target !== root) fs.writeFileSync(target, 'unrelated target');
const original = fs.lstatSync;
fs.lstatSync = (...args) => {
const stats = original(...args);
if (args[0] === target) {
const symlink = Object.create(stats);
symlink.isSymbolicLink = () => true;
return symlink;
}
return stats;
};
try {
assert.throws(() => withOpenCodeInstallLocks([root], () => assert.fail('must refuse')), /symlink/);
} finally { fs.lstatSync = original; }
if (target !== root) assert.strictEqual(fs.readFileSync(target, 'utf8'), 'unrelated target');
}
});
test('permission errors remain permission errors', root => {
const original = fs.lstatSync;
const denied = Object.assign(new Error('permission denied'), { code: 'EACCES' });
fs.lstatSync = (...args) => { if (args[0] === lockPath(root)) throw denied; return original(...args); };
try { assert.throws(() => acquireOpenCodeInstallLocks([root]), error => error === denied); }
finally { fs.lstatSync = original; }
assert.strictEqual(fs.existsSync(lockPath(root)), false);
});
test('releasing all roots continues after one replaced lock and rejects later lease reuse', root => {
const a = path.join(root, 'a');
const b = path.join(root, 'b');
const holder = acquireOpenCodeInstallLocks([a, b]);
fs.renameSync(lockPath(b), `${lockPath(b)}.owned`);
fs.writeFileSync(lockPath(b), 'replacement');
assert.throws(() => holder.release(), /changed/);
assert.strictEqual(fs.existsSync(lockPath(a)), false);
assert.strictEqual(fs.readFileSync(lockPath(b), 'utf8'), 'replacement');
assert.throws(() => acquireOpenCodeInstallLocks([a], holder.lease), /lease/);
});
test('a root replaced during a held scope is preserved while other roots release and the lease expires', root => {
const a = path.join(root, 'a');
const b = path.join(root, 'b');
const movedRoot = path.join(root, 'b-original');
const replacementState = path.join(b, 'ecc-install-state.json');
let heldLease;
let originalLockBytes;
assert.throws(() => withOpenCodeInstallLocks([a, b], lease => {
heldLease = lease;
originalLockBytes = fs.readFileSync(lockPath(b));
fs.renameSync(b, movedRoot);
fs.mkdirSync(b);
fs.writeFileSync(lockPath(b), 'replacement root lock');
fs.writeFileSync(replacementState, 'replacement root state');
return 'release must refuse instead of returning this result';
}), /Refusing changed OpenCode install lock root/);
assert.strictEqual(fs.existsSync(lockPath(a)), false);
assert.strictEqual(fs.readFileSync(lockPath(b), 'utf8'), 'replacement root lock');
assert.strictEqual(fs.readFileSync(replacementState, 'utf8'), 'replacement root state');
assert.deepStrictEqual(fs.readdirSync(b).sort(), ['ecc-install-state.json', 'ecc-install-state.json.ecc.lock']);
assert.deepStrictEqual(fs.readFileSync(lockPath(movedRoot)), originalLockBytes);
assert.throws(() => acquireOpenCodeInstallLocks([a, b], heldLease), /Invalid or inactive OpenCode install lease/);
withOpenCodeInstallLocks([a], () => assert.ok(fs.existsSync(lockPath(a))));
assert.strictEqual(fs.existsSync(lockPath(a)), false);
});
test('an absent callback is rejected before a lock is created', root => {
assert.throws(() => withOpenCodeInstallLocks([root], null), /callback/);
assert.strictEqual(fs.existsSync(lockPath(root)), false);
});
test('a replacement immediately after lock publication cannot enter the protected callback', root => {
const target = lockPath(root);
const originalLink = fs.linkSync;
let callbacks = 0;
let replacements = 0;
fs.linkSync = (from, to) => {
originalLink(from, to);
if (to === target) {
replacements++;
fs.renameSync(target, `${target}.owned`);
fs.writeFileSync(target, 'replacement at acquisition boundary');
}
};
try {
assert.throws(() => withOpenCodeInstallLocks([root], () => { callbacks++; }), /changed .*lock/);
} finally { fs.linkSync = originalLink; }
assert.strictEqual(replacements, 1);
assert.strictEqual(callbacks, 0, 'A pathname replacement must never become the acquired identity');
assert.strictEqual(fs.readFileSync(target, 'utf8'), 'replacement at acquisition boundary');
assert.strictEqual(JSON.parse(fs.readFileSync(`${target}.owned`, 'utf8')).pid, process.pid);
});
test('a transient post-acquisition validation failure releases authentic locks and preserves the primary error', root => {
const a = path.join(root, 'a');
const b = path.join(root, 'b');
const primary = Object.assign(new Error('one-shot validation error'), { code: 'EIO' });
const originalLink = fs.linkSync;
const originalStat = fs.lstatSync;
let published = false;
let faults = 0;
let callbacks = 0;
let caught;
fs.linkSync = (from, to) => {
originalLink(from, to);
if (to === lockPath(b)) published = true;
};
fs.lstatSync = (...args) => {
if (published && args[0] === b && faults === 0) { faults++; throw primary; }
return originalStat(...args);
};
try {
try { withOpenCodeInstallLocks([a, b], () => { callbacks++; }); }
catch (error) { caught = error; }
} finally { fs.linkSync = originalLink; fs.lstatSync = originalStat; }
assert.strictEqual(faults, 1);
assert.strictEqual(callbacks, 0);
assert.strictEqual(caught, primary);
assert.strictEqual(caught.releaseError, undefined, 'Cleanup must not dereference a missing identity');
assert.strictEqual(fs.existsSync(lockPath(a)), false);
assert.strictEqual(fs.existsSync(lockPath(b)), false);
});
test('engine ownership metadata is immutable and derived from the acquired descriptor', root => {
const settingsPath = path.join(root, 'settings.json');
const release = acquireSettingsLock(settingsPath);
try {
const identity = getSettingsLockIdentity(release);
assert.ok(Object.isFrozen(identity));
assert.deepStrictEqual(Object.keys(identity).sort(), ['dev', 'ino']);
assert.ok(sameFileIdentity(identity, fs.lstatSync(`${settingsPath}.ecc.lock`, { bigint: true })));
assert.throws(() => { identity.ino = 0n; }, TypeError);
assert.throws(() => getSettingsLockIdentity(() => {}), /identity/);
} finally { release(); }
assert.strictEqual(fs.existsSync(`${settingsPath}.ecc.lock`), false);
});
function captureThrown(callback) {
try { return { didThrow: false, result: callback() }; }
catch (value) { return { didThrow: true, value }; }
}
const falsyThrownValues = [undefined, null, false, 0, '', NaN];
for (const [index, primary] of falsyThrownValues.entries()) {
for (const cleanupFails of [false, true]) {
test(`falsy callback value ${index} survives ${cleanupFails ? 'failed' : 'healthy'} cleanup`, root => {
let lease;
const caught = captureThrown(() => withOpenCodeInstallLocks([root], active => {
lease = active;
if (cleanupFails) {
fs.renameSync(lockPath(root), `${lockPath(root)}.owned`);
fs.writeFileSync(lockPath(root), 'replacement');
}
throw primary;
}));
assert.strictEqual(caught.didThrow, true, 'A thrown falsy value must not become success');
assert.ok(Object.is(caught.value, primary), 'Preserve the exact thrown value, including NaN');
assert.throws(() => acquireOpenCodeInstallLocks([root], lease), /inactive/);
if (cleanupFails) {
assert.strictEqual(fs.readFileSync(lockPath(root), 'utf8'), 'replacement');
assert.ok(fs.existsSync(`${lockPath(root)}.owned`));
} else assert.strictEqual(fs.existsSync(lockPath(root)), false);
});
}
}
for (const kind of ['frozen', 'nonextensible', 'nonwritable', 'accessor', 'proxy', 'primitive']) {
test(`${kind} callback primary survives cleanup failure without invoking accessors`, root => {
const a = path.join(root, 'a');
const b = path.join(root, 'b');
const marker = new Error('preexisting release diagnostic');
let accesses = 0;
let definitions = 0;
let primary = new Error('primary');
if (kind === 'frozen') Object.freeze(primary);
if (kind === 'nonextensible') Object.preventExtensions(primary);
if (kind === 'nonwritable') Object.defineProperty(primary, 'releaseError', { value: marker });
if (kind === 'accessor') Object.defineProperty(primary, 'releaseError', {
configurable: true,
get() { accesses++; throw new Error('getter must not run'); },
set() { accesses++; throw new Error('setter must not run'); }
});
if (kind === 'proxy') primary = new Proxy(primary, { defineProperty() {
definitions++;
assert.strictEqual(fs.existsSync(lockPath(a)), false, 'All safe cleanup precedes annotation');
throw new Error('annotation rejected');
} });
if (kind === 'primitive') primary = 'literal primary';
let lease;
const caught = captureThrown(() => withOpenCodeInstallLocks([a, b], active => {
lease = active;
fs.renameSync(lockPath(b), `${lockPath(b)}.owned`);
fs.writeFileSync(lockPath(b), 'replacement');
throw primary;
}));
assert.strictEqual(caught.didThrow, true);
assert.ok(Object.is(caught.value, primary));
assert.strictEqual(accesses, 0);
if (kind === 'proxy') assert.strictEqual(definitions, 1);
if (kind === 'nonwritable') assert.strictEqual(primary.releaseError, marker);
if (kind === 'accessor') {
const descriptor = Object.getOwnPropertyDescriptor(primary, 'releaseError');
assert.ok('value' in descriptor, 'Diagnostic should be an own data property');
assert.match(descriptor.value.message, /changed/);
}
assert.strictEqual(fs.existsSync(lockPath(a)), false);
assert.strictEqual(fs.readFileSync(lockPath(b), 'utf8'), 'replacement');
assert.ok(fs.existsSync(`${lockPath(b)}.owned`));
assert.throws(() => acquireOpenCodeInstallLocks([a], lease), /inactive/);
});
}
for (const kind of ['mutable', 'frozen', 'frozen array', 'non-array', 'readonly', 'accessor', 'proxy']) {
test(`three-root cleanup preserves ${kind} first failure and finishes reverse cleanup`, root => {
const roots = ['a', 'b', 'c'].map(name => path.join(root, name));
const [a, b, c] = roots;
const originalRename = fs.renameSync;
const secondary = new Error('second cleanup failure');
const priorDiagnostic = new Error('prior diagnostic');
const originalArray = Object.freeze([priorDiagnostic]);
let accesses = 0;
let primary = new Error('first cleanup failure');
if (kind === 'frozen') Object.freeze(primary);
if (kind === 'frozen array') primary.releaseErrors = originalArray;
if (kind === 'non-array') primary.releaseErrors = { push() { accesses++; throw new Error('caller push must not run'); } };
if (kind === 'readonly') Object.defineProperty(primary, 'releaseErrors', { value: originalArray });
if (kind === 'accessor') Object.defineProperty(primary, 'releaseErrors', {
get() { accesses++; throw new Error('caller getter must not run'); },
set() { accesses++; throw new Error('caller setter must not run'); }
});
if (kind === 'proxy') primary = new Proxy(primary, { defineProperty() {
assert.strictEqual(fs.existsSync(lockPath(a)), false, 'Finish cleanup before a diagnostic trap');
throw new Error('diagnostic trap');
} });
const holder = acquireOpenCodeInstallLocks(roots);
const attempts = [];
fs.renameSync = (from, to) => {
if (roots.some(candidate => lockPath(candidate) === from)) attempts.push(from);
if (from === lockPath(c)) throw primary;
if (from === lockPath(b)) throw secondary;
return originalRename(from, to);
};
let caught;
try { caught = captureThrown(() => holder.release()); }
finally { fs.renameSync = originalRename; }
assert.strictEqual(caught.didThrow, true);
assert.strictEqual(caught.value, primary);
assert.deepStrictEqual(attempts, [lockPath(c), lockPath(b), lockPath(a)]);
assert.strictEqual(fs.existsSync(lockPath(a)), false);
assert.ok(fs.existsSync(lockPath(b)) && fs.existsSync(lockPath(c)), 'Failed releases must not be claimed removed');
assert.strictEqual(accesses, 0);
assert.deepStrictEqual(originalArray, [priorDiagnostic], 'Never mutate a caller-owned diagnostics array');
if (['mutable', 'frozen array', 'non-array'].includes(kind)) {
assert.deepStrictEqual(primary.releaseErrors, [secondary]);
assert.notStrictEqual(primary.releaseErrors, originalArray);
}
if (kind === 'readonly') assert.strictEqual(primary.releaseErrors, originalArray);
assert.throws(() => acquireOpenCodeInstallLocks([a], holder.lease), /inactive/);
});
}
for (const [index, primary] of falsyThrownValues.entries()) {
test(`falsy first cleanup value ${index} is thrown after all remaining roots are attempted`, root => {
const roots = ['a', 'b', 'c'].map(name => path.join(root, name));
const [a, b, c] = roots;
const originalRename = fs.renameSync;
const secondary = new Error('second cleanup failure');
const attempts = [];
let lease;
let caught;
fs.renameSync = (from, to) => {
if (roots.some(candidate => lockPath(candidate) === from)) attempts.push(from);
if (from === lockPath(c)) throw primary;
if (from === lockPath(b)) throw secondary;
return originalRename(from, to);
};
try { caught = captureThrown(() => withOpenCodeInstallLocks(roots, active => { lease = active; return 'success'; })); }
finally { fs.renameSync = originalRename; }
assert.strictEqual(caught.didThrow, true);
assert.ok(Object.is(caught.value, primary));
assert.deepStrictEqual(attempts, [lockPath(c), lockPath(b), lockPath(a)]);
assert.strictEqual(fs.existsSync(lockPath(a)), false);
assert.ok(fs.existsSync(lockPath(b)) && fs.existsSync(lockPath(c)));
assert.throws(() => acquireOpenCodeInstallLocks([a], lease), /inactive/);
});
}
test('frozen acquisition failure survives rollback while replaced ownership is preserved', root => {
const roots = ['a', 'b', 'c'].map(name => path.join(root, name));
const [a, b, c] = roots;
const primary = Object.freeze(new Error('one-shot acquisition validation failure'));
const originalLink = fs.linkSync;
const originalStat = fs.lstatSync;
const originalRename = fs.renameSync;
let published = false;
let faults = 0;
let callbacks = 0;
const checked = [];
fs.linkSync = (from, to) => { originalLink(from, to); if (to === lockPath(c)) published = true; };
fs.lstatSync = (...args) => {
if (published && args[0] === c && faults === 0) {
faults++;
originalRename(lockPath(b), `${lockPath(b)}.owned`);
fs.writeFileSync(lockPath(b), 'replacement during rollback');
throw primary;
}
if (faults > 0 && roots.includes(args[0]) && args[1]?.bigint) checked.push(args[0]);
return originalStat(...args);
};
let caught;
try { caught = captureThrown(() => withOpenCodeInstallLocks(roots, () => { callbacks++; })); }
finally { fs.linkSync = originalLink; fs.lstatSync = originalStat; }
assert.strictEqual(caught.didThrow, true);
assert.strictEqual(caught.value, primary);
assert.strictEqual(faults, 1);
assert.strictEqual(callbacks, 0);
assert.deepStrictEqual(checked, [c, b, a]);
assert.strictEqual(fs.existsSync(lockPath(a)), false);
assert.strictEqual(fs.existsSync(lockPath(c)), false);
assert.strictEqual(fs.readFileSync(lockPath(b), 'utf8'), 'replacement during rollback');
assert.ok(fs.existsSync(`${lockPath(b)}.owned`));
});
console.log(`Results: Passed: ${passed}, Failed: ${failed}`);
process.exitCode = failed ? 1 : 0;