feat(config): add custom_agents overrides and strict agent validation

This commit is contained in:
edxeth
2026-02-18 19:57:35 +01:00
parent 15519b9580
commit ae12f2e9d2
15 changed files with 1120 additions and 27 deletions
+14
View File
@@ -1,11 +1,25 @@
export {
OhMyOpenCodeConfigSchema,
AgentOverrideConfigSchema,
AgentOverridesSchema,
CustomAgentOverridesSchema,
McpNameSchema,
AgentNameSchema,
OverridableAgentNameSchema,
HookNameSchema,
BuiltinCommandNameSchema,
SisyphusAgentConfigSchema,
ExperimentalConfigSchema,
RalphLoopConfigSchema,
TmuxConfigSchema,
TmuxLayoutSchema,
} from "./schema"
export type {
OhMyOpenCodeConfig,
AgentOverrideConfig,
AgentOverrides,
CustomAgentOverrides,
McpName,
AgentName,
HookName,
+32
View File
@@ -0,0 +1,32 @@
import { describe, expect, test } from "bun:test"
import { createOhMyOpenCodeJsonSchema } from "../../script/build-schema-document"
function asRecord(value: unknown): Record<string, unknown> | undefined {
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined
}
describe("schema document generation", () => {
test("custom_agents schema allows arbitrary custom agent keys with override shape", () => {
// given
const schema = createOhMyOpenCodeJsonSchema()
// when
const rootProperties = asRecord(schema.properties)
const agentsSchema = asRecord(rootProperties?.agents)
const customAgentsSchema = asRecord(rootProperties?.custom_agents)
const customPropertyNames = asRecord(customAgentsSchema?.propertyNames)
const customAdditionalProperties = asRecord(customAgentsSchema?.additionalProperties)
const customAgentProperties = asRecord(customAdditionalProperties?.properties)
// then
expect(agentsSchema).toBeDefined()
expect(agentsSchema?.additionalProperties).toBeFalse()
expect(customAgentsSchema).toBeDefined()
expect(customPropertyNames?.pattern).toBeDefined()
expect(customAdditionalProperties).toBeDefined()
expect(customAgentProperties?.model).toEqual({ type: "string" })
expect(customAgentProperties?.temperature).toEqual(
expect.objectContaining({ type: "number" }),
)
})
})
+73
View File
@@ -530,6 +530,79 @@ describe("Sisyphus-Junior agent override", () => {
expect(result.data.agents?.momus?.category).toBe("quick")
}
})
test("schema accepts custom_agents override keys", () => {
// given
const config = {
custom_agents: {
translator: {
model: "google/gemini-3-flash-preview",
temperature: 0,
},
},
}
// when
const result = OhMyOpenCodeConfigSchema.safeParse(config)
// then
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.custom_agents?.translator?.model).toBe("google/gemini-3-flash-preview")
expect(result.data.custom_agents?.translator?.temperature).toBe(0)
}
})
test("schema rejects unknown keys under agents", () => {
// given
const config = {
agents: {
sisyphuss: {
model: "openai/gpt-5.3-codex",
},
},
}
// when
const result = OhMyOpenCodeConfigSchema.safeParse(config)
// then
expect(result.success).toBe(false)
})
test("schema rejects built-in agent names under custom_agents", () => {
// given
const config = {
custom_agents: {
sisyphus: {
model: "openai/gpt-5.3-codex",
},
},
}
// when
const result = OhMyOpenCodeConfigSchema.safeParse(config)
// then
expect(result.success).toBe(false)
})
test("schema rejects built-in agent names under custom_agents case-insensitively", () => {
// given
const config = {
custom_agents: {
Sisyphus: {
model: "openai/gpt-5.3-codex",
},
},
}
// when
const result = OhMyOpenCodeConfigSchema.safeParse(config)
// then
expect(result.success).toBe(false)
})
})
describe("BrowserAutomationProviderSchema", () => {
+34 -2
View File
@@ -1,5 +1,6 @@
import { z } from "zod"
import { FallbackModelsSchema } from "./fallback-models"
import { OverridableAgentNameSchema } from "./agent-names"
import { AgentPermissionSchema } from "./internal/permission"
export const AgentOverrideConfigSchema = z.object({
@@ -55,7 +56,7 @@ export const AgentOverrideConfigSchema = z.object({
.optional(),
})
export const AgentOverridesSchema = z.object({
const BuiltinAgentOverridesSchema = z.object({
build: AgentOverrideConfigSchema.optional(),
plan: AgentOverrideConfigSchema.optional(),
sisyphus: AgentOverrideConfigSchema.optional(),
@@ -70,7 +71,38 @@ export const AgentOverridesSchema = z.object({
explore: AgentOverrideConfigSchema.optional(),
"multimodal-looker": AgentOverrideConfigSchema.optional(),
atlas: AgentOverrideConfigSchema.optional(),
})
}).strict()
export const AgentOverridesSchema = BuiltinAgentOverridesSchema
const RESERVED_CUSTOM_AGENT_NAMES = OverridableAgentNameSchema.options
const RESERVED_CUSTOM_AGENT_NAME_SET = new Set(
RESERVED_CUSTOM_AGENT_NAMES.map((name) => name.toLowerCase()),
)
const RESERVED_CUSTOM_AGENT_NAME_PATTERN = new RegExp(
`^(?!(?:${RESERVED_CUSTOM_AGENT_NAMES.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})$).+`,
)
export const CustomAgentOverridesSchema = z
.record(
z.string().regex(
RESERVED_CUSTOM_AGENT_NAME_PATTERN,
"custom_agents key cannot reuse built-in agent override name",
),
AgentOverrideConfigSchema,
)
.superRefine((value, ctx) => {
for (const key of Object.keys(value)) {
if (RESERVED_CUSTOM_AGENT_NAME_SET.has(key.toLowerCase())) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [key],
message: "custom_agents key cannot reuse built-in agent override name",
})
}
}
})
export type AgentOverrideConfig = z.infer<typeof AgentOverrideConfigSchema>
export type AgentOverrides = z.infer<typeof AgentOverridesSchema>
export type CustomAgentOverrides = z.infer<typeof CustomAgentOverridesSchema>
+2 -1
View File
@@ -1,7 +1,7 @@
import { z } from "zod"
import { AnyMcpNameSchema } from "../../mcp/types"
import { BuiltinAgentNameSchema, BuiltinSkillNameSchema } from "./agent-names"
import { AgentOverridesSchema } from "./agent-overrides"
import { AgentOverridesSchema, CustomAgentOverridesSchema } from "./agent-overrides"
import { BabysittingConfigSchema } from "./babysitting"
import { BackgroundTaskConfigSchema } from "./background-task"
import { BrowserAutomationConfigSchema } from "./browser-automation"
@@ -38,6 +38,7 @@ export const OhMyOpenCodeConfigSchema = z.object({
/** Enable model fallback on API errors (default: false). Set to true to enable automatic model switching when model errors occur. */
model_fallback: z.boolean().optional(),
agents: AgentOverridesSchema.optional(),
custom_agents: CustomAgentOverridesSchema.optional(),
categories: CategoriesConfigSchema.optional(),
claude_code: ClaudeCodeConfigSchema.optional(),
sisyphus_agent: SisyphusAgentConfigSchema.optional(),