fix(letta): harden official hook integration
This commit is contained in:
1 parent
69a75153f5
commit
53b08ec4a7
13 files changed
+1985
-146
No files matched your search
@@ -0,0 +1,153 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import {
|
||||
createFixture,
|
||||
environment,
|
||||
managedHandler,
|
||||
readSettings,
|
||||
runProcess,
|
||||
startApiServer,
|
||||
} from '../test-support/letta.mjs';
|
||||
|
||||
const sourceRoot = process.env.ELYDORA_LETTA_SOURCE;
|
||||
const bunBinary = process.env.ELYDORA_BUN_BINARY ?? 'bun';
|
||||
|
||||
function moduleUrl(relativePath) {
|
||||
return pathToFileURL(path.join(sourceRoot, relativePath)).href;
|
||||
}
|
||||
|
||||
function resultJson(stdout) {
|
||||
const line = stdout.trim().split(/\r?\n/).at(-1);
|
||||
return JSON.parse(line);
|
||||
}
|
||||
|
||||
async function runOfficialProbe(fixture, mode) {
|
||||
const settings = await readSettings(fixture.globalPath);
|
||||
const commands = {
|
||||
guard: managedHandler(settings, 'PreToolUse').command,
|
||||
audit: managedHandler(settings, 'PostToolUse').command,
|
||||
failure: managedHandler(settings, 'PostToolUseFailure').command,
|
||||
};
|
||||
const source = `
|
||||
import { settingsManager } from ${JSON.stringify(moduleUrl('src/settings-manager.ts'))};
|
||||
import { getHooksForEvent } from ${JSON.stringify(moduleUrl('src/hooks/loader.ts'))};
|
||||
import { executeCommandHook } from ${JSON.stringify(moduleUrl('src/hooks/executor.ts'))};
|
||||
import { runPostToolUseHooks } from ${JSON.stringify(moduleUrl('src/hooks/index.ts'))};
|
||||
const workspace = process.env.ELYDORA_OFFICIAL_WORKSPACE;
|
||||
const commands = JSON.parse(process.env.ELYDORA_OFFICIAL_COMMANDS);
|
||||
await settingsManager.initialize();
|
||||
await settingsManager.loadProjectSettings(workspace);
|
||||
await settingsManager.loadLocalProjectSettings(workspace);
|
||||
const pre = await getHooksForEvent('PreToolUse', 'Bash', workspace);
|
||||
const post = await getHooksForEvent('PostToolUse', 'Bash', workspace);
|
||||
const failure = await getHooksForEvent('PostToolUseFailure', 'Bash', workspace);
|
||||
const managed = (hooks, command) => hooks.find((hook) => (
|
||||
hook.type === 'command' && hook.command === command
|
||||
));
|
||||
if (process.env.ELYDORA_OFFICIAL_MODE === 'active') {
|
||||
const guardResult = await executeCommandHook(
|
||||
managed(pre, commands.guard),
|
||||
{
|
||||
event_type: 'PreToolUse',
|
||||
working_directory: workspace,
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'echo official' },
|
||||
tool_call_id: 'official-pre',
|
||||
agent_id: 'letta-agent',
|
||||
},
|
||||
workspace,
|
||||
);
|
||||
const postResult = await executeCommandHook(
|
||||
managed(post, commands.audit),
|
||||
{
|
||||
event_type: 'PostToolUse',
|
||||
working_directory: workspace,
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'echo official' },
|
||||
tool_result: { status: 'success', output: 'official' },
|
||||
tool_call_id: 'official-post',
|
||||
},
|
||||
workspace,
|
||||
);
|
||||
const failureResult = await executeCommandHook(
|
||||
managed(failure, commands.failure),
|
||||
{
|
||||
event_type: 'PostToolUseFailure',
|
||||
working_directory: workspace,
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'exit 1' },
|
||||
error_message: 'official failure',
|
||||
error_type: 'ProcessError',
|
||||
tool_call_id: 'official-failure',
|
||||
},
|
||||
workspace,
|
||||
);
|
||||
console.log(JSON.stringify({
|
||||
counts: [pre.length, post.length, failure.length],
|
||||
exits: [guardResult.exitCode, postResult.exitCode, failureResult.exitCode],
|
||||
}));
|
||||
} else {
|
||||
const result = await runPostToolUseHooks(
|
||||
'Bash',
|
||||
{ command: 'echo unavailable' },
|
||||
{ status: 'success', output: 'unavailable' },
|
||||
'official-post-error',
|
||||
workspace,
|
||||
);
|
||||
console.log(JSON.stringify({
|
||||
blocked: result.blocked,
|
||||
errored: result.errored,
|
||||
exits: result.results.map((entry) => entry.exitCode),
|
||||
}));
|
||||
}
|
||||
await settingsManager.flush();
|
||||
`;
|
||||
return runProcess(
|
||||
bunBinary,
|
||||
['--silent', '--eval', source],
|
||||
{
|
||||
...environment(fixture),
|
||||
LETTA_SKIP_KEYCHAIN_CHECK: '1',
|
||||
ELYDORA_OFFICIAL_COMMANDS: JSON.stringify(commands),
|
||||
ELYDORA_OFFICIAL_MODE: mode,
|
||||
ELYDORA_OFFICIAL_WORKSPACE: fixture.projectDir,
|
||||
},
|
||||
sourceRoot,
|
||||
);
|
||||
}
|
||||
|
||||
test('official Letta Code 0.28.13 loads and executes all managed hooks', {
|
||||
skip: sourceRoot ? false : 'set ELYDORA_LETTA_SOURCE to the official Letta Code source tree',
|
||||
timeout: 60_000,
|
||||
}, async () => {
|
||||
const manifest = JSON.parse(await readFile(path.join(sourceRoot, 'package.json'), 'utf-8'));
|
||||
assert.equal(manifest.version, '0.28.13');
|
||||
const api = await startApiServer();
|
||||
const fixture = await createFixture({ baseUrl: api.baseUrl });
|
||||
let apiClosed = false;
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const active = await runOfficialProbe(fixture, 'active');
|
||||
assert.equal(active.code, 0, active.stderr);
|
||||
assert.deepEqual(resultJson(active.stdout), {
|
||||
counts: [1, 1, 1],
|
||||
exits: [0, 0, 0],
|
||||
});
|
||||
assert.equal(api.requests.filter((request) => request.method === 'POST').length, 2);
|
||||
await api.close();
|
||||
apiClosed = true;
|
||||
const unavailable = await runOfficialProbe(fixture, 'unavailable');
|
||||
assert.equal(unavailable.code, 0, unavailable.stderr);
|
||||
assert.deepEqual(resultJson(unavailable.stdout), {
|
||||
blocked: false,
|
||||
errored: true,
|
||||
exits: [1],
|
||||
});
|
||||
} finally {
|
||||
await fixture.close();
|
||||
if (!apiClosed) await api.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,334 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { lstat, readFile, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
VALID_PRIVATE_KEY,
|
||||
cliPath,
|
||||
createFixture,
|
||||
environment,
|
||||
managedHandler,
|
||||
readSettings,
|
||||
registryModuleUrl,
|
||||
runNode,
|
||||
runPlugin,
|
||||
} from '../test-support/letta.mjs';
|
||||
|
||||
async function assertMissing(filePath) {
|
||||
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
|
||||
}
|
||||
|
||||
function assertManagedTriple(settings) {
|
||||
for (const event of ['PreToolUse', 'PostToolUse', 'PostToolUseFailure']) {
|
||||
const group = settings.hooks[event].find((entry) => entry.hooks[0]?.timeout === 10_000);
|
||||
assert.deepEqual(Object.keys(group).sort(), ['hooks', 'matcher']);
|
||||
assert.equal(group.matcher, '*');
|
||||
assert.equal(group.hooks.length, 1);
|
||||
assert.deepEqual(Object.keys(group.hooks[0]).sort(), ['command', 'timeout', 'type']);
|
||||
assert.equal(group.hooks[0].type, 'command');
|
||||
assert.equal(group.hooks[0].timeout, 10_000);
|
||||
assert.doesNotMatch(group.hooks[0].command, /^node /);
|
||||
}
|
||||
assert.equal(
|
||||
managedHandler(settings, 'PostToolUse').command,
|
||||
managedHandler(settings, 'PostToolUseFailure').command,
|
||||
);
|
||||
}
|
||||
|
||||
test('Letta Code is registered in the SDK and CLI', async () => {
|
||||
const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
|
||||
assert.deepEqual(SUPPORTED_AGENTS.get('letta'), {
|
||||
name: 'Letta Code',
|
||||
configDir: '~/.letta',
|
||||
configFile: 'settings.json',
|
||||
});
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
const result = await runNode(
|
||||
['--no-warnings', cliPath, 'status'],
|
||||
environment(fixture),
|
||||
fixture.projectDir,
|
||||
);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
assert.match(result.stdout, /Letta Code \(letta\)/);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta installs the current three-event contract and preserves every source', async () => {
|
||||
const globalSettings = [
|
||||
'{',
|
||||
' "theme": "dark",',
|
||||
' "hooks": {',
|
||||
' "FutureEvent": [null],',
|
||||
' "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "user-hook", "quiet": true }] }]',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
].join('\r\n');
|
||||
const projectSettings = { hooks: { PostToolUse: [{
|
||||
matcher: 'Read',
|
||||
hooks: [{ type: 'prompt', prompt: 'Review $ARGUMENTS', model: 'fast' }],
|
||||
}] } };
|
||||
const localSettings = { windowTitle: 'workspace' };
|
||||
const fixture = await createFixture({ globalSettings, projectSettings, localSettings });
|
||||
try {
|
||||
const projectBefore = await readFile(fixture.projectPath, 'utf-8');
|
||||
const localBefore = await readFile(fixture.localPath, 'utf-8');
|
||||
const first = await fixture.install();
|
||||
assert.equal(first.code, 0, first.stderr);
|
||||
assert.match(first.stdout, /run \/hooks/i);
|
||||
assert.match(first.stdout, /restart active sessions/i);
|
||||
const raw = await readFile(fixture.globalPath, 'utf-8');
|
||||
assert.match(raw, /\r\n/);
|
||||
const settings = JSON.parse(raw);
|
||||
assert.equal(settings.theme, 'dark');
|
||||
assert.deepEqual(settings.hooks.FutureEvent, [null]);
|
||||
assert.equal(settings.hooks.PreToolUse[0].hooks[0].command, 'user-hook');
|
||||
assertManagedTriple(settings);
|
||||
assert.equal(await readFile(fixture.projectPath, 'utf-8'), projectBefore);
|
||||
assert.equal(await readFile(fixture.localPath, 'utf-8'), localBefore);
|
||||
for (const filePath of [
|
||||
fixture.guardScriptPath,
|
||||
fixture.hookScriptPath,
|
||||
path.join(fixture.agentDir, 'config.json'),
|
||||
path.join(fixture.agentDir, 'private.key'),
|
||||
]) await lstat(filePath);
|
||||
const before = await readFile(fixture.globalPath, 'utf-8');
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
assert.equal(await readFile(fixture.globalPath, 'utf-8'), before);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta validates known hook schemas and preserves future events', async () => {
|
||||
const fixture = await createFixture({ globalSettings: {
|
||||
hooks: {
|
||||
FutureEvent: [null],
|
||||
PreToolUse: [{ matcher: '[', hooks: [{
|
||||
type: 'command',
|
||||
command: 'user-command',
|
||||
timeout: 0,
|
||||
quiet: false,
|
||||
futureField: true,
|
||||
}] }],
|
||||
Stop: [{ hooks: [{
|
||||
type: 'prompt',
|
||||
prompt: 'Evaluate $ARGUMENTS',
|
||||
model: 'fast',
|
||||
timeout: 30,
|
||||
}] }],
|
||||
},
|
||||
} });
|
||||
try {
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
const settings = await readSettings(fixture.globalPath);
|
||||
assert.deepEqual(settings.hooks.FutureEvent, [null]);
|
||||
assert.equal(settings.hooks.PreToolUse[0].matcher, '[');
|
||||
assert.equal(settings.hooks.PreToolUse[0].hooks[0].futureField, true);
|
||||
assert.equal(settings.hooks.Stop[0].hooks[0].type, 'prompt');
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta rejects malformed affected settings before runtime writes', async (t) => {
|
||||
const cases = [
|
||||
['syntax', '{ malformed', /parse Letta Code global settings/i],
|
||||
['root', '[]', /must contain a JSON object/i],
|
||||
['comment', '{ // comment\n "hooks": {} }', /parse Letta Code global settings/i],
|
||||
['trailing comma', '{ "theme": true, }', /parse Letta Code global settings/i],
|
||||
['duplicate', '{ "hooks": {}, "hooks": {} }', /duplicate field "hooks"/i],
|
||||
['hooks shape', '{ "hooks": [] }', /field "hooks" must be an object/i],
|
||||
['disabled', '{ "hooks": { "disabled": "yes" } }', /hooks.disabled.*boolean/i],
|
||||
['event shape', '{ "hooks": { "PreToolUse": null } }', /must be an array/i],
|
||||
['group shape', '{ "hooks": { "PreToolUse": [null] } }', /group.*object/i],
|
||||
['tool matcher', '{ "hooks": { "PreToolUse": [{ "hooks": [] }] } }', /matcher must be a string/i],
|
||||
['simple matcher', '{ "hooks": { "Stop": [{ "matcher": "*", "hooks": [] }] } }', /matcher is unsupported/i],
|
||||
['hooks missing', '{ "hooks": { "PreToolUse": [{ "matcher": "*" }] } }', /hooks array/i],
|
||||
['handler shape', '{ "hooks": { "Stop": [{ "hooks": [null] }] } }', /hooks\[0\].*object/i],
|
||||
['handler type', '{ "hooks": { "Stop": [{ "hooks": [{ "type": "http" }] }] } }', /unsupported type/i],
|
||||
['command', '{ "hooks": { "PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command" }] }] } }', /non-empty command/i],
|
||||
['prompt', '{ "hooks": { "Stop": [{ "hooks": [{ "type": "prompt" }] }] } }', /non-empty prompt/i],
|
||||
['model', '{ "hooks": { "Stop": [{ "hooks": [{ "type": "prompt", "prompt": "x", "model": 1 }] }] } }', /model must be a string/i],
|
||||
['quiet', '{ "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "x", "quiet": 1 }] }] } }', /quiet must be a boolean/i],
|
||||
['timeout', '{ "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "x", "timeout": -1 }] }] } }', /non-negative finite number/i],
|
||||
];
|
||||
for (const [name, source, pattern] of cases) {
|
||||
await t.test(name, async () => {
|
||||
const fixture = await createFixture({ globalSettings: source });
|
||||
try {
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, pattern);
|
||||
assert.equal(await readFile(fixture.globalPath, 'utf-8'), source);
|
||||
await assertMissing(fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta surfaces malformed project sources before writes', async (t) => {
|
||||
for (const field of ['projectSettings', 'localSettings']) {
|
||||
await t.test(field, async () => {
|
||||
const fixture = await createFixture({ [field]: '{ malformed' });
|
||||
try {
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /parse Letta Code (project|project-local) settings/i);
|
||||
await assertMissing(fixture.globalPath);
|
||||
await assertMissing(fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta applies the official hooks.disabled precedence', async (t) => {
|
||||
const cases = [
|
||||
['global true', { globalSettings: { hooks: { disabled: true } } }, 1],
|
||||
['project true', { projectSettings: { hooks: { disabled: true } } }, 1],
|
||||
['local true', { localSettings: { hooks: { disabled: true } } }, 1],
|
||||
['global false override', {
|
||||
globalSettings: { hooks: { disabled: false } },
|
||||
projectSettings: { hooks: { disabled: true } },
|
||||
localSettings: { hooks: { disabled: true } },
|
||||
}, 0],
|
||||
];
|
||||
for (const [name, options, expectedCode] of cases) {
|
||||
await t.test(name, async () => {
|
||||
const fixture = await createFixture(options);
|
||||
try {
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, expectedCode, result.stderr);
|
||||
if (expectedCode === 1) {
|
||||
assert.match(result.stderr, /hooks.disabled/i);
|
||||
await assertMissing(fixture.agentDir);
|
||||
}
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta migrates exact legacy handlers and preserves ownership lookalikes', async () => {
|
||||
const fixture = await createFixture({ globalSettings: {} });
|
||||
const legacy = (scriptPath) => ({
|
||||
matcher: '*',
|
||||
hooks: [{ type: 'command', command: `node "${scriptPath}"` }],
|
||||
});
|
||||
const lookalike = legacy(fixture.guardScriptPath);
|
||||
lookalike.hooks[0].quiet = true;
|
||||
await writeFile(fixture.globalPath, `${JSON.stringify({ hooks: {
|
||||
PreToolUse: [legacy(fixture.guardScriptPath), lookalike],
|
||||
PostToolUse: [legacy(fixture.hookScriptPath)],
|
||||
} }, null, 2)}\n`, { mode: 0o600 });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
let settings = await readSettings(fixture.globalPath);
|
||||
assertManagedTriple(settings);
|
||||
assert(settings.hooks.PreToolUse.some((group) => group.hooks[0].quiet === true));
|
||||
const managed = settings.hooks.PreToolUse.find((group) => group.hooks[0].timeout === 10_000);
|
||||
managed.userField = 'preserve-group';
|
||||
managed.hooks.push({ type: 'command', command: 'user-command' });
|
||||
await writeFile(fixture.globalPath, `${JSON.stringify(settings, null, 2)}\n`);
|
||||
assert.equal((await runPlugin(fixture, 'uninstall', 'agent-1')).code, 0);
|
||||
settings = await readSettings(fixture.globalPath);
|
||||
assert(settings.hooks.PreToolUse.some((group) => group.hooks[0].quiet === true));
|
||||
assert(settings.hooks.PreToolUse.some((group) => (
|
||||
group.hooks.some((handler) => handler.command === 'user-command')
|
||||
)));
|
||||
assert.equal(settings.hooks.PostToolUse, undefined);
|
||||
assert.equal(settings.hooks.PostToolUseFailure, undefined);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta status requires exact hooks and strict runtime identity', async () => {
|
||||
const fixture = await createFixture({ projectSettings: {} });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
let status = JSON.parse((await runPlugin(fixture, 'status', null)).stdout);
|
||||
assert.equal(status.installed, true);
|
||||
await writeFile(fixture.projectPath, '{"hooks":{"disabled":true}}\n');
|
||||
status = JSON.parse((await runPlugin(fixture, 'status', null)).stdout);
|
||||
assert.equal(status.hookConfigured, false);
|
||||
await writeFile(fixture.projectPath, '{}\n');
|
||||
status = JSON.parse((await runPlugin(fixture, 'status', null)).stdout);
|
||||
assert.equal(status.installed, true);
|
||||
const settings = await readSettings(fixture.globalPath);
|
||||
settings.hooks.PostToolUseFailure.push(settings.hooks.PostToolUseFailure.at(-1));
|
||||
await writeFile(fixture.globalPath, `${JSON.stringify(settings, null, 2)}\n`);
|
||||
status = JSON.parse((await runPlugin(fixture, 'status', null)).stdout);
|
||||
assert.equal(status.installed, false);
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
await writeFile(path.join(fixture.agentDir, 'private.key'), 'invalid');
|
||||
const invalid = await runPlugin(fixture, 'status', null);
|
||||
assert.equal(invalid.code, 1);
|
||||
assert.match(invalid.stderr, /private key is invalid/i);
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
await writeFile(fixture.hookScriptPath, 'tampered');
|
||||
status = JSON.parse((await runPlugin(fixture, 'status', null)).stdout);
|
||||
assert.equal(status.installed, false);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta uninstall removes a settings file containing only managed hooks', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
assert.equal((await runPlugin(fixture, 'uninstall', 'agent-1')).code, 0);
|
||||
await assertMissing(fixture.globalPath);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta CLI completes install, status, and uninstall end to end', async () => {
|
||||
const fixture = await createFixture({ globalSettings: { theme: 'dark' } });
|
||||
const privateKeyFile = path.join(fixture.rootDir, 'install-private.key');
|
||||
const tokenFile = path.join(fixture.rootDir, 'install-token.txt');
|
||||
try {
|
||||
await writeFile(privateKeyFile, `${VALID_PRIVATE_KEY}\n`, { mode: 0o600 });
|
||||
await writeFile(tokenFile, 'token-1\n', { mode: 0o600 });
|
||||
const install = await runNode([
|
||||
'--no-warnings', cliPath, 'install',
|
||||
'--agent', 'letta',
|
||||
'--org_id', 'org-1',
|
||||
'--agent_id', 'agent-1',
|
||||
'--kid', 'kid-1',
|
||||
'--private_key_file', privateKeyFile,
|
||||
'--token_file', tokenFile,
|
||||
'--base_url', fixture.baseUrl,
|
||||
], environment(fixture), fixture.projectDir);
|
||||
assert.equal(install.code, 0, install.stderr);
|
||||
assertManagedTriple(await readSettings(fixture.globalPath));
|
||||
const status = await runNode(
|
||||
['--no-warnings', cliPath, 'status'],
|
||||
environment(fixture),
|
||||
fixture.projectDir,
|
||||
);
|
||||
assert.equal(status.code, 0, status.stderr);
|
||||
assert.match(status.stdout, /Letta Code \(letta\) \[installed\]/);
|
||||
const uninstall = await runNode([
|
||||
'--no-warnings', cliPath, 'uninstall',
|
||||
'--agent', 'letta',
|
||||
'--agent_id', 'agent-1',
|
||||
], environment(fixture), fixture.projectDir);
|
||||
assert.equal(uninstall.code, 0, uninstall.stderr);
|
||||
assert.deepEqual(await readSettings(fixture.globalPath), { theme: 'dark' });
|
||||
await assertMissing(fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,179 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile, stat } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createFixture,
|
||||
managedHandler,
|
||||
readSettings,
|
||||
runLettaHook,
|
||||
startApiServer,
|
||||
} from '../test-support/letta.mjs';
|
||||
|
||||
function officialInput(fixture, event) {
|
||||
const base = {
|
||||
event_type: event,
|
||||
working_directory: fixture.projectDir,
|
||||
session_id: 'session-1',
|
||||
};
|
||||
if (event === 'PreToolUse') {
|
||||
return {
|
||||
...base,
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'echo test' },
|
||||
tool_call_id: 'call-1',
|
||||
agent_id: 'letta-agent-1',
|
||||
};
|
||||
}
|
||||
if (event === 'PostToolUse') {
|
||||
return {
|
||||
...base,
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'echo test' },
|
||||
tool_call_id: 'call-1',
|
||||
tool_result: { status: 'success', output: 'test' },
|
||||
agent_id: 'letta-agent-1',
|
||||
preceding_reasoning: 'Run the command.',
|
||||
preceding_assistant_message: 'Executing.',
|
||||
};
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'exit 1' },
|
||||
tool_call_id: 'call-2',
|
||||
error_message: 'Command failed',
|
||||
error_type: 'ProcessError',
|
||||
agent_id: 'letta-agent-1',
|
||||
preceding_reasoning: 'Run the command.',
|
||||
preceding_assistant_message: 'Executing.',
|
||||
};
|
||||
}
|
||||
|
||||
async function installedHandlers(fixture) {
|
||||
const settings = await readSettings(fixture.globalPath);
|
||||
return {
|
||||
guard: managedHandler(settings, 'PreToolUse'),
|
||||
audit: managedHandler(settings, 'PostToolUse'),
|
||||
failure: managedHandler(settings, 'PostToolUseFailure'),
|
||||
};
|
||||
}
|
||||
|
||||
test('Letta guard accepts active agents with native exit semantics', async () => {
|
||||
const api = await startApiServer('active');
|
||||
const fixture = await createFixture({ baseUrl: api.baseUrl });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const { guard } = await installedHandlers(fixture);
|
||||
const result = await runLettaHook(
|
||||
guard,
|
||||
JSON.stringify(officialInput(fixture, 'PreToolUse')),
|
||||
fixture,
|
||||
);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
assert.equal(result.stdout, '');
|
||||
assert.equal(api.requests.filter((request) => request.method === 'GET').length, 1);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
await api.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta guard propagates frozen and revoked states through exit code 2', async (t) => {
|
||||
for (const status of ['frozen', 'revoked']) {
|
||||
await t.test(status, async () => {
|
||||
const api = await startApiServer(status);
|
||||
const fixture = await createFixture({ baseUrl: api.baseUrl });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const { guard } = await installedHandlers(fixture);
|
||||
const result = await runLettaHook(
|
||||
guard,
|
||||
JSON.stringify(officialInput(fixture, 'PreToolUse')),
|
||||
fixture,
|
||||
);
|
||||
assert.equal(result.code, 2, result.stderr);
|
||||
assert.equal(result.stdout, '');
|
||||
assert.match(result.stderr, new RegExp(`is ${status}`, 'i'));
|
||||
} finally {
|
||||
await fixture.close();
|
||||
await api.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta audit forwards complete success and failure payloads', async () => {
|
||||
const api = await startApiServer();
|
||||
const fixture = await createFixture({ baseUrl: api.baseUrl });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const { audit, failure } = await installedHandlers(fixture);
|
||||
for (const [handler, event] of [
|
||||
[audit, 'PostToolUse'],
|
||||
[failure, 'PostToolUseFailure'],
|
||||
]) {
|
||||
const payload = {
|
||||
...officialInput(fixture, event),
|
||||
future_provider_field: { preserved: true },
|
||||
};
|
||||
const result = await runLettaHook(handler, JSON.stringify(payload), fixture);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
assert.equal(result.stdout, '');
|
||||
}
|
||||
const posts = api.requests.filter((request) => request.method === 'POST');
|
||||
assert.equal(posts.length, 2);
|
||||
assert.deepEqual(JSON.parse(posts[0].raw).payload, {
|
||||
...officialInput(fixture, 'PostToolUse'),
|
||||
future_provider_field: { preserved: true },
|
||||
});
|
||||
assert.deepEqual(JSON.parse(posts[1].raw).payload, {
|
||||
...officialInput(fixture, 'PostToolUseFailure'),
|
||||
future_provider_field: { preserved: true },
|
||||
});
|
||||
} finally {
|
||||
await fixture.close();
|
||||
await api.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta audit failures emit stderr, persist error.log, and exit 1', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const { audit } = await installedHandlers(fixture);
|
||||
const result = await runLettaHook(
|
||||
audit,
|
||||
JSON.stringify(officialInput(fixture, 'PostToolUse')),
|
||||
fixture,
|
||||
);
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /Elydora audit|fetch failed|ECONNREFUSED/i);
|
||||
assert.match(
|
||||
await readFile(path.join(fixture.agentDir, 'error.log'), 'utf-8'),
|
||||
/Elydora audit|fetch failed|ECONNREFUSED/i,
|
||||
);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta runtime artifacts use private modes on POSIX', {
|
||||
skip: process.platform === 'win32' ? 'POSIX mode bits are not authoritative on Windows' : false,
|
||||
}, async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
for (const [filePath, expected] of [
|
||||
[path.join(fixture.agentDir, 'config.json'), 0o600],
|
||||
[path.join(fixture.agentDir, 'private.key'), 0o600],
|
||||
[fixture.guardScriptPath, 0o700],
|
||||
[fixture.hookScriptPath, 0o700],
|
||||
[fixture.globalPath, 0o600],
|
||||
]) {
|
||||
assert.equal((await stat(filePath)).mode & 0o777, expected);
|
||||
}
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { lstat, mkdir, readFile, readdir, symlink, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
configModuleUrl,
|
||||
contractModuleUrl,
|
||||
createFixture,
|
||||
environment,
|
||||
installConfig,
|
||||
installationModuleUrl,
|
||||
runNode,
|
||||
sourcesModuleUrl,
|
||||
} from '../test-support/letta.mjs';
|
||||
|
||||
async function assertMissing(filePath) {
|
||||
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
|
||||
}
|
||||
|
||||
async function assertNoTransactionFiles(...directories) {
|
||||
const names = [];
|
||||
for (const directory of directories) {
|
||||
try {
|
||||
names.push(...await readdir(directory));
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') throw error;
|
||||
}
|
||||
}
|
||||
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false, names.join(', '));
|
||||
}
|
||||
|
||||
async function runPreparedInstallation(fixture, {
|
||||
failSettings = false,
|
||||
racePath,
|
||||
raceSource,
|
||||
} = {}) {
|
||||
const source = `
|
||||
import fsp from 'node:fs/promises';
|
||||
import { buildLettaGroup } from ${JSON.stringify(contractModuleUrl)};
|
||||
import { renderLettaDocument } from ${JSON.stringify(configModuleUrl)};
|
||||
import { readLettaSources } from ${JSON.stringify(sourcesModuleUrl)};
|
||||
import {
|
||||
commitLettaInstallation,
|
||||
preflightLettaInstallation,
|
||||
prepareLettaInstallation,
|
||||
} from ${JSON.stringify(installationModuleUrl)};
|
||||
|
||||
const config = JSON.parse(process.env.ELYDORA_TEST_CONFIG);
|
||||
const sources = await readLettaSources();
|
||||
const paths = await preflightLettaInstallation(config, sources);
|
||||
const rendered = renderLettaDocument(sources.global, undefined, new Map([
|
||||
['PreToolUse', buildLettaGroup(paths.guardPath)],
|
||||
['PostToolUse', buildLettaGroup(paths.auditPath)],
|
||||
['PostToolUseFailure', buildLettaGroup(paths.auditPath)],
|
||||
]));
|
||||
const prepared = await prepareLettaInstallation(config, sources, rendered);
|
||||
if (process.env.ELYDORA_TEST_RACE_PATH) {
|
||||
await fsp.writeFile(
|
||||
process.env.ELYDORA_TEST_RACE_PATH,
|
||||
process.env.ELYDORA_TEST_RACE_SOURCE,
|
||||
);
|
||||
}
|
||||
await commitLettaInstallation(prepared, async (from, to) => {
|
||||
if (process.env.ELYDORA_TEST_FAIL_SETTINGS === 'true'
|
||||
&& to === sources.global.filePath) {
|
||||
throw new Error('injected Letta settings rename failure');
|
||||
}
|
||||
await fsp.rename(from, to);
|
||||
});
|
||||
`;
|
||||
return runNode(
|
||||
['--input-type=module', '--eval', source],
|
||||
{
|
||||
...environment(fixture),
|
||||
ELYDORA_TEST_CONFIG: JSON.stringify(installConfig(fixture)),
|
||||
ELYDORA_TEST_FAIL_SETTINGS: String(failSettings),
|
||||
ELYDORA_TEST_RACE_PATH: racePath,
|
||||
ELYDORA_TEST_RACE_SOURCE: raceSource,
|
||||
},
|
||||
fixture.projectDir,
|
||||
);
|
||||
}
|
||||
|
||||
test('Letta rolls back every runtime file when the settings commit fails', async () => {
|
||||
const original = '{"theme":"dark"}\n';
|
||||
const fixture = await createFixture({ globalSettings: original });
|
||||
try {
|
||||
const result = await runPreparedInstallation(fixture, { failSettings: true });
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /injected Letta settings rename failure/i);
|
||||
assert.equal(await readFile(fixture.globalPath, 'utf-8'), original);
|
||||
for (const filePath of [
|
||||
fixture.guardScriptPath,
|
||||
fixture.hookScriptPath,
|
||||
path.join(fixture.agentDir, 'config.json'),
|
||||
path.join(fixture.agentDir, 'private.key'),
|
||||
]) await assertMissing(filePath);
|
||||
await assertNoTransactionFiles(path.dirname(fixture.globalPath), fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta aborts when a read-only project source changes after prepare', async () => {
|
||||
const original = '{"windowTitle":"before"}\n';
|
||||
const raceSource = '{"windowTitle":"after"}\n';
|
||||
const fixture = await createFixture({ projectSettings: original });
|
||||
try {
|
||||
const result = await runPreparedInstallation(fixture, {
|
||||
racePath: fixture.projectPath,
|
||||
raceSource,
|
||||
});
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /project settings changed during Letta Code installation/i);
|
||||
assert.equal(await readFile(fixture.projectPath, 'utf-8'), raceSource);
|
||||
await assertMissing(fixture.globalPath);
|
||||
for (const filePath of [
|
||||
fixture.guardScriptPath,
|
||||
fixture.hookScriptPath,
|
||||
path.join(fixture.agentDir, 'config.json'),
|
||||
path.join(fixture.agentDir, 'private.key'),
|
||||
]) await assertMissing(filePath);
|
||||
await assertNoTransactionFiles(path.dirname(fixture.globalPath), fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta rejects linked configuration directories before writes', {
|
||||
skip: process.platform === 'win32' ? 'directory symlinks require platform privileges on Windows' : false,
|
||||
}, async () => {
|
||||
const fixture = await createFixture();
|
||||
const target = path.join(fixture.rootDir, 'redirected-letta');
|
||||
await mkdir(target);
|
||||
await symlink(target, path.dirname(fixture.globalPath), 'dir');
|
||||
try {
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /global configuration directory is not a physical directory/i);
|
||||
await assertMissing(fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta rejects a settings directory at the settings file path', async () => {
|
||||
const fixture = await createFixture();
|
||||
await mkdir(fixture.globalPath, { recursive: true });
|
||||
try {
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /global settings path is not a physical file/i);
|
||||
await assertMissing(fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user