fix(node-cli): confine agent runtime paths
This commit is contained in:
1 parent
3d2dccd8d4
commit
9ae1b77e45
6 files changed
+322
-59
No files matched your search
@@ -1,11 +1,12 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { chmod, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
||||
import { chmod, mkdir, mkdtemp, readFile, rm, stat, symlink, writeFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import test from 'node:test';
|
||||
|
||||
import { resolveInstallSecrets } from '../dist/cli-secrets.js';
|
||||
import { requirePhysicalDirectory } from '../dist/runtime-paths.js';
|
||||
|
||||
const PRIVATE_KEY = Buffer.alloc(32, 7).toString('base64url');
|
||||
const API_TOKEN = 'ely_test_token';
|
||||
@@ -102,6 +103,151 @@ test('CLI rejects legacy secret arguments before writing files', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('CLI rejects agent IDs that escape the credential directory', async () => {
|
||||
const invalidAgentIds = [
|
||||
'../escape',
|
||||
'..\\escape',
|
||||
'C:escape',
|
||||
'agent.',
|
||||
'agent ',
|
||||
' agent',
|
||||
'CON',
|
||||
'COM¹.log',
|
||||
'.',
|
||||
'..',
|
||||
];
|
||||
for (const agentId of invalidAgentIds) {
|
||||
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-node-cli-'));
|
||||
try {
|
||||
const result = await runCli([
|
||||
'install',
|
||||
'--agent', 'opencode',
|
||||
'--org_id', 'org-1',
|
||||
'--agent_id', agentId,
|
||||
'--kid', 'key-1',
|
||||
], homeDir);
|
||||
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /(Invalid agent ID for local storage|Agent ID escapes)/);
|
||||
await assert.rejects(stat(path.join(homeDir, '.elydora')), { code: 'ENOENT' });
|
||||
} finally {
|
||||
await rm(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('uninstall validates the stored runtime identity before plugin changes', async () => {
|
||||
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-node-cli-'));
|
||||
const runtimeDir = path.join(homeDir, '.elydora', 'stored-directory');
|
||||
try {
|
||||
await mkdir(runtimeDir, { recursive: true });
|
||||
await writeFile(
|
||||
path.join(runtimeDir, 'config.json'),
|
||||
JSON.stringify({ agent_name: 'opencode', agent_id: 'different-agent' }),
|
||||
'utf-8',
|
||||
);
|
||||
const result = await runCli(['uninstall', '--agent', 'opencode'], homeDir);
|
||||
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /crosses its runtime directory/);
|
||||
assert.equal((await stat(runtimeDir)).isDirectory(), true);
|
||||
} finally {
|
||||
await rm(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('uninstall requires an explicit ID for ambiguous agent runtimes', async () => {
|
||||
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-node-cli-'));
|
||||
try {
|
||||
for (const agentId of ['agent-1', 'agent-2']) {
|
||||
const runtimeDir = path.join(homeDir, '.elydora', agentId);
|
||||
await mkdir(runtimeDir, { recursive: true });
|
||||
await writeFile(
|
||||
path.join(runtimeDir, 'config.json'),
|
||||
JSON.stringify({ agent_name: 'opencode', agent_id: agentId }),
|
||||
'utf-8',
|
||||
);
|
||||
}
|
||||
const result = await runCli(['uninstall', '--agent', 'opencode'], homeDir);
|
||||
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /Multiple installed agents/);
|
||||
assert.equal((await stat(path.join(homeDir, '.elydora', 'agent-1'))).isDirectory(), true);
|
||||
assert.equal((await stat(path.join(homeDir, '.elydora', 'agent-2'))).isDirectory(), true);
|
||||
} finally {
|
||||
await rm(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('runtime directory checks reject symbolic-link children', async (t) => {
|
||||
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-node-cli-'));
|
||||
const targetDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-node-target-'));
|
||||
const runtimeRoot = path.join(homeDir, '.elydora');
|
||||
const linkPath = path.join(runtimeRoot, 'agent-1');
|
||||
try {
|
||||
await mkdir(runtimeRoot, { recursive: true });
|
||||
try {
|
||||
await symlink(targetDir, linkPath, 'dir');
|
||||
} catch (error) {
|
||||
t.skip(`Directory symbolic links are unavailable: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await assert.rejects(
|
||||
requirePhysicalDirectory(linkPath),
|
||||
/not a physical directory/,
|
||||
);
|
||||
} finally {
|
||||
await rm(homeDir, { recursive: true, force: true });
|
||||
await rm(targetDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('status surfaces malformed runtime config', async () => {
|
||||
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-node-cli-'));
|
||||
const runtimeDir = path.join(homeDir, '.elydora', 'agent-1');
|
||||
try {
|
||||
await mkdir(runtimeDir, { recursive: true });
|
||||
await writeFile(path.join(runtimeDir, 'config.json'), '{invalid-json', 'utf-8');
|
||||
|
||||
const result = await runCli(['status'], homeDir);
|
||||
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /Could not parse agent config/);
|
||||
} finally {
|
||||
await rm(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('status rejects symbolic-link runtime configs', async (t) => {
|
||||
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-node-cli-'));
|
||||
const targetDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-node-target-'));
|
||||
const runtimeDir = path.join(homeDir, '.elydora', 'agent-1');
|
||||
const targetConfig = path.join(targetDir, 'config.json');
|
||||
try {
|
||||
await mkdir(runtimeDir, { recursive: true });
|
||||
await writeFile(
|
||||
targetConfig,
|
||||
JSON.stringify({ agent_name: 'opencode', agent_id: 'agent-1' }),
|
||||
'utf-8',
|
||||
);
|
||||
try {
|
||||
await symlink(targetConfig, path.join(runtimeDir, 'config.json'), 'file');
|
||||
} catch (error) {
|
||||
t.skip(`File symbolic links are unavailable: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await runCli(['status'], homeDir);
|
||||
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /runtime config is not a physical file/);
|
||||
} finally {
|
||||
await rm(homeDir, { recursive: true, force: true });
|
||||
await rm(targetDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('CLI installs from owner-only files and protects persisted credentials', async () => {
|
||||
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-node-cli-'));
|
||||
const privateKeyFile = path.join(homeDir, 'private-key');
|
||||
|
||||
Reference in new issue
Block a user