timmy-talking-turd/tests/hermes-agent-service.test.js
Timmy 0f3104ebde
All checks were successful
Quality gates / quality (pull_request) Successful in 1m21s
fix: recognize past-tense vomiting in chat safety
2026-08-20 17:14:20 +00:00

216 lines
12 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
AgentGatewayError,
buildHermesEnvironment,
createHermesAgentService,
parseHermesCliOutput,
resolveHermesAgentConfig,
runHermesCliTurn,
} from '../src/hermes-agent-service.js';
const origin = 'http://127.0.0.1:4173';
const configured = () => resolveHermesAgentConfig({
TIMMY_AGENT_ENABLED: 'true',
TIMMY_AGENT_ACCESS_TOKEN: 'test-agent-access-code-2026',
TIMMY_PUBLIC_ORIGIN: origin,
TIMMY_AGENT_WORKDIR: '/tmp/timmy-agent-workspace',
TIMMY_AGENT_TIMEOUT_MS: '45000',
});
async function rejectsStatus(fn, status) {
await assert.rejects(fn, error => error instanceof AgentGatewayError && error.status === status);
}
test('Hermes agent is disabled by default and public status exposes no secrets', () => {
const config = resolveHermesAgentConfig({});
assert.equal(config.enabled, false);
assert.equal(config.configured, false);
assert.deepEqual(config.publicStatus(), { enabled: false, configured: false, authenticated: false, mode: 'local-fallback' });
assert.doesNotMatch(JSON.stringify(config.publicStatus()), /token|workdir|command/i);
});
test('enabled agent requires a strong access token, exact origin, and absolute workdir', () => {
for (const env of [
{ TIMMY_AGENT_ENABLED: 'true', TIMMY_PUBLIC_ORIGIN: origin, TIMMY_AGENT_WORKDIR: '/tmp/agent' },
{ TIMMY_AGENT_ENABLED: 'true', TIMMY_AGENT_ACCESS_TOKEN: 'short', TIMMY_PUBLIC_ORIGIN: origin, TIMMY_AGENT_WORKDIR: '/tmp/agent' },
{ TIMMY_AGENT_ENABLED: 'true', TIMMY_AGENT_ACCESS_TOKEN: 'test-agent-access-code-2026', TIMMY_AGENT_WORKDIR: '/tmp/agent' },
{ TIMMY_AGENT_ENABLED: 'true', TIMMY_AGENT_ACCESS_TOKEN: 'test-agent-access-code-2026', TIMMY_PUBLIC_ORIGIN: origin, TIMMY_AGENT_WORKDIR: 'relative' },
]) assert.equal(resolveHermesAgentConfig(env).configured, false);
assert.equal(configured().configured, true);
});
test('unlock is exact-origin, rate bounded, and returns only an opaque browser credential', async () => {
let serial = 0;
const service = createHermesAgentService({ config: configured(), randomToken: () => `opaque-${++serial}` });
await rejectsStatus(() => service.unlock({ origin: 'https://evil.example', accessCode: 'test-agent-access-code-2026' }), 403);
await rejectsStatus(() => service.unlock({ origin, accessCode: 'wrong-access-code-value' }), 401);
const result = await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
assert.equal(result.cookieToken, 'opaque-1');
assert.deepEqual(result.public, { enabled: true, configured: true, authenticated: true, mode: 'hermes-agent' });
assert.doesNotMatch(JSON.stringify(result), /test-agent-access|session_id|workdir/i);
});
test('unlock brute-force attempts are bounded before a correct code is accepted', async () => {
const service = createHermesAgentService({ config: { ...configured(), maxUnlockAttemptsPerMinute: 3 }, randomToken: () => 'unused-cookie' });
for (let attempt = 0; attempt < 3; attempt += 1) await rejectsStatus(() => service.unlock({ origin, accessCode: `wrong-access-code-${attempt}` }), 401);
await rejectsStatus(() => service.unlock({ origin, accessCode: 'test-agent-access-code-2026' }), 429);
});
test('opaque browser credential generation never evicts an active conversation on collision', async () => {
const tokens = ['collision-cookie', 'collision-cookie', 'fresh-cookie'];
const service = createHermesAgentService({ config: configured(), randomToken: () => tokens.shift(), runTurn: async () => ({ reply: 'ok', sessionId: 'private-session' }) });
const first = await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
await service.chat({ origin, cookieToken: first.cookieToken, payload: { message: 'first turn', ledger: [] } });
const second = await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
assert.equal(first.cookieToken, 'collision-cookie');
assert.equal(second.cookieToken, 'fresh-cookie');
assert.equal(service.status(first.cookieToken).authenticated, true);
});
test('session capacity never evicts an active authenticated user', async () => {
let clock = 0;
const tokens = ['one-cookie', 'two-cookie', 'three-cookie'];
const service = createHermesAgentService({ config: { ...configured(), maxSessions: 1 }, randomToken: () => tokens.shift(), now: () => clock });
const first = await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
await rejectsStatus(() => service.unlock({ origin, accessCode: 'test-agent-access-code-2026' }), 429);
assert.equal(service.status(first.cookieToken).authenticated, true);
clock = 24 * 60 * 60 * 1000 + 1;
const replacement = await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
assert.equal(replacement.cookieToken, 'two-cookie');
});
test('chat rejects unauthenticated, cross-origin, oversized, and browser-controlled agent policy', async () => {
const service = createHermesAgentService({ config: configured(), randomToken: () => 'browser-cookie', runTurn: async () => ({ reply: 'ok', sessionId: 'private' }) });
await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
await rejectsStatus(() => service.chat({ origin, cookieToken: '', payload: { message: 'hello', ledger: [] } }), 401);
await rejectsStatus(() => service.chat({ origin: 'https://evil.example', cookieToken: 'browser-cookie', payload: { message: 'hello', ledger: [] } }), 403);
await rejectsStatus(() => service.chat({ origin, cookieToken: 'browser-cookie', payload: { message: 'x'.repeat(4001), ledger: [] } }), 413);
for (const forbidden of ['sessionId', 'toolsets', 'model', 'provider', 'systemPrompt']) {
await rejectsStatus(() => service.chat({ origin, cookieToken: 'browser-cookie', payload: { message: 'hello', ledger: [], [forbidden]: 'attacker-controlled' } }), 400);
}
});
test('chat keeps Hermes session IDs server-side, resumes continuity, and strips photos from ledger context', async () => {
const calls = [];
const service = createHermesAgentService({
config: configured(),
randomToken: () => 'browser-cookie',
runTurn: async input => {
calls.push(input);
return calls.length === 1
? { reply: 'I see one confirmed log.', sessionId: 'hermes-private-session' }
: { reply: 'We were discussing that confirmed log.', sessionId: 'hermes-private-session' };
},
});
await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
const ledger = [{ id: 'one', occurredAt: '2026-08-20T12:00:00Z', bristolType: 4, color: 'brown', urgency: 1, discomfort: 0, note: 'normal', photoDataUrl: 'data:image/jpeg;base64,PRIVATE' }];
const first = await service.chat({ origin, cookieToken: 'browser-cookie', payload: { message: 'What changed?', ledger } });
const second = await service.chat({ origin, cookieToken: 'browser-cookie', payload: { message: 'What were we discussing?', ledger } });
assert.equal(calls[0].hermesSessionId, null);
assert.equal(calls[1].hermesSessionId, 'hermes-private-session');
assert.match(calls[0].prompt, /observable patterns/i);
assert.match(calls[0].prompt, /confirmed ledger context/i);
assert.doesNotMatch(calls[0].prompt, /PRIVATE|photoDataUrl/);
assert.deepEqual(first, { reply: 'I see one confirmed log.', connected: true });
assert.deepEqual(second, { reply: 'We were discussing that confirmed log.', connected: true });
assert.doesNotMatch(JSON.stringify([first, second]), /hermes-private-session/);
});
test('authoritative chat service intercepts urgent message and ledger symptoms before Hermes', async () => {
const calls = [];
const service = createHermesAgentService({
config: configured(),
randomToken: () => 'safety-cookie',
runTurn: async input => { calls.push(input); return { reply: 'unsafe upstream reply', sessionId: 'unsafe-session' }; },
});
await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
const messageResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: { message: 'I have rectal bleeding', ledger: [] } });
const pastTenseVomitingResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: { message: 'I threw up', ledger: [] } });
const slangVomitingResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: { message: 'I barfed', ledger: [] } });
const possessiveIdiomResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: { message: 'She tosses her cookies', ledger: [] } });
const thirdPersonVomitingResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: { message: 'She hurls', ledger: [] } });
const ledgerResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: {
message: 'What does my journal show?',
ledger: [{ bristolType: 4, color: 'brown', note: '', symptoms: { cannotPassGas: true } }],
} });
const noteResult = await service.chat({ origin, cookieToken: 'safety-cookie', payload: {
message: 'Summarize this entry',
ledger: [{ bristolType: 4, color: 'brown', note: 'unable to pass gas', symptoms: {} }],
} });
assert.equal(calls.length, 0);
for (const result of [messageResult, pastTenseVomitingResult, slangVomitingResult, possessiveIdiomResult, thirdPersonVomitingResult, ledgerResult, noteResult]) {
assert.equal(result.safetyOverride, true);
assert.match(result.reply, /medical help/i);
assert.doesNotMatch(result.reply, /unsafe upstream/i);
}
});
test('Hermes CLI parser accepts quiet output, strips control sequences, and rejects leaked metadata', () => {
const parsed = parseHermesCliOutput('\u001b[32msession_id: 20260820_abcd\u001b[0m\nA concise answer.');
assert.deepEqual(parsed, { sessionId: '20260820_abcd', reply: 'A concise answer.' });
assert.throws(() => parseHermesCliOutput('answer only'), /session metadata/i);
assert.throws(() => parseHermesCliOutput('session_id: private\nsession_id: leaked'), /unsafe/i);
});
test('Hermes CLI adapter combines stderr session metadata with stdout answer', async () => {
let captured;
const result = await runHermesCliTurn({
prompt: 'hello',
hermesSessionId: null,
config: configured(),
execImpl: async (command, args, options) => {
captured = { command, args, options };
return { stdout: 'bounded answer\n', stderr: '\u001b[33mwarning\u001b[0m\nsession_id: 20260820_fixture\n' };
},
});
assert.deepEqual(result, { sessionId: '20260820_fixture', reply: 'bounded answer' });
assert.equal(captured.command, 'hermes');
assert.equal(captured.args.includes('--in'), true);
assert.equal(captured.options.cwd, '/tmp/timmy-agent-workspace');
});
test('Hermes child receives only an explicit non-secret environment allowlist', () => {
const childEnv = buildHermesEnvironment({
HOME: '/root',
PATH: '/usr/bin',
HERMES_HOME: '/root/.hermes/profiles/timmyapp',
LANG: 'C.UTF-8',
TIMMY_AGENT_ACCESS_TOKEN: 'test-must-not-reach-child',
GITEA_TOKEN: 'test-must-not-reach-child',
OPENAI_API_KEY: 'test-must-not-reach-child',
RANDOM_PRIVATE_VALUE: 'test-must-not-reach-child',
});
assert.deepEqual(childEnv, {
HOME: '/root',
PATH: '/usr/bin',
HERMES_HOME: '/root/.hermes/profiles/timmyapp',
LANG: 'C.UTF-8',
TIMMY_AGENT_BROWSER_REQUEST: '1',
});
});
test('upstream failures and concurrency fail closed without leaking process detail', async () => {
let release;
const runTurn = () => new Promise(resolve => { release = resolve; });
const service = createHermesAgentService({ config: configured(), randomToken: () => 'browser-cookie', runTurn });
await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
const active = service.chat({ origin, cookieToken: 'browser-cookie', payload: { message: 'first', ledger: [] } });
await new Promise(resolve => setImmediate(resolve));
await rejectsStatus(() => service.chat({ origin, cookieToken: 'browser-cookie', payload: { message: 'second', ledger: [] } }), 429);
release({ reply: 'done', sessionId: 'private-session' });
await active;
const broken = createHermesAgentService({ config: configured(), randomToken: () => 'broken-cookie', runTurn: async () => { throw new Error('spawn /root/.hermes/auth.json SECRET'); } });
await broken.unlock({ origin, accessCode: 'test-agent-access-code-2026' });
await assert.rejects(() => broken.chat({ origin, cookieToken: 'broken-cookie', payload: { message: 'hello', ledger: [] } }), error => {
assert.equal(error.status, 503);
assert.match(error.message, /temporarily unavailable/i);
assert.doesNotMatch(error.message, /auth\.json|SECRET|spawn/);
return true;
});
});