Skip to content
Merged
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)
- `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
7 changes: 0 additions & 7 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,8 +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) {
printUsage();
Expand Down Expand Up @@ -344,7 +338,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
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"./state/*": "./src/state/*.ts"
},
"scripts": {
"test": "bun test src/handlers/command-handler.test.ts && bun test src/handlers/clone.test.ts && bun test src/db/adapters/postgres.test.ts && bun test src/db/adapters/sqlite.test.ts src/db/codebases.test.ts src/db/connection.test.ts src/db/conversations.test.ts src/db/env-vars.test.ts src/db/isolation-environments.test.ts src/db/messages.test.ts src/db/sessions.test.ts src/db/workflow-events.test.ts src/db/workflows.test.ts src/utils/defaults-copy.test.ts src/utils/worktree-sync.test.ts src/utils/conversation-lock.test.ts src/utils/credential-sanitizer.test.ts src/utils/port-allocation.test.ts src/utils/error.test.ts src/utils/error-formatter.test.ts src/utils/github-graphql.test.ts src/utils/env-leak-scanner.test.ts src/config/ src/state/ && bun test src/utils/path-validation.test.ts && bun test src/services/cleanup-service.test.ts && bun test src/services/title-generator.test.ts && bun test src/workflows/ && bun test src/operations/workflow-operations.test.ts && bun test src/operations/isolation-operations.test.ts && bun test src/orchestrator/orchestrator.test.ts && bun test src/orchestrator/orchestrator-agent.test.ts && bun test src/orchestrator/orchestrator-isolation.test.ts",
"test": "bun test src/handlers/command-handler.test.ts && bun test src/handlers/clone.test.ts && bun test src/db/adapters/postgres.test.ts && bun test src/db/adapters/sqlite.test.ts src/db/codebases.test.ts src/db/connection.test.ts src/db/conversations.test.ts src/db/env-vars.test.ts src/db/isolation-environments.test.ts src/db/messages.test.ts src/db/sessions.test.ts src/db/workflow-events.test.ts src/db/workflows.test.ts src/utils/defaults-copy.test.ts src/utils/worktree-sync.test.ts src/utils/conversation-lock.test.ts src/utils/credential-sanitizer.test.ts src/utils/port-allocation.test.ts src/utils/error.test.ts src/utils/error-formatter.test.ts src/utils/github-graphql.test.ts src/config/ src/state/ && bun test src/utils/path-validation.test.ts && bun test src/services/cleanup-service.test.ts && bun test src/services/title-generator.test.ts && bun test src/workflows/ && bun test src/operations/workflow-operations.test.ts && bun test src/operations/isolation-operations.test.ts && bun test src/orchestrator/orchestrator.test.ts && bun test src/orchestrator/orchestrator-agent.test.ts && bun test src/orchestrator/orchestrator-isolation.test.ts",
"type-check": "bun x tsc --noEmit",
"build": "echo 'No build needed - Bun runs TypeScript directly'"
},
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
17 changes: 0 additions & 17 deletions packages/core/src/db/adapters/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,22 +215,6 @@ export class SqliteAdapter implements IDatabase {
} catch (e: unknown) {
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');
}
}

/**
Expand All @@ -252,7 +236,6 @@ export class SqliteAdapter implements IDatabase {
default_cwd TEXT NOT NULL,
default_branch TEXT DEFAULT 'main',
ai_assistant_type TEXT DEFAULT 'claude',
allow_env_keys INTEGER DEFAULT 0,
commands TEXT DEFAULT '{}',
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
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]
Comment on lines +23 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stop returning the legacy allow_env_keys field.

Dropping the column from the INSERT is only half of the removal. Because the table still keeps allow_env_keys, this RETURNING * and the other SELECT * queries in this module will continue to surface the deprecated consent flag at runtime. packages/server/src/routes/api.ts returns those Codebase rows directly, so the removed primitive can still leak back out over GET/POST responses.

🧩 Suggested fix
+const codebaseColumns =
+  'id, name, repository_url, default_cwd, ai_assistant_type, commands, created_at, updated_at';
+
 export async function createCodebase(data: {
   name: string;
   repository_url?: string;
   default_cwd: string;
   ai_assistant_type?: string;
 }): Promise<Codebase> {
   const assistantType = data.ai_assistant_type ?? 'claude';
   const result = await pool.query<Codebase>(
-    'INSERT INTO remote_agent_codebases (name, repository_url, default_cwd, ai_assistant_type) VALUES ($1, $2, $3, $4) RETURNING *',
+    `INSERT INTO remote_agent_codebases (name, repository_url, default_cwd, ai_assistant_type)
+     VALUES ($1, $2, $3, $4)
+     RETURNING ${codebaseColumns}`,
     [data.name, data.repository_url ?? null, data.default_cwd, assistantType]
   );

Apply the same codebaseColumns projection to the SELECT * helpers in this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/db/codebases.ts` around lines 23 - 24, The INSERT currently
uses RETURNING * and the module's SELECT helpers still use SELECT *, which
causes the deprecated allow_env_keys to be returned; update the INSERT and all
SELECT helpers in this file to use the existing codebaseColumns projection
instead of * so the allow_env_keys column is excluded — locate the queries in
functions like the create/insert method (where RETURNING * is used) and any
get/list/select helpers and swap their RETURNING/SELECT * to reference the
codebaseColumns symbol (or the same column list) so responses no longer include
allow_env_keys.

);
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
Loading
Loading