Skip to content
Closed
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
7 changes: 1 addition & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,6 @@ bun run cli workflow run implement --branch feature-auth "Add auth"
# Opt out of isolation (run in live checkout)
bun run cli workflow run quick-fix --no-worktree "Fix typo"

# Grant env-leak-gate consent during auto-registration (for repos whose .env
# contains sensitive keys). Audit-logged with actor: 'user-cli'.
bun run cli workflow run plan --cwd /path/to/leaky/repo --allow-env-keys "..."

# Show running workflows
bun run cli workflow status

Expand Down Expand Up @@ -768,8 +764,7 @@ Pattern: Use `classifyIsolationError()` (from `@archon/isolation`) to map git er

**Codebases:**
- `GET /api/codebases` / `GET /api/codebases/:id` - List / fetch codebases
- `POST /api/codebases` - Register a codebase (clone or local path); body accepts `allowEnvKeys` for the env-leak gate
- `PATCH /api/codebases/:id` - Flip the `allow_env_keys` consent bit; body: `{ allowEnvKeys: boolean }`. Audit-logged at `warn` level on every grant/revoke (`env_leak_consent_granted` / `env_leak_consent_revoked`) with `codebaseId`, `path`, `files`, `keys`, `scanStatus`, `actor`
- `POST /api/codebases` - Register a codebase (clone or local path); rejects repos with sensitive keys in auto-loaded `.env` files (no bypass)
- `DELETE /api/codebases/:id` - Delete a codebase and clean up resources
- `GET /api/codebases/:id/env` - List env var keys for a codebase (never returns values)
- `PUT /api/codebases/:id/env` / `DELETE /api/codebases/:id/env/:key` - Upsert / delete a single codebase env var
Expand Down
6 changes: 0 additions & 6 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,6 @@ Options:
--json Output machine-readable JSON (for workflow list)
--workflow <name> Workflow to run for 'continue' (default: archon-assist)
--no-context Skip context injection for 'continue'
--allow-env-keys Grant env-key consent during auto-registration
(bypasses the env-leak gate for this codebase;
logs an audit entry)
--port <port> Override server port for 'serve' (default: 3090)
--download-only Download web UI without starting the server

Expand Down Expand Up @@ -207,7 +204,6 @@ async function main(): Promise<number> {
reason: { type: 'string' },
workflow: { type: 'string' },
'no-context': { type: 'boolean' },
'allow-env-keys': { type: 'boolean' },
port: { type: 'string' },
'download-only': { type: 'boolean' },
},
Expand All @@ -231,7 +227,6 @@ async function main(): Promise<number> {
const resumeFlag = values.resume as boolean | undefined;
const spawnFlag = values.spawn as boolean | undefined;
const jsonFlag = values.json as boolean | undefined;
const allowEnvKeysFlag = values['allow-env-keys'] as boolean | undefined;

// Handle help flag
if (values.help) {
Expand Down Expand Up @@ -344,7 +339,6 @@ async function main(): Promise<number> {
fromBranch,
noWorktree,
resume: resumeFlag,
allowEnvKeys: allowEnvKeysFlag,
quiet: values.quiet as boolean | undefined,
verbose: values.verbose as boolean | undefined,
};
Expand Down
4 changes: 1 addition & 3 deletions packages/cli/src/commands/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,6 @@ export interface WorkflowRunOptions {
noWorktree?: boolean;
resume?: boolean;
codebaseId?: string; // Passed by resume/approve to skip path-based lookup
/** When true, skip the env-leak-gate during auto-registration. */
allowEnvKeys?: boolean;
quiet?: boolean;
verbose?: boolean;
/** Platform conversation ID (e.g. `cli-{ts}-{rand}`), NOT a DB UUID. */
Expand Down Expand Up @@ -325,7 +323,7 @@ export async function workflowRunCommand(
const repoRoot = await git.findRepoRoot(cwd);
if (repoRoot) {
try {
const result = await registerRepository(repoRoot, options.allowEnvKeys, 'register-cli');
const result = await registerRepository(repoRoot);
codebase = await codebaseDb.getCodebase(result.codebaseId);
if (!result.alreadyExisted) {
getLog().info({ name: result.name }, 'cli.codebase_auto_registered');
Expand Down
33 changes: 0 additions & 33 deletions packages/core/src/config/config-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,6 @@ function getLog(): ReturnType<typeof createLogger> {
return cachedLog;
}

/**
* Tracks which env-leak-gate-disabled sources have already warned in this
* process. `loadConfig()` is called once per pre-spawn check (per workflow
* step), so without this guard the warn would flood logs and break alert
* rate-limiting downstream.
*/
const envLeakGateDisabledWarnedSources = new Set<'global_config' | 'repo_config'>();
function warnEnvLeakGateDisabledOnce(source: 'global_config' | 'repo_config'): void {
if (envLeakGateDisabledWarnedSources.has(source)) return;
envLeakGateDisabledWarnedSources.add(source);
getLog().warn({ source }, 'env_leak_gate_disabled');
}

// Test-only: reset the warn-once state so unit tests can re-trigger the log.
export function resetEnvLeakGateWarnedSourcesForTests(): void {
envLeakGateDisabledWarnedSources.clear();
}

/**
* Parse YAML using Bun's native YAML parser
*/
Expand Down Expand Up @@ -216,7 +198,6 @@ function getDefaults(): MergedConfig {
loadDefaultCommands: true,
loadDefaultWorkflows: true,
},
allowTargetRepoKeys: false,
};
}

Expand Down Expand Up @@ -321,12 +302,6 @@ function mergeGlobalConfig(defaults: MergedConfig, global: GlobalConfig): Merged
result.concurrency.maxConversations = global.concurrency.maxConversations;
}

// Env-leak gate bypass (global)
if (global.allow_target_repo_keys === true) {
result.allowTargetRepoKeys = true;
warnEnvLeakGateDisabledOnce('global_config');
}

return result;
}

Expand Down Expand Up @@ -400,14 +375,6 @@ function mergeRepoConfig(merged: MergedConfig, repo: RepoConfig): MergedConfig {
result.envVars = { ...result.envVars, ...repo.env };
}

// Repo-level env-leak gate override (wins over global)
if (repo.allow_target_repo_keys !== undefined) {
result.allowTargetRepoKeys = repo.allow_target_repo_keys;
if (repo.allow_target_repo_keys) {
warnEnvLeakGateDisabledOnce('repo_config');
}
}

return result;
}

Expand Down
28 changes: 0 additions & 28 deletions packages/core/src/config/config-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,6 @@ export interface GlobalConfig {
*/
maxConversations?: number;
};

/**
* Bypass the env-leak gate globally. When true, Archon will not refuse to
* register or spawn subprocesses for codebases whose auto-loaded .env files
* contain sensitive keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc).
*
* WARNING: Weakens the env-leak gate. Keys in the target repo's .env will
* be auto-loaded by Bun subprocesses (Claude/Codex) and bypass Archon's
* env allowlist. Use only on trusted machines.
*
* YAML key: `allow_target_repo_keys`
* @default false
*/
allow_target_repo_keys?: boolean;
}

/**
Expand Down Expand Up @@ -162,12 +148,6 @@ export interface RepoConfig {
*/
env?: Record<string, string>;

/**
* Per-repo override for the env-leak gate bypass. Repo value wins over global.
* YAML key: `allow_target_repo_keys`
*/
allow_target_repo_keys?: boolean;

/**
* Default commands/workflows configuration
*/
Expand Down Expand Up @@ -250,14 +230,6 @@ export interface MergedConfig {
* Undefined when no env vars are configured.
*/
envVars?: Record<string, string>;

/**
* Effective value of the env-leak gate bypass. When true, the env scanner
* is skipped during registration and pre-spawn. Repo-level override wins
* over global (explicit `false` at repo level re-enables the gate).
* @default false
*/
allowTargetRepoKeys: boolean;
}

/**
Expand Down
18 changes: 3 additions & 15 deletions packages/core/src/db/adapters/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,21 +216,9 @@ export class SqliteAdapter implements IDatabase {
getLog().warn({ err: e as Error }, 'db.sqlite_migration_session_columns_failed');
}

// Codebases columns (added in #983 — env-leak gate consent bit)
try {
const cbCols = this.db.prepare("PRAGMA table_info('remote_agent_codebases')").all() as {
name: string;
}[];
const cbColNames = new Set(cbCols.map(c => c.name));

if (!cbColNames.has('allow_env_keys')) {
this.db.run(
'ALTER TABLE remote_agent_codebases ADD COLUMN allow_env_keys INTEGER DEFAULT 0'
);
}
} catch (e: unknown) {
getLog().warn({ err: e as Error }, 'db.sqlite_migration_codebases_columns_failed');
}
// Note: allow_env_keys column remains in the schema for backwards compatibility
// with existing databases but is no longer read or written by the application.
// Target repo ambient env is unconditionally blocked (#1135).
}

/**
Expand Down
31 changes: 4 additions & 27 deletions packages/core/src/db/codebases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
findCodebaseByDefaultCwd,
findCodebaseByName,
updateCodebase,
updateCodebaseAllowEnvKeys,
deleteCodebase,
} from './codebases';

Expand All @@ -37,7 +36,6 @@ describe('codebases', () => {
repository_url: 'https://github.com/user/repo',
default_cwd: '/workspace/test-project',
ai_assistant_type: 'claude',
allow_env_keys: false,
commands: { plan: { path: '.claude/commands/plan.md', description: 'Plan feature' } },
created_at: new Date(),
updated_at: new Date(),
Expand All @@ -56,8 +54,8 @@ describe('codebases', () => {

expect(result).toEqual(mockCodebase);
expect(mockQuery).toHaveBeenCalledWith(
'INSERT INTO remote_agent_codebases (name, repository_url, default_cwd, ai_assistant_type, allow_env_keys) VALUES ($1, $2, $3, $4, $5) RETURNING *',
['test-project', 'https://github.com/user/repo', '/workspace/test-project', 'claude', false]
'INSERT INTO remote_agent_codebases (name, repository_url, default_cwd, ai_assistant_type) VALUES ($1, $2, $3, $4) RETURNING *',
['test-project', 'https://github.com/user/repo', '/workspace/test-project', 'claude']
);
});

Expand All @@ -75,8 +73,8 @@ describe('codebases', () => {

expect(result).toEqual(codebaseWithoutOptional);
expect(mockQuery).toHaveBeenCalledWith(
'INSERT INTO remote_agent_codebases (name, repository_url, default_cwd, ai_assistant_type, allow_env_keys) VALUES ($1, $2, $3, $4, $5) RETURNING *',
['test-project', null, '/workspace/test-project', 'claude', false]
'INSERT INTO remote_agent_codebases (name, repository_url, default_cwd, ai_assistant_type) VALUES ($1, $2, $3, $4) RETURNING *',
['test-project', null, '/workspace/test-project', 'claude']
);
});

Expand Down Expand Up @@ -299,7 +297,6 @@ describe('codebases', () => {
name: 'test-repo',
default_cwd: '/workspace/test-repo',
ai_assistant_type: 'claude',
allow_env_keys: false,
repository_url: null,
commands: {},
created_at: new Date(),
Expand Down Expand Up @@ -399,26 +396,6 @@ describe('codebases', () => {
});
});

describe('updateCodebaseAllowEnvKeys', () => {
test('flips the consent bit', async () => {
mockQuery.mockResolvedValueOnce(createQueryResult([], 1));

await updateCodebaseAllowEnvKeys('codebase-123', true);

expect(mockQuery).toHaveBeenCalledWith(
'UPDATE remote_agent_codebases SET allow_env_keys = $1, updated_at = NOW() WHERE id = $2',
[true, 'codebase-123']
);
});

test('throws when codebase not found', async () => {
mockQuery.mockResolvedValueOnce(createQueryResult([], 0));
await expect(updateCodebaseAllowEnvKeys('missing', false)).rejects.toThrow(
'Codebase missing not found'
);
});
});

describe('deleteCodebase', () => {
test('should unlink sessions, conversations, and delete codebase', async () => {
// First call: unlink sessions
Expand Down
21 changes: 2 additions & 19 deletions packages/core/src/db/codebases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@ export async function createCodebase(data: {
repository_url?: string;
default_cwd: string;
ai_assistant_type?: string;
allow_env_keys?: boolean;
}): Promise<Codebase> {
const assistantType = data.ai_assistant_type ?? 'claude';
const allowEnvKeys = data.allow_env_keys ?? false;
const result = await pool.query<Codebase>(
'INSERT INTO remote_agent_codebases (name, repository_url, default_cwd, ai_assistant_type, allow_env_keys) VALUES ($1, $2, $3, $4, $5) RETURNING *',
[data.name, data.repository_url ?? null, data.default_cwd, assistantType, allowEnvKeys]
'INSERT INTO remote_agent_codebases (name, repository_url, default_cwd, ai_assistant_type) VALUES ($1, $2, $3, $4) RETURNING *',
[data.name, data.repository_url ?? null, data.default_cwd, assistantType]
);
if (!result.rows[0]) {
throw new Error('Failed to create codebase: INSERT succeeded but no row returned');
Expand Down Expand Up @@ -158,21 +156,6 @@ export async function updateCodebase(
}
}

/**
* Flip the `allow_env_keys` consent bit for an existing codebase.
* Throws when the codebase does not exist.
*/
export async function updateCodebaseAllowEnvKeys(id: string, allowEnvKeys: boolean): Promise<void> {
const dialect = getDialect();
const result = await pool.query(
`UPDATE remote_agent_codebases SET allow_env_keys = $1, updated_at = ${dialect.now()} WHERE id = $2`,
[allowEnvKeys, id]
);
if ((result.rowCount ?? 0) === 0) {
throw new Error(`Codebase ${id} not found`);
}
}

export async function listCodebases(): Promise<readonly Codebase[]> {
const result = await pool.query<Codebase>(
'SELECT * FROM remote_agent_codebases ORDER BY name ASC'
Expand Down
22 changes: 8 additions & 14 deletions packages/core/src/handlers/clone.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const mockCreateCodebase = mock(() =>
repository_url: 'https://github.com/owner/repo',
default_cwd: '/home/test/.archon/workspaces/owner/repo/source',
ai_assistant_type: 'claude',
allow_env_keys: false,
commands: {},
created_at: new Date(),
updated_at: new Date(),
Expand Down Expand Up @@ -70,7 +69,7 @@ mock.module('../utils/commands', () => ({
// ── env-leak-scanner mock ───────────────────────────────────────────────────
class MockEnvLeakError extends Error {
constructor(public report: unknown) {
super('Cannot add codebase — /test/path contains keys that will leak into AI subprocesses');
super('Cannot use codebase — /test/path contains keys that will leak into AI subprocesses');
this.name = 'EnvLeakError';
}
}
Expand Down Expand Up @@ -157,7 +156,6 @@ function makeCodebase(
repository_url: 'https://github.com/owner/repo',
default_cwd: '/home/test/.archon/workspaces/owner/repo/source',
ai_assistant_type: 'claude',
allow_env_keys: false,
commands: {},
created_at: new Date(),
updated_at: new Date(),
Expand Down Expand Up @@ -950,31 +948,27 @@ describe('RegisterResult shape', () => {
});

describe('env leak gate', () => {
test('throws EnvLeakError when scanner finds sensitive keys and allowEnvKeys is false', async () => {
test('throws EnvLeakError when scanner finds sensitive keys', async () => {
mockScanPathForSensitiveKeys.mockReturnValueOnce({
path: '/home/test/.archon/workspaces/owner/repo/source',
findings: [{ file: '.env', keys: ['ANTHROPIC_API_KEY'] }],
});

await expect(cloneRepository('https://github.com/owner/repo')).rejects.toThrow(
'Cannot add codebase'
'Cannot use codebase'
);
});

test('does not throw when allowEnvKeys is true, even with scanner findings present', async () => {
mockCreateCodebase.mockResolvedValueOnce(makeCodebase() as ReturnType<typeof makeCodebase>);
// Scanner is still called for the audit-log payload (files/keys), but the
// gate must NOT throw — the per-call grant is the bypass.
test('always blocks — no bypass path exists', async () => {
mockScanPathForSensitiveKeys.mockReturnValueOnce({
path: '/home/test/.archon/workspaces/owner/repo/source',
findings: [{ file: '.env', keys: ['ANTHROPIC_API_KEY'] }],
});

const result = await cloneRepository('https://github.com/owner/repo', true);

expect(result.codebaseId).toBe('codebase-uuid-1');
// Scanner is called once — for the audit log, not as a gate
expect(mockScanPathForSensitiveKeys).toHaveBeenCalledTimes(1);
// cloneRepository no longer accepts an allowEnvKeys parameter
await expect(cloneRepository('https://github.com/owner/repo')).rejects.toThrow(
'Cannot use codebase'
);
});
});
});
Loading
Loading