mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Adds an exact-key tier (ALLOWED_ENV_KEYS) beside ALLOWED_ENV_PREFIXES in schemas.ts, admitting CLAUDE_CONFIG_DIR so a case can run on a separate Claude subscription (client-billed accounts). Exact match only: other CLAUDE_* keys and near-misses like CLAUDE_CONFIG_DIR_EXTRA stay rejected, blocked keys stay blocked. The key also survives getEnvOverridesForPersist() (a path, not a secret; dropping it would silently switch a rebuilt session back to the default account after a reboot). Docs cover the transcript caveat: a relocated config dir writes transcripts outside ~/.claude/projects, so response viewer / subagent windows / ultracode / Read My Mind go blind for that session unless projects is symlinked back into the shared tree. Design and spec contributed by @jordan8037310 in #255. Closes #255. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
80 lines
2.7 KiB
TypeScript
80 lines
2.7 KiB
TypeScript
/**
|
|
* @fileoverview envOverrides allowlist: exact-key entries alongside the prefixes.
|
|
*
|
|
* CLAUDE_CONFIG_DIR (#255) relocates the Claude CLI's user config (credentials,
|
|
* settings, stats) so a case can run on a separate Claude subscription. It starts
|
|
* with `CLAUDE_`, not `CLAUDE_CODE_`, so the prefix allowlist alone rejects it;
|
|
* ALLOWED_ENV_KEYS in schemas.ts admits it as an exact match. These tests pin:
|
|
* the exact key is accepted, near-misses stay rejected (no accidental prefix
|
|
* widening), blocked keys stay blocked, and the key survives persist filtering
|
|
* (losing it on restart would silently move a session back to the default account).
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { CreateSessionSchema } from '../src/web/schemas.js';
|
|
import { Session } from '../src/session.js';
|
|
|
|
describe('envOverrides exact-key allowlist', () => {
|
|
it('accepts CLAUDE_CONFIG_DIR', () => {
|
|
const parsed = CreateSessionSchema.parse({
|
|
workingDir: '/tmp',
|
|
mode: 'claude',
|
|
envOverrides: { CLAUDE_CONFIG_DIR: '/home/user/.claude-clients/acme' },
|
|
});
|
|
expect(parsed.envOverrides).toEqual({ CLAUDE_CONFIG_DIR: '/home/user/.claude-clients/acme' });
|
|
});
|
|
|
|
it('accepts CLAUDE_CONFIG_DIR alongside prefix-allowlisted keys', () => {
|
|
const parsed = CreateSessionSchema.parse({
|
|
workingDir: '/tmp',
|
|
mode: 'claude',
|
|
envOverrides: {
|
|
CLAUDE_CONFIG_DIR: '/home/user/.claude-clients/acme',
|
|
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
|
|
},
|
|
});
|
|
expect(Object.keys(parsed.envOverrides ?? {})).toHaveLength(2);
|
|
});
|
|
|
|
it('rejects other CLAUDE_-prefixed keys (exact match only, no prefix widening)', () => {
|
|
expect(() =>
|
|
CreateSessionSchema.parse({
|
|
workingDir: '/tmp',
|
|
envOverrides: { CLAUDE_SOMETHING_ELSE: 'x' },
|
|
})
|
|
).toThrow();
|
|
expect(() =>
|
|
CreateSessionSchema.parse({
|
|
workingDir: '/tmp',
|
|
envOverrides: { CLAUDE_CONFIG_DIR_EXTRA: '/tmp/x' },
|
|
})
|
|
).toThrow();
|
|
});
|
|
|
|
it('still blocks security-sensitive keys', () => {
|
|
for (const key of ['PATH', 'LD_PRELOAD', 'NODE_OPTIONS', 'CODEMAN_MUX_NAME']) {
|
|
expect(() =>
|
|
CreateSessionSchema.parse({
|
|
workingDir: '/tmp',
|
|
envOverrides: { [key]: 'x' },
|
|
})
|
|
).toThrow();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('CLAUDE_CONFIG_DIR persistence', () => {
|
|
it('survives the state.json persist filter (path, not a secret)', () => {
|
|
const session = new Session({
|
|
workingDir: '/tmp',
|
|
envOverrides: {
|
|
CLAUDE_CONFIG_DIR: '/home/user/.claude-clients/acme',
|
|
OPENCODE_API_KEY: 'secret-must-not-persist',
|
|
},
|
|
});
|
|
expect(session.getEnvOverridesForPersist()).toEqual({
|
|
CLAUDE_CONFIG_DIR: '/home/user/.claude-clients/acme',
|
|
});
|
|
});
|
|
});
|