Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lib/distill/prompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { checkExitOf, verdictOf } from '../checks.js';
import { withoutSecrets } from '../redact.js';
import { checkSourceOf } from '../reuse.js';
import { TRIGGERS } from './candidate.js';
import { DIFF_OMITTED } from './project-diff.js';
import { DIFF_OMITTED, diffFieldsOf } from './project-diff.js';

const LIST_FIELDS = ['strategy', 'validation', 'signals', 'preconditions'];
const ARGUMENT_FIELDS = ['title', 'summary', ...LIST_FIELDS];
Expand Down Expand Up @@ -94,6 +94,7 @@ function withinBodyLimit(body) {
}

export function distillBody({ platform, sessionId, candidate, args, usage, trigger = TRIGGERS.SESSION_END }) {
const { diff, ...project } = candidate.project ?? {};
return withinBodyLimit({
platform,
trigger,
Expand All @@ -103,7 +104,8 @@ export function distillBody({ platform, sessionId, candidate, args, usage, trigg
final_answer: candidate.answer,
...args,
execution: { trace: traceOf(candidate) },
...(candidate.project && { project: candidate.project }),
// A candidate can hold a snapshot written before the current secret filter.
...(candidate.project && { project: { ...project, ...(typeof diff === 'string' && diffFieldsOf(diff)) } }),
persist: true,
publish: false,
...(usage ? { distill_usage: usage } : {}),
Expand Down
9 changes: 8 additions & 1 deletion lib/redact.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 EvoMap

const JSON_SECRET_PATTERNS = [
/("(?:api[_-]?key|secret|token|passw(?:or)?d|pwd|access[_-]?key|private[_-]?key|client[_-]?secret|[a-z][a-z0-9_]*_(?:key|token|secret|password|passwd|pwd|credentials?))"\s*:\s*")(?:\\[^\r\n]|[^"\\\r\n])+(")/gi,
// Match camelCase suffixes without treating words like "monkey" as credentials.
/("[A-Za-z][A-Za-z0-9_-]*(?:Key|Token|Secret|Password|Passwd|Pwd|Credentials?)"\s*:\s*")(?:\\[^\r\n]|[^"\\\r\n])+(")/g,
];

const SECRET_PATTERNS = [
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g,
/\b(?:sk|pk|rk)-(?:ant-|proj-|live-|test-)?[A-Za-z0-9_-]{16,}/g,
Expand All @@ -24,7 +30,8 @@ export const REDACTED = '[redacted]';

export function withoutSecrets(text) {
if (typeof text !== 'string') return '';
return SECRET_PATTERNS.reduce((cleaned, pattern) => cleaned.replace(pattern, REDACTED), text);
const redactedJson = JSON_SECRET_PATTERNS.reduce((cleaned, pattern) => cleaned.replace(pattern, '$1' + REDACTED + '$2'), text);
return SECRET_PATTERNS.reduce((cleaned, pattern) => cleaned.replace(pattern, REDACTED), redactedJson);
}

export function containsSecret(text) {
Expand Down
12 changes: 12 additions & 0 deletions test/distill-prompt.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,15 @@ test('an earlier unknown check stays unknown in the model record and supplies no
const body = distillBody({ platform: 'codex', sessionId: 's1', candidate, args: {} });
assert.deepEqual(body.execution.trace, [{ command: 'npm test', exit: 0, source: 'reported' }]);
});

test('the Proxy body omits a stored snapshot with a quoted JSON credential', () => {
const project = {
root: '/project', head: 'abc123',
diff: 'diff --git a/settings.json b/settings.json\n+{"refreshToken":"opaque-example-123456789"}\n',
};
const candidate = { checks: [], project };
const body = distillBody({ platform: 'codex', sessionId: 's1', candidate, args: {} });
assert.deepEqual(body.project, { root: project.root, head: project.head, diff_omitted: 'secret' });
assert.doesNotMatch(JSON.stringify(body), /opaque-example/);
assert.match(candidate.project.diff, /opaque-example/);
});
16 changes: 14 additions & 2 deletions test/runtime/project.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { join } from 'node:path';
import { test } from 'node:test';

import { DIFF_MAX_BYTES } from '../../lib/distill/project-diff.js';
import { distillBody } from '../../lib/distill/prompt.js';
import { projectSnapshotOf } from '../../lib/runtime/project.js';

const git = (cwd, ...args) => execFileSync('git', ['-c', 'user.email=t@t', '-c', 'user.name=t', ...args], { cwd, encoding: 'utf8' });
Expand All @@ -26,13 +27,16 @@ test('a work tree snapshot is its root, HEAD and a diff that rebuilds it on HEAD
writeFileSync(join(dir, 'a.js'), 'export const a = 2;\n');
mkdirSync(join(dir, 'lib'));
writeFileSync(join(dir, 'lib', 'new.js'), 'export const b = 3;\n');
writeFileSync(join(dir, 'settings.json'), '{"maxTokens":"4096","monkey":"example"}\n');
mkdirSync(join(dir, 'node_modules'));
writeFileSync(join(dir, 'node_modules', 'dep.js'), 'ignored\n');
const statusBefore = git(dir, 'status', '--porcelain');
const snapshot = await projectSnapshotOf().snapshot(join(dir, 'lib'));
assert.equal(git(dir, 'status', '--porcelain'), statusBefore);
assert.deepEqual([snapshot.root, snapshot.head], [git(dir, 'rev-parse', '--show-toplevel').trim(), git(dir, 'rev-parse', 'HEAD').trim()]);
assert.doesNotMatch(snapshot.diff, /node_modules/);
assert.match(snapshot.diff, /"maxTokens":"4096"/);
assert.match(snapshot.diff, /"monkey":"example"/);

const copy = mkdtempSync(join(tmpdir(), 'evolver-copy-'));
t.after(() => rmSync(copy, { recursive: true, force: true }));
Expand All @@ -48,12 +52,20 @@ test('a work tree snapshot is its root, HEAD and a diff that rebuilds it on HEAD
});

test('a work tree whose diff holds a secret, or touches a file keys are kept in, sends no diff at all', async (t) => {
for (const [file, text] of [['credentials.txt', 'harmless\n'], ['a.js', 'PRIVATE_CREDENTIAL=unique-secret-123\n']]) {
for (const [file, text] of [
['credentials.txt', 'harmless\n'],
['a.js', 'PRIVATE_CREDENTIAL=unique-secret-123\n'],
['settings.json', '{"token":"opaque-example-123456789"}\n'],
['deploy.json', '{"DEPLOY_TOKEN":"opaque-example-123456789"}\n'],
['settings.json', '{"accessToken":"opaque-example-123456789"}\n'],
]) {
const dir = repo(t);
writeFileSync(join(dir, file), text);
const snapshot = await projectSnapshotOf().snapshot(dir);
const body = distillBody({ platform: 'codex', sessionId: 's1', candidate: { checks: [], project: snapshot }, args: {} });
assert.doesNotMatch(JSON.stringify(body.project), /unique-secret-123|harmless|opaque-example/);
assert.deepEqual([snapshot.diff, snapshot.diff_omitted], [undefined, 'secret'], file);
assert.doesNotMatch(JSON.stringify(snapshot), /unique-secret-123|harmless/);
assert.doesNotMatch(JSON.stringify(snapshot), /unique-secret-123|harmless|opaque-example/);
}
});

Expand Down
Loading