Built-in design generation now runs as an agentic MCP tool-loop (reusing the agent-rs BuiltInProvider), gated behind OPENPENCIL_DESIGN_AGENT_LOOP / the Settings experimental toggle; the orchestrator stays the default. - design-agent system prompt + in-process design toolset (parity-locked with the MCP surface) + flag-gated Intent::Design routing - spawn_agents execution as sequential sub-loops + live creation-mode badges (per-agent glow + 'N/M designing...' header) - new MCP tools: get_guidelines, ToolSearch, get_screenshot, get_editor_state, export_nodes, spawn_agents; style-guide local audit - #27 AI panel restyle: rounded tool cards + green check-rings, gray user bubbles, model-pill bottom toolbar, header, empty-state pills, the PARALLEL AGENTS (agent_team_size) 1x-6x chip dropdown - multi-chat tabs: ChatSessions model (Deref-to-active) + tab row UI (switch / close / + / Cmd+T) with each run bound to its tab Large checkpoint commit spanning the working tree (Rust shell crates).
158 lines
5.9 KiB
TypeScript
158 lines
5.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { extractCodexConfigEnvKeys, filterCodexEnv } from '../utils/codex-client';
|
|
import { SENSITIVE_LOG_PATTERN, ALLOWED_MEDIA_TYPES, resolveMediaExtension } from '../api/ai/chat';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 1. Codex client env allowlist
|
|
// ---------------------------------------------------------------------------
|
|
describe('codex client env allowlist', () => {
|
|
it('should strip dangerous env vars', () => {
|
|
const env = {
|
|
PATH: '/usr/bin',
|
|
HOME: '/home/user',
|
|
AWS_SECRET_KEY: 'supersecret',
|
|
DATABASE_URL: 'postgres://...',
|
|
ANTHROPIC_API_KEY: 'sk-ant-xxx',
|
|
GITHUB_TOKEN: 'ghp_xxx',
|
|
};
|
|
const filtered = filterCodexEnv(env);
|
|
expect(filtered).not.toHaveProperty('AWS_SECRET_KEY');
|
|
expect(filtered).not.toHaveProperty('DATABASE_URL');
|
|
expect(filtered).not.toHaveProperty('ANTHROPIC_API_KEY');
|
|
expect(filtered).not.toHaveProperty('GITHUB_TOKEN');
|
|
});
|
|
|
|
it('should keep PATH, HOME, and shell vars', () => {
|
|
const env = {
|
|
PATH: '/usr/bin',
|
|
HOME: '/home/user',
|
|
TERM: 'xterm-256color',
|
|
LANG: 'en_US.UTF-8',
|
|
SHELL: '/bin/zsh',
|
|
TMPDIR: '/tmp',
|
|
};
|
|
const filtered = filterCodexEnv(env);
|
|
expect(filtered.PATH).toBe('/usr/bin');
|
|
expect(filtered.HOME).toBe('/home/user');
|
|
expect(filtered.TERM).toBe('xterm-256color');
|
|
expect(filtered.LANG).toBe('en_US.UTF-8');
|
|
expect(filtered.SHELL).toBe('/bin/zsh');
|
|
expect(filtered.TMPDIR).toBe('/tmp');
|
|
});
|
|
|
|
it('should keep OPENAI_* and CODEX_* vars', () => {
|
|
const env = {
|
|
PATH: '/usr/bin',
|
|
HOME: '/home/user',
|
|
OPENAI_API_KEY: 'sk-openai-xxx',
|
|
OPENAI_ORG_ID: 'org-xxx',
|
|
CODEX_TOKEN: 'codex-xxx',
|
|
CODEX_SANDBOX: 'read-only',
|
|
};
|
|
const filtered = filterCodexEnv(env);
|
|
expect(filtered.OPENAI_API_KEY).toBe('sk-openai-xxx');
|
|
expect(filtered.OPENAI_ORG_ID).toBe('org-xxx');
|
|
expect(filtered.CODEX_TOKEN).toBe('codex-xxx');
|
|
expect(filtered.CODEX_SANDBOX).toBe('read-only');
|
|
});
|
|
|
|
it('should keep custom provider env keys declared in codex config', () => {
|
|
const env = {
|
|
PATH: '/usr/bin',
|
|
HOME: '/home/user',
|
|
agw_CODE: 'sk-agw-xxx',
|
|
PACKYCODE: 'sk-packy-xxx',
|
|
OTHER_SECRET: 'bad',
|
|
};
|
|
const extraAllowed = extractCodexConfigEnvKeys(`
|
|
[model_providers.agw]
|
|
env_key = "agw_CODE"
|
|
|
|
[model_providers.packycode]
|
|
env_key = "PACKYCODE"
|
|
`);
|
|
const filtered = filterCodexEnv(env, extraAllowed);
|
|
|
|
expect(filtered.agw_CODE).toBe('sk-agw-xxx');
|
|
expect(filtered.PACKYCODE).toBe('sk-packy-xxx');
|
|
expect(filtered).not.toHaveProperty('OTHER_SECRET');
|
|
});
|
|
|
|
it('should not leak vars with similar prefixes', () => {
|
|
const env = {
|
|
PATH: '/usr/bin',
|
|
HOME: '/home/user',
|
|
OPENAI_API_KEY: 'ok',
|
|
OPENAI_COMPAT: 'ok',
|
|
OPEN_SECRET: 'bad',
|
|
CODEX_MODE: 'ok',
|
|
CODE_SECRET: 'bad',
|
|
};
|
|
const filtered = filterCodexEnv(env);
|
|
expect(filtered).not.toHaveProperty('OPEN_SECRET');
|
|
expect(filtered).not.toHaveProperty('CODE_SECRET');
|
|
expect(filtered.OPENAI_API_KEY).toBe('ok');
|
|
expect(filtered.CODEX_MODE).toBe('ok');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 2. Debug tail sanitization
|
|
// ---------------------------------------------------------------------------
|
|
describe('debug tail sanitization', () => {
|
|
it('should match ANTHROPIC_API_KEY leak', () => {
|
|
expect(SENSITIVE_LOG_PATTERN.test('ANTHROPIC_API_KEY=sk-ant-abc123')).toBe(true);
|
|
});
|
|
|
|
it('should match Authorization Bearer header', () => {
|
|
expect(SENSITIVE_LOG_PATTERN.test('Authorization: Bearer token123')).toBe(true);
|
|
expect(SENSITIVE_LOG_PATTERN.test('authorization: Bearer xyz')).toBe(true);
|
|
});
|
|
|
|
it('should match api_key and api-key patterns', () => {
|
|
expect(SENSITIVE_LOG_PATTERN.test('api_key=secret')).toBe(true);
|
|
expect(SENSITIVE_LOG_PATTERN.test('api-key: secret')).toBe(true);
|
|
expect(SENSITIVE_LOG_PATTERN.test('apikey=secret')).toBe(true);
|
|
});
|
|
|
|
it('should NOT match normal log lines', () => {
|
|
expect(SENSITIVE_LOG_PATTERN.test('Using API endpoint https://api.anthropic.com')).toBe(false);
|
|
expect(SENSITIVE_LOG_PATTERN.test('Model: claude-sonnet-4-5-20250929')).toBe(false);
|
|
expect(SENSITIVE_LOG_PATTERN.test('Request completed in 1200ms')).toBe(false);
|
|
expect(SENSITIVE_LOG_PATTERN.test('Connecting to upstream server...')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 3. Media type allowlist
|
|
// ---------------------------------------------------------------------------
|
|
describe('media type allowlist', () => {
|
|
it('should allow standard image types', () => {
|
|
expect(ALLOWED_MEDIA_TYPES.has('image/png')).toBe(true);
|
|
expect(ALLOWED_MEDIA_TYPES.has('image/jpeg')).toBe(true);
|
|
expect(ALLOWED_MEDIA_TYPES.has('image/gif')).toBe(true);
|
|
expect(ALLOWED_MEDIA_TYPES.has('image/webp')).toBe(true);
|
|
});
|
|
|
|
it('should reject non-image types', () => {
|
|
expect(ALLOWED_MEDIA_TYPES.has('image/svg+xml')).toBe(false);
|
|
expect(ALLOWED_MEDIA_TYPES.has('application/pdf')).toBe(false);
|
|
expect(ALLOWED_MEDIA_TYPES.has('text/html')).toBe(false);
|
|
});
|
|
|
|
it('should resolve extensions correctly', () => {
|
|
expect(resolveMediaExtension('image/png')).toBe('png');
|
|
expect(resolveMediaExtension('image/jpeg')).toBe('jpeg');
|
|
expect(resolveMediaExtension('image/gif')).toBe('gif');
|
|
expect(resolveMediaExtension('image/webp')).toBe('webp');
|
|
});
|
|
|
|
it('should fall back to png for disallowed types', () => {
|
|
expect(resolveMediaExtension('image/x-sh')).toBe('png');
|
|
expect(resolveMediaExtension('image/svg+xml')).toBe('png');
|
|
expect(resolveMediaExtension('application/pdf')).toBe('png');
|
|
expect(resolveMediaExtension('text/html')).toBe('png');
|
|
expect(resolveMediaExtension('')).toBe('png');
|
|
});
|
|
});
|