Files
TORCH/test/fleet/runtime-plugins.test.mjs
T

238 lines
12 KiB
JavaScript

import assert from 'node:assert/strict';
import { execFileSync, spawnSync } from 'node:child_process';
import { existsSync, mkdtempSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { createRuntimeAdapterRegistry } from '../../src/adapters/registry.mjs';
import { inspectRuntimePluginTrust, planRuntimePluginTrust, trustRuntimePlugin } from '../../src/adapters/plugins.mjs';
import { openControlPlane } from '../../src/control-plane/service.mjs';
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
import { proposeDomains } from '../../src/kernel/domains.mjs';
import { diagnoseProject } from '../../src/kernel/doctor.mjs';
import { inspectRepository } from '../../src/kernel/git.mjs';
import { installProject } from '../../src/kernel/install.mjs';
import { createWorktrees } from '../../src/kernel/worktrees.mjs';
import { planFleetUp } from '../../src/runtime/lifecycle.mjs';
const CLI = new URL('../../bin/torch.mjs', import.meta.url).pathname;
const PLUGIN_NAME = 'portable-local';
function pluginSource(name, sentinel) {
return `
const fs = require('node:fs');
fs.appendFileSync(${JSON.stringify(sentinel)}, 'loaded\\n');
exports.createTorchRuntimeAdapter = ({ env }) => ({
name: ${JSON.stringify(name)},
configuration: { executable: 'local-agent' },
capabilities: {
detect: true, configure: 'plan-only', modelSelection: true, modelRequired: true,
reasoningSelection: true, createSession: true, resumeSession: true,
perInvocationCostCeiling: { createSession: [], resumeSession: [] },
sendOrSteer: false, getStatus: false, listSessions: false, stopSession: false,
captureRuntimeId: false, installHooks: false, removeHooks: false,
},
detect: () => ({ available: true, adapter: ${JSON.stringify(name)} }),
configure: ({ repositoryRoot, areaId }) => ({
adapter: ${JSON.stringify(name)}, mutationPerformed: false,
mcp: { name: 'torch-' + areaId, command: process.execPath, args: [], cwd: repositoryRoot },
}),
createSession: ({ areaId, worktree, model, reasoning }) => ({
adapter: ${JSON.stringify(name)}, areaId, runtimeSessionId: null, requiresRuntimeIdCapture: false,
profile: { model, reasoning },
launch: { command: 'local-agent', args: [model, reasoning].filter(Boolean), cwd: worktree, mutatesRuntime: true },
}),
resumeSession: ({ areaId, runtimeSessionId, worktree, model, reasoning }) => ({
adapter: ${JSON.stringify(name)}, areaId, runtimeSessionId, profile: { model, reasoning },
launch: { command: 'local-agent', args: [model, reasoning].filter(Boolean), cwd: worktree, mutatesRuntime: true },
}),
sendOrSteer: () => ({ supported: false }), getStatus: () => ({ supported: false }),
listSessions: () => ({ supported: false }), stopSession: () => ({ supported: false }),
captureRuntimeId: () => ({ supported: false }), installHooks: () => ({ supported: false }),
removeHooks: () => ({ supported: false }),
});
`;
}
function environment(root) {
return {
...process.env,
XDG_CONFIG_HOME: join(root, 'config'),
XDG_DATA_HOME: join(root, 'data'),
};
}
function runCli(args, env, cwd) {
return spawnSync(process.execPath, [CLI, ...args], { cwd, env, encoding: 'utf8' });
}
function repository(root) {
execFileSync('git', ['init', '-b', 'main', root]);
execFileSync('git', ['-C', root, 'config', 'user.email', '[email protected]']);
execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']);
writeFileSync(join(root, 'app.mjs'), 'export const application = true;\n');
execFileSync('git', ['-C', root, 'add', '.']);
execFileSync('git', ['-C', root, 'commit', '-m', 'fixture']);
return inspectRepository(root);
}
test('SCN-runtime-plugin-trust: user approval pins one module hash without executing it during review', () => {
const root = mkdtempSync(join(tmpdir(), 'torch-runtime-plugin-trust-'));
const env = environment(root);
const modulePath = join(root, 'portable-local.cjs');
const sentinel = join(root, 'plugin-loaded.txt');
writeFileSync(modulePath, pluginSource(PLUGIN_NAME, sentinel));
const preview = planRuntimePluginTrust({ name: PLUGIN_NAME, modulePath, env });
assert.equal(preview.mutationPerformed, false);
assert.equal(preview.executesPluginDuringPlan, false);
assert.equal(existsSync(sentinel), false, 'review must never evaluate plugin code');
assert.equal(existsSync(preview.trustStorePath), false);
const cliPreview = runCli([
'runtimes', 'trust', '--name', PLUGIN_NAME, '--module', modulePath, '--json',
], env, root);
assert.equal(cliPreview.status, 0, cliPreview.stderr || cliPreview.stdout);
let plannedTrust = JSON.parse(cliPreview.stdout);
assert.equal(plannedTrust.requiresApproval, true);
assert.equal(existsSync(sentinel), false);
assert.equal(existsSync(preview.trustStorePath), false, 'preview does not change user config');
writeFileSync(modulePath, `${pluginSource(PLUGIN_NAME, sentinel)}\n// changed after preview\n`);
const staleApproval = runCli([
'runtimes', 'trust', '--name', PLUGIN_NAME, '--module', modulePath,
'--yes', '--sha256', plannedTrust.sha256, '--json',
], env, root);
assert.equal(staleApproval.status, 2);
assert.equal(JSON.parse(staleApproval.stdout).error, 'RUNTIME_PLUGIN_REVIEW_STALE');
assert.equal(existsSync(preview.trustStorePath), false, 'a changed module cannot inherit prior approval');
const refreshedPreview = runCli([
'runtimes', 'trust', '--name', PLUGIN_NAME, '--module', modulePath, '--json',
], env, root);
plannedTrust = JSON.parse(refreshedPreview.stdout);
const trusted = runCli([
'runtimes', 'trust', '--name', PLUGIN_NAME, '--module', modulePath,
'--yes', '--sha256', plannedTrust.sha256, '--json',
], env, root);
assert.equal(trusted.status, 0, trusted.stderr || trusted.stdout);
assert.equal(JSON.parse(trusted.stdout).mutationPerformed, true);
assert.equal(existsSync(sentinel), false, 'trusting records code; it does not load or launch it');
const trustPath = JSON.parse(trusted.stdout).trustStorePath;
assert.equal(statSync(trustPath).mode & 0o777, 0o600);
const catalog = runCli(['runtimes', 'list', '--json'], env, root);
assert.equal(catalog.status, 0, catalog.stderr || catalog.stdout);
const list = JSON.parse(catalog.stdout);
assert.equal(list.executesPluginCode, false);
assert.equal(list.trust.plugins[0].status, 'trusted');
assert.equal(existsSync(sentinel), false, 'catalog inspection must not evaluate plugins');
const inspectionRegistry = createRuntimeAdapterRegistry({ env });
assert.equal(inspectionRegistry.has(PLUGIN_NAME), false, 'general registry inspection does not execute plugins');
assert.equal(inspectionRegistry.trustedPlugins[0].status, 'trusted');
assert.equal(existsSync(sentinel), false);
const registry = createRuntimeAdapterRegistry({ env, loadPlugins: true });
assert.equal(registry.has(PLUGIN_NAME), true);
assert.equal(registry.pluginErrors.length, 0);
assert.equal(existsSync(sentinel), true, 'only a registry load executes explicitly trusted code');
writeFileSync(modulePath, `${pluginSource(PLUGIN_NAME, sentinel)}\n// changed after approval\n`);
const changedRegistry = createRuntimeAdapterRegistry({ env });
assert.equal(changedRegistry.has(PLUGIN_NAME), false);
assert.equal(changedRegistry.pluginErrors[0].code, 'RUNTIME_PLUGIN_HASH_MISMATCH');
assert.equal(inspectRuntimePluginTrust({ env }).plugins[0].status, 'hash-mismatch');
const revokePreview = runCli(['runtimes', 'revoke', '--name', PLUGIN_NAME, '--json'], env, root);
assert.equal(revokePreview.status, 0, revokePreview.stderr || revokePreview.stdout);
assert.equal(JSON.parse(revokePreview.stdout).requiresApproval, true);
const plannedRevoke = JSON.parse(revokePreview.stdout);
const revoked = runCli([
'runtimes', 'revoke', '--name', PLUGIN_NAME, '--yes', '--sha256', plannedRevoke.sha256, '--json',
], env, root);
assert.equal(revoked.status, 0, revoked.stderr || revoked.stdout);
assert.equal(JSON.parse(revoked.stdout).disposition, 'revoked');
assert.equal(JSON.parse(revoked.stdout).sessionsStopped, false);
assert.deepEqual(inspectRuntimePluginTrust({ env }).plugins, []);
});
test('SCN-runtime-plugin-lifecycle: an explicitly trusted local adapter participates in mixed Fleet planning only', () => {
const root = mkdtempSync(join(tmpdir(), 'torch-runtime-plugin-lifecycle-'));
const env = environment(root);
const repositoryRoot = join(root, 'project');
const modulePath = join(root, 'portable-local.cjs');
writeFileSync(modulePath, pluginSource(PLUGIN_NAME, join(root, 'loaded.txt')));
const trustPlan = planRuntimePluginTrust({ name: PLUGIN_NAME, modulePath, env });
trustRuntimePlugin({
name: PLUGIN_NAME, modulePath, env, approved: true, expectedSha256: trustPlan.sha256,
});
const inspected = repository(repositoryRoot);
const proposal = proposeDomains({ repository: inspected, analysis: analyzeRepository(inspected) });
const worker = proposal.domains[0];
worker.runtime = 'claude';
worker.model = null;
worker.reasoning = null;
proposal.session_manager = { runtime: 'claude', reasoning: 'high' };
proposal.review = {
status: 'approved', reviewedAt: '2026-09-28T00:00:00Z', reviewedBy: 'fixture-owner', notes: [],
};
installProject({
repository: inspected, proposal, env, projectId: 'trusted-runtime-plugin',
runtimes: ['claude'],
});
execFileSync('git', ['-C', repositoryRoot, 'add', '.torch']);
execFileSync('git', ['-C', repositoryRoot, 'commit', '-m', 'install TORCH']);
const profileChange = runCli([
'profile', 'set', '--area', worker.id, '--runtime', PLUGIN_NAME,
'--model', 'local-provider/model-a', '--reasoning', 'high', '--yes', '--json',
], env, repositoryRoot);
assert.equal(profileChange.status, 0, profileChange.stderr || profileChange.stdout);
assert.equal(JSON.parse(profileChange.stdout).profile.runtime, PLUGIN_NAME);
const config = JSON.parse(readFileSync(join(repositoryRoot, '.torch', 'torch.yaml'), 'utf8'));
assert.deepEqual(config.runtimes[PLUGIN_NAME], { executable: 'local-agent' });
assert.equal(config.domains.find((domain) => domain.id === worker.id).runtime, PLUGIN_NAME);
execFileSync('git', ['-C', repositoryRoot, 'add', '.torch/torch.yaml']);
execFileSync('git', ['-C', repositoryRoot, 'commit', '-m', 'assign trusted local runtime']);
createWorktrees({
repository: inspectRepository(repositoryRoot),
parentOverride: join(root, 'worktrees'),
});
const control = openControlPlane({ repositoryRoot, env });
try {
const registry = createRuntimeAdapterRegistry({ env, loadPlugins: true });
const plan = planFleetUp({
repositoryRoot, controlPlane: control, adapters: registry.toMap(), fresh: true,
});
assert.equal(plan.canProceed, true, JSON.stringify(plan.blockers));
const action = plan.actions.find((entry) => entry.areaId === worker.id);
assert.equal(action.runtime, PLUGIN_NAME);
assert.deepEqual(action.profile, {
model: 'local-provider/model-a', reasoning: 'high', launchPolicy: {},
});
assert.equal(action.launch.command, 'local-agent');
assert.equal(plan.mutationPerformed, false);
assert.equal(plan.actions.every((entry) => entry.launch.mutatesRuntime), true);
assert.equal(existsSync(join(root, 'loaded.txt')), true);
} finally {
control.close();
}
unlinkSync(join(root, 'loaded.txt'));
const trustedDiagnosis = diagnoseProject({ repository: inspectRepository(repositoryRoot), env });
assert.equal(trustedDiagnosis.findings.some((finding) =>
finding.code === 'RUNTIME_ADAPTER_TRUSTED_NOT_LOADED' && finding.runtime === PLUGIN_NAME), true);
assert.equal(existsSync(join(root, 'loaded.txt')), false, 'doctor reports the user trust record without loading plugin code');
const untrustedRoot = mkdtempSync(join(tmpdir(), 'torch-runtime-plugin-untrusted-'));
const untrustedEnv = environment(untrustedRoot);
const untrustedRegistry = createRuntimeAdapterRegistry({ env: untrustedEnv });
assert.equal(untrustedRegistry.has(PLUGIN_NAME), false);
assert.equal(untrustedRegistry.pluginErrors.length, 0);
const diagnosis = diagnoseProject({ repository: inspectRepository(repositoryRoot), env: untrustedEnv });
assert.equal(diagnosis.findings.some((finding) =>
finding.code === 'RUNTIME_ADAPTER_UNKNOWN' && finding.runtime === PLUGIN_NAME), true,
'project runtime configuration cannot grant trust to a local plugin');
});