mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-29 04:55:12 +02:00
fix(opencode): refuse known legacy hooks without consent
This commit is contained in:
@@ -334,9 +334,72 @@ function readOpenCodeAliasForAttribution(plan, destinationPath) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const digests = new Set();
|
||||
if (typeof plan.sourceRoot !== 'string' || !path.isAbsolute(plan.sourceRoot)) return digests;
|
||||
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']) {
|
||||
|
||||
@@ -13,6 +13,47 @@ const { buildDoctorReport, repairInstalledStates } = require('../../scripts/lib/
|
||||
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');
|
||||
@@ -259,6 +300,72 @@ function runTests() {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user