mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-28 20:45:11 +02:00
169 lines
8.9 KiB
JavaScript
169 lines
8.9 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
// Opt-in, credential-free native discovery. Never starts a thread or model turn.
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { spawn, spawnSync } = require('node:child_process');
|
|
|
|
function run(command, args, options) {
|
|
const result = spawnSync(command, args, { ...options, encoding: 'utf8', timeout: 60000,
|
|
maxBuffer: 16 * 1024 * 1024 });
|
|
assert.equal(result.status, 0, `${command}: ${result.error || result.stderr || result.stdout}`);
|
|
return result.stdout.trim();
|
|
}
|
|
|
|
async function listSkills() {
|
|
const server = spawn(process.env.ECC_NATIVE_CODEX || 'codex', ['app-server', '--stdio'], {
|
|
cwd: process.cwd(), env: process.env, stdio: ['pipe', 'pipe', 'pipe'],
|
|
});
|
|
let buffer = '';
|
|
let stderr = '';
|
|
const pending = new Map();
|
|
let nextId = 0;
|
|
server.stderr.on('data', chunk => { stderr += chunk; });
|
|
server.stdout.on('data', chunk => {
|
|
buffer += chunk;
|
|
let end;
|
|
while ((end = buffer.indexOf('\n')) >= 0) {
|
|
const line = buffer.slice(0, end);
|
|
buffer = buffer.slice(end + 1);
|
|
if (!line.trim()) continue;
|
|
const message = JSON.parse(line);
|
|
const handler = pending.get(message.id);
|
|
if (handler) {
|
|
pending.delete(message.id);
|
|
if (message.error) handler.reject(new Error(JSON.stringify(message.error)));
|
|
else handler.resolve(message.result);
|
|
}
|
|
}
|
|
});
|
|
const fail = error => { for (const handler of pending.values()) handler.reject(error); };
|
|
server.on('error', fail);
|
|
server.on('exit', code => fail(new Error(`App server exited ${code}: ${stderr}`)));
|
|
const timer = setTimeout(() => { fail(new Error('Native discovery timed out')); server.kill(); }, 45000);
|
|
const request = (method, params) => new Promise((resolve, reject) => {
|
|
const id = ++nextId;
|
|
pending.set(id, { resolve, reject });
|
|
server.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
|
|
});
|
|
try {
|
|
const initialized = await request('initialize', {
|
|
clientInfo: { name: 'ecc-context-native-probe', version: '1.0.0' },
|
|
capabilities: { experimentalApi: true },
|
|
});
|
|
server.stdin.write(`${JSON.stringify({ method: 'initialized' })}\n`);
|
|
const skills = await request('skills/list', { cwds: [process.cwd()], forceReload: true });
|
|
process.stdout.write(`${JSON.stringify({ initialized, skills })}\n`);
|
|
} finally {
|
|
clearTimeout(timer);
|
|
server.kill();
|
|
}
|
|
}
|
|
|
|
function probe(options) {
|
|
const repoRoot = path.resolve(process.env.ECC_NATIVE_PACKAGE_ROOT || path.join(__dirname, '../..'));
|
|
const { planContextCarrier } = require(path.join(repoRoot, 'scripts/lib/context-carriers'));
|
|
const { compileContextProfile } = require(path.join(repoRoot, 'scripts/lib/context-profiles'));
|
|
// The independent structural oracle remains source-only test infrastructure.
|
|
const { withCarrierFixture } = require('../../tests/lib/helpers/context-carrier-fixture');
|
|
const artifact = planContextCarrier({ repoRoot, ...options });
|
|
const expectedPlan = compileContextProfile({ repoRoot, ...options });
|
|
return withCarrierFixture({ repoRoot, artifact, expectedPlan }, ({ root, verify }) => {
|
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-context-native-'));
|
|
try {
|
|
const home = path.join(temp, 'home');
|
|
const codexHome = path.join(home, '.codex');
|
|
const cwd = path.join(temp, 'project');
|
|
const marketplace = path.join(temp, 'marketplace');
|
|
for (const dir of [codexHome, cwd, path.join(marketplace, '.agents/plugins')]) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
const env = { PATH: process.env.PATH, HOME: home, CODEX_HOME: codexHome,
|
|
CLAUDE_CONFIG_DIR: path.join(home, '.claude'), LANG: 'C.UTF-8',
|
|
DISABLE_TELEMETRY: '1', DISABLE_AUTOUPDATER: '1',
|
|
ECC_NATIVE_CODEX: process.env.ECC_NATIVE_CODEX || 'codex' };
|
|
const commandOptions = { cwd, env };
|
|
if (options.target === 'claude') {
|
|
const version = run('claude', ['--version'], commandOptions);
|
|
const validation = run('claude', ['plugin', 'validate', root], commandOptions);
|
|
const details = run('claude', ['--setting-sources', '', '--plugin-dir', root,
|
|
'plugin', 'details', 'ecc-context-carrier'], commandOptions);
|
|
const names = details.match(/Skills \(\d+\)\s+([^\n]+)/);
|
|
assert.ok(names, 'Claude did not report the skill inventory');
|
|
const nativeNames = names[1].split(', ').sort();
|
|
assert.deepEqual(nativeNames, artifact.entries.map(skill => skill.name).sort());
|
|
for (const component of ['Agents', 'Hooks', 'MCP servers', 'LSP servers']) {
|
|
assert.ok(details.includes(`${component} (0)`), `Unexpected native ${component}`);
|
|
}
|
|
verify();
|
|
return { provider: version, profileId: artifact.profileId,
|
|
selectedIds: artifact.selectedIds, excludedIds: artifact.excludedIds,
|
|
nativeNames, discovery: 'verified-component-inventory',
|
|
validation, projectedTokens: details.match(/Always-on:\s+([^\n]+)/)?.[1],
|
|
carrierDigest: artifact.carrierDigest,
|
|
invocation: 'unobserved', modelCalls: 0, credentialsCopied: false };
|
|
}
|
|
const codex = env.ECC_NATIVE_CODEX;
|
|
const version = run(codex, ['--version'], commandOptions);
|
|
fs.cpSync(root, path.join(marketplace, 'carrier'), { recursive: true });
|
|
fs.writeFileSync(path.join(marketplace, '.agents/plugins/marketplace.json'), JSON.stringify({
|
|
name: 'ecc-context-probe', plugins: [{ name: 'ecc-context-carrier',
|
|
source: { source: 'local', path: './carrier' },
|
|
policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' } }],
|
|
}));
|
|
const added = JSON.parse(run(codex, ['plugin', 'marketplace', 'add', marketplace, '--json'], commandOptions));
|
|
const installed = JSON.parse(run(codex, ['plugin', 'add', 'ecc-context-carrier@ecc-context-probe', '--json'], commandOptions));
|
|
// Discovery must survive removal of the marketplace's source skill tree.
|
|
fs.rmSync(path.join(marketplace, 'carrier'), { recursive: true });
|
|
const observed = JSON.parse(run(process.execPath, [__filename, '--list-skills'], commandOptions));
|
|
assert.equal(observed.skills.data.length, 1);
|
|
const entry = observed.skills.data[0];
|
|
assert.deepEqual(entry.errors, [], 'Native parser rejected a selected skill');
|
|
const nativeSkills = entry.skills.filter(skill => skill.pluginId === 'ecc-context-carrier@ecc-context-probe');
|
|
const expectedNames = artifact.entries.map(skill => `ecc-context-carrier:${skill.name}`).sort();
|
|
const actualNames = nativeSkills.map(skill => skill.name).sort();
|
|
assert.deepEqual(actualNames, expectedNames, `Native skill selection mismatch: ${JSON.stringify(entry)}`);
|
|
let resourceCount = 0;
|
|
for (const skill of nativeSkills) {
|
|
assert.equal(skill.enabled, true);
|
|
assert.ok(skill.path.startsWith(`${fs.realpathSync(codexHome)}${path.sep}`), 'Skill escaped isolated Codex home');
|
|
const expected = artifact.entries.find(item => `ecc-context-carrier:${item.name}` === skill.name);
|
|
for (const file of artifact.files.filter(item => item.skillId === expected.id)) {
|
|
const relative = file.destinationPath.slice(`skills/${expected.name}/`.length);
|
|
const bytes = fs.readFileSync(path.join(path.dirname(skill.path), relative));
|
|
const digest = require('node:crypto').createHash('sha256').update(bytes).digest('hex');
|
|
assert.equal(digest, file.digest, 'Installed resource bytes changed');
|
|
resourceCount++;
|
|
}
|
|
}
|
|
verify();
|
|
assert.equal(fs.existsSync(path.join(codexHome, 'auth.json')), false);
|
|
return { provider: version, profileId: artifact.profileId, selectedIds: artifact.selectedIds,
|
|
excludedIds: artifact.excludedIds, discovery: 'verified', resources: resourceCount,
|
|
relocation: 'verified-after-source-removal', carrierDigest: artifact.carrierDigest,
|
|
nativeNames: actualNames, systemSkills: entry.skills.filter(skill => !skill.pluginId).map(skill => skill.name),
|
|
marketplaceAdded: !!added, installed: !!installed, invocation: 'unobserved',
|
|
modelCalls: 0, credentialsCopied: false };
|
|
} finally {
|
|
fs.rmSync(temp, { recursive: true, force: true });
|
|
}
|
|
});
|
|
}
|
|
|
|
if (process.argv.includes('--list-skills')) {
|
|
listSkills().catch(error => { console.error(error); process.exitCode = 1; });
|
|
} else {
|
|
const cases = process.argv.includes('--claude') ? [
|
|
{ profileId: 'lean@1', target: 'claude' },
|
|
{ profileId: 'full@1', target: 'claude', exclude: ['skill:python-patterns'] },
|
|
] : [
|
|
{ profileId: 'lean@1', target: 'codex' },
|
|
{ profileId: 'lean@1', target: 'codex', include: ['skill:angular-developer'] },
|
|
{ profileId: 'full@1', target: 'codex', exclude: ['skill:python-patterns'] },
|
|
];
|
|
for (const options of cases) process.stdout.write(`${JSON.stringify(probe(options))}\n`);
|
|
}
|