Skip to content

Commit dd8b571

Browse files
committed
Actually validate subagents, locally + database
1 parent d23c454 commit dd8b571

File tree

7 files changed

+135
-55
lines changed

7 files changed

+135
-55
lines changed

backend/src/templates/agent-registry.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ async function fetchAgentFromDatabase(parsedAgentId: {
8080
{ ...rawAgentData, id: agentId },
8181
{
8282
filePath: `${publisherId}/${agentId}@${agentConfig.version}`,
83-
skipSubagentValidation: true,
8483
},
8584
)
8685

common/src/templates/agent-validation.ts

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import { convertJsonSchemaToZod } from 'zod-from-json-schema'
22

3-
import {
4-
formatSpawnableAgentError,
5-
validateSpawnableAgents,
6-
} from '../util/agent-template-validation'
3+
import { validateSpawnableAgents } from '../util/agent-template-validation'
74
import { logger } from '../util/logger'
85
import {
96
DynamicAgentDefinitionSchema,
@@ -22,8 +19,9 @@ export interface DynamicAgentValidationError {
2219
*/
2320
export function collectAgentIds(
2421
agentTemplates: Record<string, DynamicAgentTemplate> = {},
25-
): string[] {
22+
): { agentIds: string[]; spawnableAgentIds: string[] } {
2623
const agentIds: string[] = []
24+
const spawnableAgentIds: string[] = []
2725
const jsonFiles = Object.keys(agentTemplates)
2826

2927
for (const filePath of jsonFiles) {
@@ -37,6 +35,9 @@ export function collectAgentIds(
3735
if (content.id && typeof content.id === 'string') {
3836
agentIds.push(content.id)
3937
}
38+
if (Array.isArray(content.spawnableAgents)) {
39+
spawnableAgentIds.push(...content.spawnableAgents)
40+
}
4041
} catch (error) {
4142
// Log but don't fail the collection process for other errors
4243
logger.debug(
@@ -46,7 +47,29 @@ export function collectAgentIds(
4647
}
4748
}
4849

49-
return agentIds
50+
return { agentIds, spawnableAgentIds }
51+
}
52+
53+
export async function validateAgentsWithSpawnableAgents(
54+
agentTemplates: Record<string, any> = {},
55+
): Promise<{
56+
templates: Record<string, AgentTemplate>
57+
dynamicTemplates: Record<string, DynamicAgentTemplate>
58+
validationErrors: DynamicAgentValidationError[]
59+
}> {
60+
const { agentIds, spawnableAgentIds } = collectAgentIds(agentTemplates)
61+
const { validationErrors } = await validateSpawnableAgents(
62+
spawnableAgentIds,
63+
agentIds,
64+
)
65+
if (validationErrors.length > 0) {
66+
return {
67+
templates: {},
68+
dynamicTemplates: {},
69+
validationErrors,
70+
}
71+
}
72+
return validateAgents(agentTemplates)
5073
}
5174

5275
/**
@@ -73,10 +96,7 @@ export function validateAgents(agentTemplates: Record<string, any> = {}): {
7396

7497
const agentKeys = Object.keys(agentTemplates)
7598

76-
// Pass 1: Collect all agent IDs from template files
77-
const dynamicAgentIds = collectAgentIds(agentTemplates)
78-
79-
// Pass 2: Load and validate each agent template
99+
// Load and validate each agent template
80100
for (const agentKey of agentKeys) {
81101
const content = agentTemplates[agentKey]
82102
try {
@@ -86,7 +106,6 @@ export function validateAgents(agentTemplates: Record<string, any> = {}): {
86106

87107
const validationResult = validateSingleAgent(content, {
88108
filePath: agentKey,
89-
dynamicAgentIds,
90109
})
91110

92111
if (!validationResult.success) {
@@ -154,21 +173,15 @@ export function validateAgents(agentTemplates: Record<string, any> = {}): {
154173
export function validateSingleAgent(
155174
template: any,
156175
options?: {
157-
dynamicAgentIds?: string[]
158176
filePath?: string
159-
skipSubagentValidation?: boolean
160177
},
161178
): {
162179
success: boolean
163180
agentTemplate?: AgentTemplate
164181
dynamicAgentTemplate?: DynamicAgentTemplate
165182
error?: string
166183
} {
167-
const {
168-
filePath,
169-
skipSubagentValidation = true,
170-
dynamicAgentIds = [],
171-
} = options || {}
184+
const { filePath } = options || {}
172185

173186
try {
174187
// First validate against the Zod schema
@@ -203,23 +216,6 @@ export function validateSingleAgent(
203216
}
204217
}
205218

206-
// Validate spawnable agents (skip if requested, e.g., for database agents)
207-
if (!skipSubagentValidation) {
208-
const spawnableAgentValidation = validateSpawnableAgents(
209-
validatedConfig.spawnableAgents,
210-
dynamicAgentIds,
211-
)
212-
if (!spawnableAgentValidation.valid) {
213-
return {
214-
success: false,
215-
error: formatSpawnableAgentError(
216-
spawnableAgentValidation.invalidAgents,
217-
spawnableAgentValidation.availableAgents,
218-
),
219-
}
220-
}
221-
}
222-
223219
// Convert schemas and handle validation errors
224220
let inputSchema: AgentTemplate['inputSchema']
225221
try {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { db } from '@codebuff/common/db'
2+
import * as schema from '@codebuff/common/db/schema'
3+
import { and, eq } from 'drizzle-orm'
4+
5+
export async function fetchAgent(
6+
agentId: string,
7+
version: string,
8+
publisher: string,
9+
) {
10+
// Find the agent template
11+
const agent = await db
12+
.select()
13+
.from(schema.agentConfig)
14+
.where(
15+
and(
16+
eq(schema.agentConfig.id, agentId),
17+
eq(schema.agentConfig.version, version),
18+
eq(schema.agentConfig.publisher_id, publisher),
19+
),
20+
)
21+
.then((rows) => rows[0])
22+
23+
return agent
24+
}

common/src/util/agent-id-parsing.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export function parseAgentId(fullAgentId: string): {
88
publisherId?: string
99
agentId?: string
1010
version?: string
11+
givenAgentId: string
1112
} {
1213
// Check if it's in the publisher/agent-id[@version] format
1314
const parts = fullAgentId.split('/')
@@ -17,40 +18,65 @@ export function parseAgentId(fullAgentId: string): {
1718
const [publisherId, agentNameWithVersion] = parts
1819

1920
if (!publisherId || !agentNameWithVersion) {
20-
return { publisherId: undefined, agentId: undefined, version: undefined }
21+
return {
22+
publisherId: undefined,
23+
agentId: undefined,
24+
version: undefined,
25+
givenAgentId: fullAgentId,
26+
}
2127
}
2228

2329
// Check for version suffix
2430
const versionMatch = agentNameWithVersion.match(/^(.+)@(.+)$/)
2531
if (versionMatch) {
2632
const [, agentId, version] = versionMatch
27-
return { publisherId, agentId, version }
33+
return { publisherId, agentId, version, givenAgentId: fullAgentId }
2834
}
2935

30-
return { publisherId, agentId: agentNameWithVersion }
36+
return {
37+
publisherId,
38+
agentId: agentNameWithVersion,
39+
givenAgentId: fullAgentId,
40+
}
3141
} else if (parts.length === 1) {
3242
// Just agent name (for backward compatibility)
3343
const agentNameWithVersion = parts[0]
3444

3545
if (!agentNameWithVersion) {
36-
return { publisherId: undefined, agentId: undefined, version: undefined }
46+
return {
47+
publisherId: undefined,
48+
agentId: undefined,
49+
version: undefined,
50+
givenAgentId: fullAgentId,
51+
}
3752
}
3853

3954
// Check for version suffix
4055
const versionMatch = agentNameWithVersion.match(/^(.+)@(.+)$/)
4156
if (versionMatch) {
4257
const [, agentId, version] = versionMatch
43-
return { publisherId: undefined, agentId, version }
58+
return {
59+
publisherId: undefined,
60+
agentId,
61+
version,
62+
givenAgentId: fullAgentId,
63+
}
4464
}
4565

4666
return {
4767
publisherId: undefined,
4868
agentId: agentNameWithVersion,
4969
version: undefined,
70+
givenAgentId: fullAgentId,
5071
}
5172
}
5273

53-
return { publisherId: undefined, agentId: undefined, version: undefined }
74+
return {
75+
publisherId: undefined,
76+
agentId: undefined,
77+
version: undefined,
78+
givenAgentId: fullAgentId,
79+
}
5480
}
5581

5682
/**

common/src/util/agent-template-validation.ts

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import { DynamicAgentValidationError } from 'src/templates/agent-validation'
2+
import { fetchAgent } from '../templates/fetch-agent'
13
import { AgentTemplateTypes } from '../types/session-state'
4+
import { parseAgentId } from './agent-id-parsing'
25

36
export interface SubagentValidationResult {
47
valid: boolean
@@ -9,24 +12,56 @@ export interface SubagentValidationResult {
912
* Centralized validation for spawnable agents.
1013
* Validates that all spawnable agents reference valid agent types.
1114
*/
12-
export function validateSpawnableAgents(
15+
export async function validateSpawnableAgents(
1316
spawnableAgents: string[],
1417
dynamicAgentIds: string[],
15-
): SubagentValidationResult & { availableAgents: string[] } {
18+
): Promise<
19+
SubagentValidationResult & {
20+
availableAgents: string[]
21+
validationErrors: DynamicAgentValidationError[]
22+
}
23+
> {
1624
// Build complete list of available agent types (normalized)
1725
const availableAgentTypes = [
1826
...Object.values(AgentTemplateTypes),
1927
...dynamicAgentIds,
2028
]
29+
const parsedIds = spawnableAgents.map((id) => parseAgentId(id))
30+
const invalidIds: string[] = []
31+
const validationErrors: DynamicAgentValidationError[] = []
32+
for (const id of parsedIds) {
33+
const { publisherId, agentId, version, givenAgentId } = id
34+
35+
if (availableAgentTypes.includes(givenAgentId)) {
36+
// Agent provided by dynamic definitions.
37+
continue
38+
}
39+
if (!publisherId || !agentId || !version) {
40+
invalidIds.push(givenAgentId)
41+
validationErrors.push({
42+
filePath: givenAgentId,
43+
message: `Invalid agent ID: ${givenAgentId}. Not found. You must include the publisher, agent id, and version if the agent is not defined locally.`,
44+
})
45+
continue
46+
}
2147

22-
// Find invalid agents (those not in available types after normalization)
23-
const invalidAgents = spawnableAgents.filter(
24-
(agent, index) => !availableAgentTypes.includes(spawnableAgents[index]),
25-
)
48+
// Check if agent exists in database.
49+
const agent = await fetchAgent(agentId, version, publisherId)
50+
if (!agent) {
51+
invalidIds.push(givenAgentId)
52+
validationErrors.push({
53+
filePath: givenAgentId,
54+
message: `Invalid agent ID: ${givenAgentId}. Agent not found in database.`,
55+
})
56+
continue
57+
}
58+
availableAgentTypes.push(givenAgentId)
59+
}
2660

2761
return {
28-
valid: invalidAgents.length === 0,
29-
invalidAgents,
62+
valid: validationErrors.length === 0,
63+
invalidAgents: invalidIds,
64+
validationErrors,
3065
availableAgents: availableAgentTypes,
3166
}
3267
}

web/src/app/api/agents/publish/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import db from '@codebuff/common/db'
22
import * as schema from '@codebuff/common/db/schema'
3-
import { validateAgents } from '@codebuff/common/templates/agent-validation'
3+
import { validateAgentsWithSpawnableAgents } from '@codebuff/common/templates/agent-validation'
44
import { publishAgentsRequestSchema } from '@codebuff/common/types/api/agents/publish'
55
import {
66
checkAuthToken,
@@ -70,7 +70,8 @@ export async function POST(request: NextRequest) {
7070
{} as Record<string, any>
7171
)
7272

73-
const { validationErrors, dynamicTemplates } = validateAgents(agentMap)
73+
const { validationErrors, dynamicTemplates } =
74+
await validateAgentsWithSpawnableAgents(agentMap)
7475
const agents = Object.values(dynamicTemplates)
7576

7677
if (validationErrors.length > 0) {
@@ -132,7 +133,6 @@ export async function POST(request: NextRequest) {
132133

133134
const requestedPublisherId = publisherIds[0]!
134135

135-
136136
// Verify user has access to the requested publisher
137137
const publisherResult = await db
138138
.select({

web/src/app/api/agents/validate/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { validateAgents } from '@codebuff/common/templates/agent-validation'
1+
import { validateAgentsWithSpawnableAgents } from '@codebuff/common/templates/agent-validation'
22
import { NextResponse } from 'next/server'
33

44
import { logger } from '@/util/logger'
@@ -33,7 +33,7 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
3333
agentDefinitions.map((config) => [config.id, config])
3434
)
3535
const { templates: configs, validationErrors } =
36-
validateAgents(definitionsObject)
36+
await validateAgentsWithSpawnableAgents(definitionsObject)
3737

3838
if (validationErrors.length > 0) {
3939
logger.warn(

0 commit comments

Comments
 (0)