diff --git a/CLAUDE.md b/CLAUDE.md index 363086969d..fab1f9a0d7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 @@ -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 diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index d7dedf4810..c28a6436fb 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -125,9 +125,6 @@ Options: --json Output machine-readable JSON (for workflow list) --workflow 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 Override server port for 'serve' (default: 3090) --download-only Download web UI without starting the server @@ -207,7 +204,6 @@ async function main(): Promise { reason: { type: 'string' }, workflow: { type: 'string' }, 'no-context': { type: 'boolean' }, - 'allow-env-keys': { type: 'boolean' }, port: { type: 'string' }, 'download-only': { type: 'boolean' }, }, @@ -231,7 +227,6 @@ async function main(): Promise { 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) { @@ -344,7 +339,6 @@ async function main(): Promise { fromBranch, noWorktree, resume: resumeFlag, - allowEnvKeys: allowEnvKeysFlag, quiet: values.quiet as boolean | undefined, verbose: values.verbose as boolean | undefined, }; diff --git a/packages/cli/src/commands/workflow.ts b/packages/cli/src/commands/workflow.ts index 89dd5911e4..6ba31d1256 100644 --- a/packages/cli/src/commands/workflow.ts +++ b/packages/cli/src/commands/workflow.ts @@ -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. */ @@ -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'); diff --git a/packages/core/src/config/config-loader.ts b/packages/core/src/config/config-loader.ts index 8ee702c613..f0f51ba0a4 100644 --- a/packages/core/src/config/config-loader.ts +++ b/packages/core/src/config/config-loader.ts @@ -38,24 +38,6 @@ function getLog(): ReturnType { 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 */ @@ -216,7 +198,6 @@ function getDefaults(): MergedConfig { loadDefaultCommands: true, loadDefaultWorkflows: true, }, - allowTargetRepoKeys: false, }; } @@ -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; } @@ -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; } diff --git a/packages/core/src/config/config-types.ts b/packages/core/src/config/config-types.ts index 7dd74ac8ba..983720c13b 100644 --- a/packages/core/src/config/config-types.ts +++ b/packages/core/src/config/config-types.ts @@ -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; } /** @@ -162,12 +148,6 @@ export interface RepoConfig { */ env?: Record; - /** - * 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 */ @@ -250,14 +230,6 @@ export interface MergedConfig { * Undefined when no env vars are configured. */ envVars?: Record; - - /** - * 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; } /** diff --git a/packages/core/src/db/adapters/sqlite.ts b/packages/core/src/db/adapters/sqlite.ts index 2864e4fc43..8aefa9db55 100644 --- a/packages/core/src/db/adapters/sqlite.ts +++ b/packages/core/src/db/adapters/sqlite.ts @@ -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). } /** diff --git a/packages/core/src/db/codebases.test.ts b/packages/core/src/db/codebases.test.ts index ec3c249d14..26c269a085 100644 --- a/packages/core/src/db/codebases.test.ts +++ b/packages/core/src/db/codebases.test.ts @@ -22,7 +22,6 @@ import { findCodebaseByDefaultCwd, findCodebaseByName, updateCodebase, - updateCodebaseAllowEnvKeys, deleteCodebase, } from './codebases'; @@ -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(), @@ -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'] ); }); @@ -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'] ); }); @@ -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(), @@ -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 diff --git a/packages/core/src/db/codebases.ts b/packages/core/src/db/codebases.ts index b9f45578b6..f3947fb6c1 100644 --- a/packages/core/src/db/codebases.ts +++ b/packages/core/src/db/codebases.ts @@ -17,13 +17,11 @@ export async function createCodebase(data: { repository_url?: string; default_cwd: string; ai_assistant_type?: string; - allow_env_keys?: boolean; }): Promise { const assistantType = data.ai_assistant_type ?? 'claude'; - const allowEnvKeys = data.allow_env_keys ?? false; const result = await pool.query( - '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'); @@ -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 { - 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 { const result = await pool.query( 'SELECT * FROM remote_agent_codebases ORDER BY name ASC' diff --git a/packages/core/src/handlers/clone.test.ts b/packages/core/src/handlers/clone.test.ts index 7f948cfb33..477778b360 100644 --- a/packages/core/src/handlers/clone.test.ts +++ b/packages/core/src/handlers/clone.test.ts @@ -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(), @@ -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'; } } @@ -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(), @@ -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); - // 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' + ); }); }); }); diff --git a/packages/core/src/handlers/clone.ts b/packages/core/src/handlers/clone.ts index 3dc96f499c..b0ca71a398 100644 --- a/packages/core/src/handlers/clone.ts +++ b/packages/core/src/handlers/clone.ts @@ -16,12 +16,7 @@ import { parseOwnerRepo, } from '@archon/paths'; import { findMarkdownFilesRecursive } from '../utils/commands'; -import { - scanPathForSensitiveKeys, - EnvLeakError, - type LeakErrorContext, -} from '../utils/env-leak-scanner'; -import { loadConfig } from '../config/config-loader'; +import { scanPathForSensitiveKeys, EnvLeakError } from '../utils/env-leak-scanner'; import { createLogger } from '@archon/paths'; /** Lazy-initialized logger (deferred so test mocks can intercept createLogger) */ @@ -46,51 +41,13 @@ export interface RegisterResult { async function registerRepoAtPath( targetPath: string, name: string, - repositoryUrl: string | null, - allowEnvKeys = false, - context: LeakErrorContext = 'register-ui' + repositoryUrl: string | null ): Promise { // Scan for sensitive keys in auto-loaded .env files before registering. - // Two bypass paths exist (in order of precedence): - // 1. Per-call `allowEnvKeys=true` (Web UI checkbox or CLI --allow-env-keys) - // 2. Config-level `allow_target_repo_keys: true` (global YAML) - // When the per-call bypass is used we still emit an audit-log entry so the - // grant has a permanent breadcrumb (parity with the PATCH route's - // `env_leak_consent_granted` log). - if (!allowEnvKeys) { - const merged = await loadConfig(targetPath); - if (!merged.allowTargetRepoKeys) { - const report = scanPathForSensitiveKeys(targetPath); - if (report.findings.length > 0) { - throw new EnvLeakError(report, context); - } - } - } else { - // Per-call grant — emit audit log mirroring the PATCH route shape so the - // CLI/UI add-with-consent paths leave the same breadcrumbs. - let files: string[] = []; - let keys: string[] = []; - let scanStatus: 'ok' | 'skipped' = 'ok'; - try { - const report = scanPathForSensitiveKeys(targetPath); - files = report.findings.map(f => f.file); - keys = Array.from(new Set(report.findings.flatMap(f => f.keys))); - } catch (scanErr) { - scanStatus = 'skipped'; - getLog().warn({ err: scanErr, path: targetPath }, 'env_leak_consent_scan_skipped'); - } - const actor = context === 'register-cli' ? 'user-cli' : 'user-ui'; - getLog().warn( - { - name, - path: targetPath, - files, - keys, - scanStatus, - actor, - }, - 'env_leak_consent_granted' - ); + // Target repo ambient env is never allowed — no bypass path exists. + const report = scanPathForSensitiveKeys(targetPath); + if (report.findings.length > 0) { + throw new EnvLeakError(report); } // Auto-detect assistant type based on folder structure @@ -173,7 +130,6 @@ async function registerRepoAtPath( repository_url: repositoryUrl ?? undefined, default_cwd: targetPath, ai_assistant_type: suggestedAssistant, - allow_env_keys: allowEnvKeys, }); // Auto-load commands if found @@ -242,15 +198,11 @@ function normalizeRepoUrl(rawUrl: string): { * Local paths (starting with /, ~, or .) are delegated to registerRepository * to avoid wrong owner/repo naming. See #383 for broader rethink. */ -export async function cloneRepository( - repoUrl: string, - allowEnvKeys?: boolean, - context: LeakErrorContext = 'register-ui' -): Promise { +export async function cloneRepository(repoUrl: string): Promise { // Local paths should be registered (symlink), not cloned (copied) if (repoUrl.startsWith('/') || repoUrl.startsWith('~') || repoUrl.startsWith('.')) { const resolvedPath = repoUrl.startsWith('~') ? expandTilde(repoUrl) : resolve(repoUrl); - return registerRepository(resolvedPath, allowEnvKeys, context); + return registerRepository(resolvedPath); } const { workingUrl, ownerName, repoName, targetPath } = normalizeRepoUrl(repoUrl); @@ -331,13 +283,7 @@ export async function cloneRepository( await execFileAsync('git', ['config', '--global', '--add', 'safe.directory', targetPath]); getLog().debug({ path: targetPath }, 'safe_directory_added'); - const result = await registerRepoAtPath( - targetPath, - `${ownerName}/${repoName}`, - workingUrl, - allowEnvKeys, - context - ); + const result = await registerRepoAtPath(targetPath, `${ownerName}/${repoName}`, workingUrl); getLog().info({ url: workingUrl, targetPath }, 'clone_completed'); return result; } @@ -345,11 +291,7 @@ export async function cloneRepository( /** * Register an existing local repository in the database (no git clone). */ -export async function registerRepository( - localPath: string, - allowEnvKeys?: boolean, - context: LeakErrorContext = 'register-ui' -): Promise { +export async function registerRepository(localPath: string): Promise { // Validate path exists and is a git repo try { await execFileAsync('git', ['-C', localPath, 'rev-parse', '--git-dir']); @@ -415,5 +357,5 @@ export async function registerRepository( ); // default_cwd is the real local path (not the symlink) - return registerRepoAtPath(localPath, name, remoteUrl, allowEnvKeys, context); + return registerRepoAtPath(localPath, name, remoteUrl); } diff --git a/packages/core/src/handlers/command-handler.test.ts b/packages/core/src/handlers/command-handler.test.ts index 4f29e7247b..03568c17da 100644 --- a/packages/core/src/handlers/command-handler.test.ts +++ b/packages/core/src/handlers/command-handler.test.ts @@ -511,7 +511,7 @@ describe('CommandHandler', () => { repository_url: 'https://github.com/user/my-repo', default_cwd: '/workspace/my-repo', ai_assistant_type: 'claude', - allow_env_keys: false, + commands: {}, created_at: new Date(), updated_at: new Date(), @@ -567,7 +567,7 @@ describe('CommandHandler', () => { repository_url: 'https://github.com/owner/repo', default_cwd: '/workspace/repo', ai_assistant_type: 'claude', - allow_env_keys: false, + commands: {}, created_at: new Date(), updated_at: new Date(), @@ -606,7 +606,7 @@ describe('CommandHandler', () => { repository_url: 'https://github.com/owner/orphaned-repo', default_cwd: '/workspace/orphaned-repo', ai_assistant_type: 'claude', - allow_env_keys: false, + commands: {}, created_at: new Date(), updated_at: new Date(), @@ -721,7 +721,7 @@ describe('CommandHandler', () => { repository_url: 'https://github.com/user/my-repo', default_cwd: '/workspace/my-repo', ai_assistant_type: 'claude', - allow_env_keys: false, + commands: {}, created_at: new Date(), updated_at: new Date(), diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a0c897481f..d9e4ee2e0c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -142,7 +142,6 @@ export { scanPathForSensitiveKeys, formatLeakError, type LeakReport, - type LeakErrorContext, } from './utils/env-leak-scanner'; // GitHub GraphQL diff --git a/packages/core/src/orchestrator/orchestrator-agent.test.ts b/packages/core/src/orchestrator/orchestrator-agent.test.ts index b1e155a8f8..dfde310bbd 100644 --- a/packages/core/src/orchestrator/orchestrator-agent.test.ts +++ b/packages/core/src/orchestrator/orchestrator-agent.test.ts @@ -182,7 +182,6 @@ function makeCodebase(name: string, id = `id-${name}`): Codebase { repository_url: null, default_cwd: `/repos/${name}`, ai_assistant_type: 'claude', - allow_env_keys: false, commands: {}, created_at: new Date(), updated_at: new Date(), @@ -806,7 +805,6 @@ function makeCodebaseForSync() { repository_url: 'https://github.com/test/repo', default_cwd: '/repos/test-repo', ai_assistant_type: 'claude', - allow_env_keys: false, commands: {}, created_at: new Date(), updated_at: new Date(), @@ -972,7 +970,6 @@ describe('workflow dispatch routing — interactive flag', () => { repository_url: null, default_cwd: '/repos/test-repo', ai_assistant_type: 'claude' as const, - allow_env_keys: false, commands: {}, created_at: new Date(), updated_at: new Date(), @@ -1073,7 +1070,6 @@ describe('natural-language approval routing', () => { repository_url: null, default_cwd: '/repos/test-repo', ai_assistant_type: 'claude' as const, - allow_env_keys: false, commands: {}, created_at: new Date(), updated_at: new Date(), diff --git a/packages/core/src/orchestrator/orchestrator-isolation.test.ts b/packages/core/src/orchestrator/orchestrator-isolation.test.ts index 6aabc41597..6bcbedb697 100644 --- a/packages/core/src/orchestrator/orchestrator-isolation.test.ts +++ b/packages/core/src/orchestrator/orchestrator-isolation.test.ts @@ -176,7 +176,6 @@ function makeCodebase(overrides?: Partial): Codebase { id: 'cb-1', name: 'test-repo', default_cwd: '/workspace/test-repo', - allow_env_keys: false, commands: {}, created_at: new Date(), updated_at: new Date(), diff --git a/packages/core/src/orchestrator/orchestrator.test.ts b/packages/core/src/orchestrator/orchestrator.test.ts index 8f99efff64..de4618ed15 100644 --- a/packages/core/src/orchestrator/orchestrator.test.ts +++ b/packages/core/src/orchestrator/orchestrator.test.ts @@ -216,7 +216,6 @@ const mockCodebase: Codebase = { repository_url: 'https://github.com/user/repo', default_cwd: '/workspace/test-project', ai_assistant_type: 'claude', - allow_env_keys: false, commands: {}, created_at: new Date(), updated_at: new Date(), diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index c847122c74..74966e3b2c 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -59,7 +59,6 @@ export interface Codebase { repository_url: string | null; default_cwd: string; ai_assistant_type: string; - allow_env_keys: boolean; commands: Record; created_at: Date; updated_at: Date; diff --git a/packages/core/src/utils/env-leak-scanner.test.ts b/packages/core/src/utils/env-leak-scanner.test.ts index 4d436bbc24..008908e4be 100644 --- a/packages/core/src/utils/env-leak-scanner.test.ts +++ b/packages/core/src/utils/env-leak-scanner.test.ts @@ -94,26 +94,12 @@ describe('EnvLeakError', () => { expect(err.report).toBe(report); }); - it('defaults context to register-ui and stores it on the error', () => { + it('produces a single error message with remediation steps', () => { const report = { path: '/x', findings: [{ file: '.env', keys: ['ANTHROPIC_API_KEY'] }] }; - const err = new EnvLeakError(report); - expect(err.context).toBe('register-ui'); - expect(err.message).toContain('Add Project'); - }); - - it('produces distinct remediation bodies per context', () => { - const report = { path: '/x', findings: [{ file: '.env', keys: ['ANTHROPIC_API_KEY'] }] }; - const ui = formatLeakError(report, 'register-ui'); - const cli = formatLeakError(report, 'register-cli'); - const spawn = formatLeakError(report, 'spawn-existing'); - expect(ui).toContain('Add Project'); - expect(cli).toContain('--allow-env-keys'); - expect(cli).toContain('allow_target_repo_keys'); - expect(spawn).toContain('Settings'); - expect(spawn).toContain('already-registered'); - // headers differ between register and spawn - expect(ui).toContain('Cannot add codebase'); - expect(spawn).toContain('Cannot run workflow'); + const msg = formatLeakError(report); + expect(msg).toContain('Cannot use codebase'); + expect(msg).toContain('.env'); + expect(msg).toContain('ANTHROPIC_API_KEY'); }); it('formats multiple findings', () => { diff --git a/packages/core/src/utils/env-leak-scanner.ts b/packages/core/src/utils/env-leak-scanner.ts index 48edc2c6b7..94122607bc 100644 --- a/packages/core/src/utils/env-leak-scanner.ts +++ b/packages/core/src/utils/env-leak-scanner.ts @@ -30,25 +30,10 @@ export interface LeakReport { findings: LeakFinding[]; } -/** - * Context in which the env-leak error is being surfaced. Drives the remediation - * copy so users see guidance that matches how they hit the gate. - * - * - `register-ui`: Add-Project flow in the Web UI (checkbox is visible) - * - `register-cli`: CLI auto-register path (no Web UI) - * - `spawn-existing`: Pre-spawn check for an already-registered codebase - */ -export type LeakErrorContext = 'register-ui' | 'register-cli' | 'spawn-existing'; - export class EnvLeakError extends Error { - public readonly context: LeakErrorContext; - constructor( - public readonly report: LeakReport, - context: LeakErrorContext = 'register-ui' - ) { - super(formatLeakError(report, context)); + constructor(public readonly report: LeakReport) { + super(formatLeakError(report)); this.name = 'EnvLeakError'; - this.context = context; } } @@ -91,48 +76,10 @@ export function scanPathForSensitiveKeys(dirPath: string): LeakReport { return { path: dirPath, findings }; } -/** - * Exhaustive per-context consent remediation copy. Using `switch` with a - * `never` default means adding a new `LeakErrorContext` variant without - * handling it here is a compile error — important for a security-visible path. - */ -function consentCopy(context: LeakErrorContext): string { - switch (context) { - case 'register-cli': - return ` 3. Acknowledge the risk and allow this codebase to use its .env key: - Re-run the CLI command with --allow-env-keys, or set - 'allow_target_repo_keys: true' in ~/.archon/config.yaml to bypass this - gate globally.`; - case 'spawn-existing': - return ` 3. Acknowledge the risk for this already-registered codebase: - Open the Web UI (Settings → Projects), find this project, and toggle - "Allow env keys". Or set 'allow_target_repo_keys: true' in - ~/.archon/config.yaml to bypass this gate globally.`; - case 'register-ui': - return ` 3. Acknowledge the risk and allow this codebase to use its .env key: - Open the web UI (Settings → Projects → Add Project) and tick - "Allow env keys (I understand the risk)" when adding this project.`; - default: { - const exhaustive: never = context; - return exhaustive; - } - } -} - -export function formatLeakError( - report: LeakReport, - context: LeakErrorContext = 'register-ui' -): string { +export function formatLeakError(report: LeakReport): string { const fileList = report.findings.map(f => ` ${f.file} — ${f.keys.join(', ')}`).join('\n'); - const header = - context === 'spawn-existing' - ? `Cannot run workflow — ${report.path} contains keys that will leak into AI subprocesses` - : `Cannot add codebase — ${report.path} contains keys that will leak into AI subprocesses`; - - const consent = consentCopy(context); - - return `${header} + return `Cannot use codebase — ${report.path} contains keys that will leak into AI subprocesses Found: ${fileList} @@ -140,16 +87,14 @@ ${fileList} Why this matters: Bun subprocesses auto-load .env from their working directory. Archon cleans its own environment, but Claude/Codex subprocesses running with cwd= - will re-inject these keys at their own startup, bypassing archon's allowlist. + will re-inject these keys at their own startup, bypassing Archon's allowlist. This can bill the wrong API account silently. - Choose one: + Fix (choose one): 1. Remove the key from this repo's .env (recommended): grep -v '^ANTHROPIC_API_KEY=' .env > .env.tmp && mv .env.tmp .env 2. Rename to a non-auto-loaded file: mv .env .env.secrets - # update your app to load it explicitly - -${consent}`; + # update your app to load it explicitly`; } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 0b502008d6..dfdf88217b 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -199,56 +199,47 @@ export async function startServer(opts: ServerOptions = {}): Promise { process.exit(1); } - // Load configuration early so the startup env-leak scan can honor the - // global bypass. Without this, users who set `allow_target_repo_keys: true` - // would get a per-codebase warn spam on every boot even though the gate - // is intentionally disabled. const config = await loadConfig(); logConfig(config); // Startup env-leak scan: warn for codebases that would be blocked at next - // spawn by the env-leak-gate. Skipped entirely when the global bypass is - // active. Best-effort — failures are surfaced but never block startup. - if (config.allowTargetRepoKeys) { - getLog().info('startup_env_leak_scan_skipped — allow_target_repo_keys is true'); - } else { - try { - const codebases = await codebaseDb.listCodebases(); - for (const cb of codebases) { - if (cb.allow_env_keys) continue; - try { - const report = scanPathForSensitiveKeys(cb.default_cwd); - if (report.findings.length > 0) { - const files = report.findings.map(f => f.file); - const keys = Array.from(new Set(report.findings.flatMap(f => f.keys))); - getLog().warn( - { - codebaseId: cb.id, - name: cb.name, - path: cb.default_cwd, - files, - keys, - }, - 'startup_env_leak_gate_will_block' - ); - } - } catch (scanErr) { - // Path may no longer exist (codebase moved/deleted on disk) — - // log at debug, do not abort the loop. This is the only quiet path. - getLog().debug( - { err: scanErr, codebaseId: cb.id, path: cb.default_cwd }, - 'startup_env_leak_scan_path_unavailable' + // spawn by the env-leak-gate. Best-effort — failures are surfaced but never + // block startup. + try { + const codebases = await codebaseDb.listCodebases(); + for (const cb of codebases) { + try { + const report = scanPathForSensitiveKeys(cb.default_cwd); + if (report.findings.length > 0) { + const files = report.findings.map(f => f.file); + const keys = Array.from(new Set(report.findings.flatMap(f => f.keys))); + getLog().warn( + { + codebaseId: cb.id, + name: cb.name, + path: cb.default_cwd, + files, + keys, + }, + 'startup_env_leak_gate_will_block' ); } + } catch (scanErr) { + // Path may no longer exist (codebase moved/deleted on disk) — + // log at debug, do not abort the loop. This is the only quiet path. + getLog().debug( + { err: scanErr, codebaseId: cb.id, path: cb.default_cwd }, + 'startup_env_leak_scan_path_unavailable' + ); } - } catch (error) { - // listCodebases() failed — the entire startup safety net is silently - // absent. Surface at error level so operators see it. - getLog().error( - { err: error }, - 'startup_env_leak_scan_failed — startup migration warnings suppressed' - ); } + } catch (error) { + // listCodebases() failed — the entire startup safety net is silently + // absent. Surface at error level so operators see it. + getLog().error( + { err: error }, + 'startup_env_leak_scan_failed — startup migration warnings suppressed' + ); } // Start cleanup scheduler diff --git a/packages/server/src/routes/api.codebases.test.ts b/packages/server/src/routes/api.codebases.test.ts index 0265a359e1..7bd3092adc 100644 --- a/packages/server/src/routes/api.codebases.test.ts +++ b/packages/server/src/routes/api.codebases.test.ts @@ -48,11 +48,10 @@ mock.module('@archon/core', () => ({ this.name = 'ConversationNotFoundError'; } }, - scanPathForSensitiveKeys: mock((_p: string) => ({ path: _p, findings: [] })), EnvLeakError: class EnvLeakError extends Error { constructor(public report: { path: string; findings: { file: string; keys: string[] }[] }) { super( - `Cannot add codebase — ${report.path} contains keys that will leak into AI subprocesses` + `Cannot use codebase — ${report.path} contains keys that will leak into AI subprocesses` ); this.name = 'EnvLeakError'; } @@ -123,12 +122,10 @@ mock.module('@archon/core/db/conversations', () => ({ getConversationById: mock(async () => null), })); -const mockUpdateCodebaseAllowEnvKeys = mock(async (_id: string, _v: boolean) => {}); mock.module('@archon/core/db/codebases', () => ({ listCodebases: mockListCodebases, getCodebase: mockGetCodebase, deleteCodebase: mockDeleteCodebase, - updateCodebaseAllowEnvKeys: mockUpdateCodebaseAllowEnvKeys, })); mock.module('@archon/core/db/isolation-environments', () => ({ @@ -181,7 +178,6 @@ const MOCK_CODEBASE = { repository_url: 'https://github.com/user/repo', default_cwd: '/home/user/projects/my-project', ai_assistant_type: 'claude', - allow_env_keys: false, commands: {}, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), @@ -399,7 +395,7 @@ describe('POST /api/codebases', () => { const body = (await response.json()) as { id: string }; expect(body.id).toBe('codebase-uuid-1'); - expect(mockCloneRepository).toHaveBeenCalledWith('https://github.com/user/repo', undefined); + expect(mockCloneRepository).toHaveBeenCalledWith('https://github.com/user/repo'); }); test('registers existing URL codebase with 200', async () => { @@ -436,7 +432,7 @@ describe('POST /api/codebases', () => { body: JSON.stringify({ path: '/home/user/my-repo' }), }); expect(response.status).toBe(201); - expect(mockRegisterRepository).toHaveBeenCalledWith('/home/user/my-repo', undefined); + expect(mockRegisterRepository).toHaveBeenCalledWith('/home/user/my-repo'); }); test('returns 400 when both url and path are provided', async () => { @@ -527,81 +523,7 @@ describe('POST /api/codebases', () => { expect(response.status).toBe(422); const body = (await response.json()) as { error: string }; - expect(body.error).toContain('Cannot add codebase'); - }); - - test('passes allowEnvKeys=true to cloneRepository when body includes it', async () => { - mockCloneRepository.mockImplementationOnce(async () => ({ - codebaseId: 'clone-uuid-2', - alreadyExisted: false, - })); - mockGetCodebase.mockImplementationOnce(async () => MOCK_CODEBASE); - - const app = makeApp(); - const response = await app.request('/api/codebases', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url: 'https://github.com/user/repo', allowEnvKeys: true }), - }); - expect(response.status).toBe(201); - expect(mockCloneRepository).toHaveBeenCalledWith('https://github.com/user/repo', true); - }); -}); - -// --------------------------------------------------------------------------- -// Tests: PATCH /api/codebases/:id -// --------------------------------------------------------------------------- - -describe('PATCH /api/codebases/:id', () => { - beforeEach(() => { - mockGetCodebase.mockReset(); - mockUpdateCodebaseAllowEnvKeys.mockReset(); - }); - - test('grants consent and returns updated codebase', async () => { - mockGetCodebase - .mockImplementationOnce(async () => MOCK_CODEBASE) - .mockImplementationOnce(async () => ({ ...MOCK_CODEBASE, allow_env_keys: true })); - mockUpdateCodebaseAllowEnvKeys.mockImplementationOnce(async () => {}); - - const app = makeApp(); - const response = await app.request('/api/codebases/codebase-uuid-1', { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ allowEnvKeys: true }), - }); - expect(response.status).toBe(200); - const body = (await response.json()) as { allow_env_keys: boolean }; - expect(body.allow_env_keys).toBe(true); - expect(mockUpdateCodebaseAllowEnvKeys).toHaveBeenCalledWith('codebase-uuid-1', true); - }); - - test('revokes consent', async () => { - mockGetCodebase - .mockImplementationOnce(async () => ({ ...MOCK_CODEBASE, allow_env_keys: true })) - .mockImplementationOnce(async () => MOCK_CODEBASE); - mockUpdateCodebaseAllowEnvKeys.mockImplementationOnce(async () => {}); - - const app = makeApp(); - const response = await app.request('/api/codebases/codebase-uuid-1', { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ allowEnvKeys: false }), - }); - expect(response.status).toBe(200); - expect(mockUpdateCodebaseAllowEnvKeys).toHaveBeenCalledWith('codebase-uuid-1', false); - }); - - test('returns 404 when codebase not found', async () => { - mockGetCodebase.mockImplementationOnce(async () => null); - - const app = makeApp(); - const response = await app.request('/api/codebases/missing', { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ allowEnvKeys: true }), - }); - expect(response.status).toBe(404); + expect(body.error).toContain('Cannot use codebase'); }); }); diff --git a/packages/server/src/routes/api.ts b/packages/server/src/routes/api.ts index ed267c1d41..2f852f78db 100644 --- a/packages/server/src/routes/api.ts +++ b/packages/server/src/routes/api.ts @@ -28,7 +28,6 @@ import { ConversationNotFoundError, generateAndSetTitle, EnvLeakError, - scanPathForSensitiveKeys, } from '@archon/core'; import { removeWorktree, toRepoPath, toWorktreePath } from '@archon/git'; import { @@ -109,7 +108,6 @@ import { codebaseSchema, codebaseIdParamsSchema, addCodebaseBodySchema, - updateCodebaseBodySchema, deleteCodebaseResponseSchema, codebaseEnvVarsResponseSchema, setEnvVarBodySchema, @@ -467,28 +465,6 @@ const addCodebaseRoute = createRoute({ }, }); -const updateCodebaseRoute = createRoute({ - method: 'patch', - path: '/api/codebases/{id}', - tags: ['Codebases'], - summary: 'Update codebase consent flags (e.g. allow_env_keys)', - request: { - params: codebaseIdParamsSchema, - body: { - content: { 'application/json': { schema: updateCodebaseBodySchema } }, - required: true, - }, - }, - responses: { - 200: { - content: { 'application/json': { schema: codebaseSchema } }, - description: 'Updated codebase', - }, - 404: jsonError('Not found'), - 500: jsonError('Server error'), - }, -}); - const deleteCodebaseRoute = createRoute({ method: 'delete', path: '/api/codebases/{id}', @@ -1531,8 +1507,8 @@ export function registerApiRoutes( try { // .refine() guarantees exactly one of url/path is present const result = body.url - ? await cloneRepository(body.url, body.allowEnvKeys) - : await registerRepository(body.path ?? '', body.allowEnvKeys); + ? await cloneRepository(body.url) + : await registerRepository(body.path ?? ''); // Fetch the full codebase record for a consistent response const codebase = await codebaseDb.getCodebase(result.codebaseId); @@ -1557,71 +1533,6 @@ export function registerApiRoutes( } }); - // PATCH /api/codebases/:id - Update consent flags - registerOpenApiRoute(updateCodebaseRoute, async c => { - const id = c.req.param('id') ?? ''; - const body = getValidatedBody(c, updateCodebaseBodySchema); - try { - const codebase = await codebaseDb.getCodebase(id); - if (!codebase) { - return apiError(c, 404, 'Codebase not found'); - } - - // Capture scanner findings for the audit log (best-effort — path may be gone) - let files: string[] = []; - let keys: string[] = []; - let scanStatus: 'ok' | 'skipped' = 'ok'; - try { - const report = scanPathForSensitiveKeys(codebase.default_cwd); - files = report.findings.map(f => f.file); - keys = Array.from(new Set(report.findings.flatMap(f => f.keys))); - } catch (scanErr) { - scanStatus = 'skipped'; - getLog().warn( - { err: scanErr, codebaseId: id, path: codebase.default_cwd }, - 'env_leak_consent_scan_skipped' - ); - } - - await codebaseDb.updateCodebaseAllowEnvKeys(id, body.allowEnvKeys); - - // Audit log: emitted unconditionally on every grant/revoke. `scanStatus` - // distinguishes "scanned and these are the findings" from "could not - // scan, files/keys are empty for that reason" — important for later - // security review of the audit trail. - getLog().warn( - { - codebaseId: id, - name: codebase.name, - path: codebase.default_cwd, - files, - keys, - scanStatus, - actor: 'user-ui', - }, - body.allowEnvKeys ? 'env_leak_consent_granted' : 'env_leak_consent_revoked' - ); - - const updated = await codebaseDb.getCodebase(id); - if (!updated) { - return apiError(c, 500, 'Codebase updated but not found'); - } - let commands = updated.commands; - if (typeof commands === 'string') { - try { - commands = JSON.parse(commands); - } catch (parseErr) { - getLog().error({ err: parseErr, codebaseId: id }, 'corrupted_commands_json'); - commands = {}; - } - } - return c.json({ ...updated, commands }); - } catch (error) { - getLog().error({ err: error, codebaseId: id }, 'update_codebase_failed'); - return apiError(c, 500, 'Failed to update codebase'); - } - }); - // DELETE /api/codebases/:id - Delete a project and clean up registerOpenApiRoute(deleteCodebaseRoute, async c => { const id = c.req.param('id') ?? ''; diff --git a/packages/server/src/routes/schemas/codebase.schemas.ts b/packages/server/src/routes/schemas/codebase.schemas.ts index e8a6dea887..d2880a6be1 100644 --- a/packages/server/src/routes/schemas/codebase.schemas.ts +++ b/packages/server/src/routes/schemas/codebase.schemas.ts @@ -16,7 +16,6 @@ export const codebaseSchema = z repository_url: z.string().nullable(), default_cwd: z.string(), ai_assistant_type: z.string(), - allow_env_keys: z.boolean(), commands: z.record(codebaseCommandSchema), created_at: z.string(), updated_at: z.string(), @@ -34,20 +33,12 @@ export const addCodebaseBodySchema = z .object({ url: z.string().min(1).optional(), path: z.string().min(1).optional(), - allowEnvKeys: z.boolean().optional(), }) .refine(b => (b.url !== undefined) !== (b.path !== undefined), { message: 'Provide either "url" or "path", not both and not neither', }) .openapi('AddCodebaseBody'); -/** PATCH /api/codebases/:id request body. */ -export const updateCodebaseBodySchema = z - .object({ - allowEnvKeys: z.boolean(), - }) - .openapi('UpdateCodebaseBody'); - /** DELETE /api/codebases/:id response. */ export const deleteCodebaseResponseSchema = z .object({ success: z.boolean() }) diff --git a/packages/web/src/lib/api.ts b/packages/web/src/lib/api.ts index 6c81aa66b1..81a3529833 100644 --- a/packages/web/src/lib/api.ts +++ b/packages/web/src/lib/api.ts @@ -38,7 +38,6 @@ export interface CodebaseResponse { repository_url: string | null; default_cwd: string; ai_assistant_type: string; - allow_env_keys: boolean; commands: Record; created_at: string; updated_at: string; @@ -158,7 +157,7 @@ export async function getCodebase(id: string): Promise { } export async function addCodebase( - input: { url: string; allowEnvKeys?: boolean } | { path: string; allowEnvKeys?: boolean } + input: { url: string } | { path: string } ): Promise { return fetchJSON('/api/codebases', { method: 'POST', @@ -167,17 +166,6 @@ export async function addCodebase( }); } -export async function updateCodebase( - id: string, - input: { allowEnvKeys: boolean } -): Promise { - return fetchJSON(`/api/codebases/${id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(input), - }); -} - export async function deleteCodebase(id: string): Promise<{ success: boolean }> { return fetchJSON<{ success: boolean }>(`/api/codebases/${id}`, { method: 'DELETE' }); } diff --git a/packages/web/src/routes/SettingsPage.tsx b/packages/web/src/routes/SettingsPage.tsx index 07a07690fc..0623150001 100644 --- a/packages/web/src/routes/SettingsPage.tsx +++ b/packages/web/src/routes/SettingsPage.tsx @@ -10,7 +10,6 @@ import { getHealth, listCodebases, addCodebase, - updateCodebase, deleteCodebase, updateAssistantConfig, getCodebaseEnvVars, @@ -251,22 +250,11 @@ function EnvVarsPanel({ codebaseId }: { codebaseId: string }): React.ReactElemen ); } -function isEnvLeakError(error: unknown): boolean { - return ( - error instanceof Error && - 'status' in error && - (error as Error & { status: number }).status === 422 && - error.message.startsWith('Cannot add codebase') - ); -} - function ProjectsSection(): React.ReactElement { const queryClient = useQueryClient(); const [addPath, setAddPath] = useState(''); const [showAdd, setShowAdd] = useState(false); - const [allowEnvKeys, setAllowEnvKeys] = useState(false); const [expandedEnvVars, setExpandedEnvVars] = useState(null); - const [toggleError, setToggleError] = useState(null); const { data: codebases } = useQuery({ queryKey: ['codebases'], @@ -274,13 +262,11 @@ function ProjectsSection(): React.ReactElement { }); const addMutation = useMutation({ - mutationFn: ({ path, allowEnvKeys }: { path: string; allowEnvKeys?: boolean }) => - addCodebase({ path, allowEnvKeys }), + mutationFn: (path: string) => addCodebase({ path }), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ['codebases'] }); setAddPath(''); setShowAdd(false); - setAllowEnvKeys(false); }, }); @@ -291,24 +277,10 @@ function ProjectsSection(): React.ReactElement { }, }); - const toggleEnvKeysMutation = useMutation({ - mutationFn: ({ id, allowEnvKeys }: { id: string; allowEnvKeys: boolean }) => - updateCodebase(id, { allowEnvKeys }), - onSuccess: () => { - setToggleError(null); - void queryClient.invalidateQueries({ queryKey: ['codebases'] }); - }, - onError: (err: Error) => { - // Without this the user clicks "Revoke env keys", confirms the - // destructive dialog, and gets no feedback if the PATCH fails. - setToggleError(err.message); - }, - }); - function handleAddSubmit(e: React.FormEvent): void { e.preventDefault(); if (addPath.trim()) { - addMutation.mutate({ path: addPath.trim(), allowEnvKeys: allowEnvKeys || undefined }); + addMutation.mutate(addPath.trim()); } } @@ -318,11 +290,6 @@ function ProjectsSection(): React.ReactElement { Projects - {toggleError && ( -
- Failed to update env-key consent: {toggleError} -
- )} {!codebases || codebases.length === 0 ? (
No projects registered.
) : ( @@ -331,40 +298,10 @@ function ProjectsSection(): React.ReactElement {
-
-
{cb.name}
- {cb.allow_env_keys && ( - - env keys allowed - - )} -
+
{cb.name}
{cb.default_cwd}
-
)}