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
16 changes: 12 additions & 4 deletions src/backends/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import { registerEngineSettingsSchema } from '../config/engineSettings.js';
import { ClaudeCodeEngine } from './claude-code/index.js';
import { CodexEngine } from './codex/index.js';
import { LlmistEngine } from './llmist/index.js';
import { OpenCodeEngine } from './opencode/index.js';
import { getEngine, registerEngine } from './registry.js';

function registerEngineWithSettings(engine: import('./types.js').AgentEngine): void {
registerEngine(engine);
if (engine.getSettingsSchema) {
registerEngineSettingsSchema(engine.definition.id, engine.getSettingsSchema());
}
}

export function registerBuiltInEngines(): void {
if (!getEngine('llmist')) {
registerEngine(new LlmistEngine());
registerEngineWithSettings(new LlmistEngine());
}
if (!getEngine('claude-code')) {
registerEngine(new ClaudeCodeEngine());
registerEngineWithSettings(new ClaudeCodeEngine());
}
if (!getEngine('codex')) {
registerEngine(new CodexEngine());
registerEngineWithSettings(new CodexEngine());
}
if (!getEngine('opencode')) {
registerEngine(new OpenCodeEngine());
registerEngineWithSettings(new OpenCodeEngine());
}
}
10 changes: 9 additions & 1 deletion src/backends/codex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import { logLlmCall } from '../shared/llmCallLogger.js';
import type { AgentEngine, AgentEngineResult, AgentExecutionPlan, LogWriter } from '../types.js';
import { buildEnv } from './env.js';
import { CODEX_MODEL_IDS, DEFAULT_CODEX_MODEL } from './models.js';
import { assertHeadlessCodexSettings, resolveCodexSettings } from './settings.js';
import {
CodexSettingsSchema,
assertHeadlessCodexSettings,
resolveCodexSettings,
} from './settings.js';

const CODEX_AUTH_DIR = join(homedir(), '.codex');
const CODEX_AUTH_FILE = join(CODEX_AUTH_DIR, 'auth.json');
Expand Down Expand Up @@ -489,6 +493,10 @@ export class CodexEngine implements AgentEngine {
return resolveCodexModel(cascadeModel);
}

getSettingsSchema() {
return CodexSettingsSchema;
}

async beforeExecute(plan: AgentExecutionPlan): Promise<void> {
this._adapterLifecycleActive = true;
this._originalAuthJson = await writeCodexAuthFile(plan.projectSecrets, plan.logWriter);
Expand Down
13 changes: 11 additions & 2 deletions src/backends/codex/settings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { CodexSettingsSchema, getEngineSettings } from '../../config/engineSettings.js';
import type { CodexSettings } from '../../config/engineSettings.js';
import { z } from 'zod';
import { getEngineSettings } from '../../config/engineSettings.js';
import type { ProjectConfig } from '../../types/index.js';

export const CodexSettingsSchema = z.object({
approvalPolicy: z.enum(['never', 'on-request', 'untrusted']).optional(),
sandboxMode: z.enum(['read-only', 'workspace-write', 'danger-full-access']).optional(),
reasoningEffort: z.enum(['low', 'medium', 'high', 'xhigh']).optional(),
webSearch: z.boolean().optional(),
});

export type CodexSettings = z.infer<typeof CodexSettingsSchema>;

export interface ResolvedCodexSettings
extends Required<Pick<CodexSettings, 'approvalPolicy' | 'sandboxMode' | 'webSearch'>> {
reasoningEffort?: CodexSettings['reasoningEffort'];
Expand Down
6 changes: 5 additions & 1 deletion src/backends/opencode/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { logLlmCall } from '../shared/llmCallLogger.js';
import type { AgentEngine, AgentEngineResult, AgentExecutionPlan } from '../types.js';
import { buildEnv } from './env.js';
import { DEFAULT_OPENCODE_MODEL } from './models.js';
import { resolveOpenCodeSettings } from './settings.js';
import { OpenCodeSettingsSchema, resolveOpenCodeSettings } from './settings.js';

function appendEngineLog(path: string | undefined, chunk: string): void {
if (!path || chunk.length === 0) return;
Expand Down Expand Up @@ -805,6 +805,10 @@ export class OpenCodeEngine implements AgentEngine {
return resolveOpenCodeModel(cascadeModel);
}

getSettingsSchema() {
return OpenCodeSettingsSchema;
}

async afterExecute(plan: AgentExecutionPlan, _result: AgentEngineResult): Promise<void> {
// Clean up offloaded context files — idempotent, safe to call from adapter hook.
// Server process and session cleanup happen inside execute()'s finally block
Expand Down
10 changes: 8 additions & 2 deletions src/backends/opencode/settings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { OpenCodeSettingsSchema, getEngineSettings } from '../../config/engineSettings.js';
import type { OpenCodeSettings } from '../../config/engineSettings.js';
import { z } from 'zod';
import { getEngineSettings } from '../../config/engineSettings.js';
import type { ProjectConfig } from '../../types/index.js';

export const OpenCodeSettingsSchema = z.object({
webSearch: z.boolean().optional(),
});

export type OpenCodeSettings = z.infer<typeof OpenCodeSettingsSchema>;

export interface ResolvedOpenCodeSettings extends Required<Pick<OpenCodeSettings, 'webSearch'>> {}

export function resolveOpenCodeSettings(project: ProjectConfig): ResolvedOpenCodeSettings {
Expand Down
7 changes: 7 additions & 0 deletions src/backends/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { z } from 'zod';
import type { AgentInput, CascadeConfig, ProjectConfig } from '../types/index.js';
import type { CompletionRequirements } from './completion.js';

Expand Down Expand Up @@ -153,6 +154,12 @@ export interface AgentEngine {
* Engines that pass the model through unchanged (e.g., LLMist) do not need to implement it.
*/
resolveModel?(cascadeModel: string): string;
/**
* Optional method that returns the Zod schema for this engine's settings.
* Engines that have configurable settings implement this method so the schema
* can be registered dynamically during bootstrap.
*/
getSettingsSchema?(): z.ZodType<Record<string, unknown>>;
/**
* Optional hook called by the adapter before engine.execute().
* Use for engine-specific environment setup (e.g., writing auth files, checking directories).
Expand Down
50 changes: 32 additions & 18 deletions src/config/engineSettings.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,44 @@
import { z } from 'zod';

export const CodexSettingsSchema = z.object({
approvalPolicy: z.enum(['never', 'on-request', 'untrusted']).optional(),
sandboxMode: z.enum(['read-only', 'workspace-write', 'danger-full-access']).optional(),
reasoningEffort: z.enum(['low', 'medium', 'high', 'xhigh']).optional(),
webSearch: z.boolean().optional(),
});

export const OpenCodeSettingsSchema = z.object({
webSearch: z.boolean().optional(),
});

const ENGINE_SETTINGS_SCHEMAS: Record<string, z.ZodType<Record<string, unknown>>> = {
codex: CodexSettingsSchema,
opencode: OpenCodeSettingsSchema,
};
// Re-export schemas from engine directories for backward compatibility.
export { CodexSettingsSchema } from '../backends/codex/settings.js';
export type { CodexSettings } from '../backends/codex/settings.js';
export { OpenCodeSettingsSchema } from '../backends/opencode/settings.js';
export type { OpenCodeSettings } from '../backends/opencode/settings.js';

/**
* Dynamic registry of engine settings schemas.
* Engines register their schema during bootstrap via registerEngineSettingsSchema().
*/
const ENGINE_SETTINGS_SCHEMAS: Map<string, z.ZodType<Record<string, unknown>>> = new Map();

/**
* Register a settings schema for an engine. Called during bootstrap when an engine
* implementing getSettingsSchema() is registered.
*/
export function registerEngineSettingsSchema(
engineId: string,
schema: z.ZodType<Record<string, unknown>>,
): void {
ENGINE_SETTINGS_SCHEMAS.set(engineId, schema);
}

/**
* Retrieve the registered settings schema for an engine, if any.
*/
export function getEngineSettingsSchema(
engineId: string,
): z.ZodType<Record<string, unknown>> | undefined {
return ENGINE_SETTINGS_SCHEMAS.get(engineId);
}

const EngineSettingsValueSchema = z.record(z.string(), z.unknown());

export const EngineSettingsSchema = z
.record(z.string(), EngineSettingsValueSchema)
.superRefine((settings, ctx) => {
for (const [engineId, rawSettings] of Object.entries(settings)) {
const schema = ENGINE_SETTINGS_SCHEMAS[engineId];
const schema = ENGINE_SETTINGS_SCHEMAS.get(engineId);
if (!schema) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
Expand All @@ -45,8 +61,6 @@ export const EngineSettingsSchema = z
})
.transform((settings) => normalizeEngineSettings(settings) ?? {});

export type CodexSettings = z.infer<typeof CodexSettingsSchema>;
export type OpenCodeSettings = z.infer<typeof OpenCodeSettingsSchema>;
export type EngineSettings = Record<string, Record<string, unknown>>;
type EngineSettingsInput = Record<string, Record<string, unknown> | undefined>;

Expand Down
7 changes: 6 additions & 1 deletion tests/unit/api/routers/projects.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TRPCError } from '@trpc/server';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import type { TRPCContext } from '../../../../src/api/trpc.js';
import { registerBuiltInEngines } from '../../../../src/backends/bootstrap.js';
import { createMockUser } from '../../../helpers/factories.js';

const mockListProjectsForOrg = vi.fn();
Expand Down Expand Up @@ -64,6 +65,10 @@ function createCaller(ctx: TRPCContext) {

const mockUser = createMockUser();

beforeAll(() => {
registerBuiltInEngines();
});

describe('projectsRouter', () => {
beforeEach(() => {
mockDbSelect.mockReturnValue({ from: mockDbFrom });
Expand Down
7 changes: 6 additions & 1 deletion tests/unit/config/schema.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { describe, expect, it } from 'vitest';
import { beforeAll, describe, expect, it } from 'vitest';
import { registerBuiltInEngines } from '../../../src/backends/bootstrap.js';
import { ProjectConfigSchema, validateConfig } from '../../../src/config/schema.js';

beforeAll(() => {
registerBuiltInEngines();
});

describe.concurrent('ProjectConfigSchema', () => {
it('validates a valid project config', () => {
const config = {
Expand Down
Loading