88 lines
3.2 KiB
TypeScript
88 lines
3.2 KiB
TypeScript
|
|
import assert from 'node:assert/strict';
|
||
|
|
import test from 'node:test';
|
||
|
|
import { fitTerminalExecuteResultForModel } from './terminalCompression';
|
||
|
|
import { fitLargeToolResultForModel } from './toolResultFitting';
|
||
|
|
import { ToolOutputStore } from './toolOutputStore';
|
||
|
|
import { redactSecretsForModel, redactSecretsInValueForModel } from './modelSecretRedaction';
|
||
|
|
|
||
|
|
test('redactSecretsForModel removes common terminal secrets', () => {
|
||
|
|
const input = [
|
||
|
|
'API_TOKEN=tok_live_1234567890',
|
||
|
|
'Authorization: Bearer abc.def.very-secret',
|
||
|
|
'postgres://admin:p4ssw0rd@db.internal/app',
|
||
|
|
'-----BEGIN PRIVATE KEY-----',
|
||
|
|
'super-secret-private-key-body',
|
||
|
|
'-----END PRIVATE KEY-----',
|
||
|
|
].join('\n');
|
||
|
|
const output = redactSecretsForModel(input);
|
||
|
|
|
||
|
|
assert.doesNotMatch(output, /tok_live|abc\.def|p4ssw0rd|private-key-body/);
|
||
|
|
assert.match(output, /\[REDACTED\]/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('redactSecretsInValueForModel recursively redacts tool arguments', () => {
|
||
|
|
const value = redactSecretsInValueForModel({
|
||
|
|
command: 'curl --password swordfish',
|
||
|
|
nested: [
|
||
|
|
'Authorization: Bearer secret_token_123456',
|
||
|
|
{
|
||
|
|
password: 'short',
|
||
|
|
telnetPassword: 'telnet-secret',
|
||
|
|
passphrase: 'key-passphrase',
|
||
|
|
privateKey: 'not-a-pem-but-still-private',
|
||
|
|
apiKey: 'tiny-api-key',
|
||
|
|
clientSecret: 'client-secret',
|
||
|
|
authToken: 'auth-token',
|
||
|
|
username: 'operator',
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
const serialized = JSON.stringify(value);
|
||
|
|
assert.doesNotMatch(serialized, /swordfish|secret_token|short|telnet-secret|key-passphrase|not-a-pem|tiny-api-key|client-secret|auth-token/);
|
||
|
|
assert.match(serialized, /operator/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('redactSecretsForModel redacts JSON and quoted shell assignments', () => {
|
||
|
|
const input = [
|
||
|
|
'{"password":"short value","apiKey":"tiny","username":"operator"}',
|
||
|
|
"passphrase='two words'",
|
||
|
|
'client_secret = "quoted secret"',
|
||
|
|
'PRIVATE_KEY=one-line-key',
|
||
|
|
].join('\n');
|
||
|
|
|
||
|
|
const output = redactSecretsForModel(input);
|
||
|
|
assert.doesNotMatch(output, /short value|tiny|two words|quoted secret|one-line-key/);
|
||
|
|
assert.match(output, /"username":"operator"/);
|
||
|
|
assert.equal((output.match(/\[REDACTED\]/g) ?? []).length, 5);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('terminal fitting keeps raw output in the local handle but redacts model-visible text', () => {
|
||
|
|
const store = new ToolOutputStore();
|
||
|
|
const secret = 'API_TOKEN=tok_live_1234567890';
|
||
|
|
const fitted = fitTerminalExecuteResultForModel({
|
||
|
|
stdout: `${secret}\n${'build line\n'.repeat(10_000)}`,
|
||
|
|
stderr: '',
|
||
|
|
exitCode: 1,
|
||
|
|
command: `deploy --token tok_live_1234567890`,
|
||
|
|
sessionId: 'session-1',
|
||
|
|
}, {
|
||
|
|
chatSessionId: 'chat-1',
|
||
|
|
toolOutputStore: store,
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.doesNotMatch(fitted.stdout, /tok_live/);
|
||
|
|
assert.doesNotMatch(fitted.command ?? '', /tok_live/);
|
||
|
|
assert.match(fitted.stdout, /restartPersistence=unavailable \(read before closing the app\)/);
|
||
|
|
const handle = store.listPendingHandles('chat-1')[0];
|
||
|
|
assert.match(handle.fullContent, /tok_live/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('generic tool fitting redacts short strings even when truncation is unnecessary', () => {
|
||
|
|
const fitted = fitLargeToolResultForModel({
|
||
|
|
result: { message: 'password=hunter2' },
|
||
|
|
capabilityId: 'example.read',
|
||
|
|
}) as { message: string };
|
||
|
|
|
||
|
|
assert.equal(fitted.message, 'password=[REDACTED]');
|
||
|
|
});
|