Files
ECC/docker/context-profiles/native-probe.js
T

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`);
}