kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
pull/715/head
rodzic
8ed18982ad
commit
22d389f759
|
@ -1,4 +1,4 @@
|
||||||
import { validateAgenticProjectConfig } from '@agentic/platform'
|
import { resolveAgenticProjectConfig } from '@agentic/platform'
|
||||||
import { assert, parseZodSchema, sha256 } from '@agentic/platform-core'
|
import { assert, parseZodSchema, sha256 } from '@agentic/platform-core'
|
||||||
import { validators } from '@agentic/platform-validators'
|
import { validators } from '@agentic/platform-validators'
|
||||||
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
||||||
|
@ -111,9 +111,8 @@ export function registerV1DeploymentsCreateDeployment(
|
||||||
// - origin API base URL
|
// - origin API base URL
|
||||||
// - origin adapter OpenAPI or MCP specs
|
// - origin adapter OpenAPI or MCP specs
|
||||||
// - tool definitions
|
// - tool definitions
|
||||||
const agenticProjectConfig = await validateAgenticProjectConfig(body, {
|
const agenticProjectConfig = await resolveAgenticProjectConfig(body, {
|
||||||
label: `deployment "${deploymentIdentifier}"`,
|
label: `deployment "${deploymentIdentifier}"`,
|
||||||
strip: true,
|
|
||||||
logger
|
logger
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
agenticProjectConfigSchema,
|
agenticProjectConfigSchema,
|
||||||
type OriginAdapter,
|
type OriginAdapter,
|
||||||
type PricingPlanList,
|
type PricingPlanList,
|
||||||
|
resolvedAgenticProjectConfigSchema,
|
||||||
type Tool,
|
type Tool,
|
||||||
type ToolConfig
|
type ToolConfig
|
||||||
} from '@agentic/platform-schemas'
|
} from '@agentic/platform-schemas'
|
||||||
|
@ -149,16 +150,16 @@ export const deploymentSelectSchema = createSelectSchema(deployments, {
|
||||||
message: 'Invalid deployment hash'
|
message: 'Invalid deployment hash'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
version: agenticProjectConfigSchema.shape.version,
|
version: resolvedAgenticProjectConfigSchema.shape.version,
|
||||||
description: agenticProjectConfigSchema.shape.description,
|
description: resolvedAgenticProjectConfigSchema.shape.description,
|
||||||
readme: agenticProjectConfigSchema.shape.readme,
|
readme: resolvedAgenticProjectConfigSchema.shape.readme,
|
||||||
iconUrl: agenticProjectConfigSchema.shape.iconUrl,
|
iconUrl: resolvedAgenticProjectConfigSchema.shape.iconUrl,
|
||||||
sourceUrl: agenticProjectConfigSchema.shape.sourceUrl,
|
sourceUrl: resolvedAgenticProjectConfigSchema.shape.sourceUrl,
|
||||||
originAdapter: agenticProjectConfigSchema.shape.originAdapter,
|
originAdapter: resolvedAgenticProjectConfigSchema.shape.originAdapter,
|
||||||
pricingPlans: agenticProjectConfigSchema.shape.pricingPlans,
|
pricingPlans: resolvedAgenticProjectConfigSchema.shape.pricingPlans,
|
||||||
pricingIntervals: agenticProjectConfigSchema.shape.pricingIntervals,
|
pricingIntervals: resolvedAgenticProjectConfigSchema.shape.pricingIntervals,
|
||||||
tools: agenticProjectConfigSchema.shape.toolConfigs,
|
tools: resolvedAgenticProjectConfigSchema.shape.tools,
|
||||||
toolConfigs: agenticProjectConfigSchema.shape.toolConfigs
|
toolConfigs: resolvedAgenticProjectConfigSchema.shape.toolConfigs
|
||||||
})
|
})
|
||||||
.omit({
|
.omit({
|
||||||
originUrl: true
|
originUrl: true
|
||||||
|
|
|
@ -32,6 +32,9 @@ export function registerDebugCommand({ program, logger }: Context) {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: we may want to resolve the resulting agentic config so we see
|
||||||
|
// the inferred `tools` (and `toolToOperationMap` for mcp servers)
|
||||||
|
|
||||||
logger.log(config)
|
logger.log(config)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "run-s test:*",
|
"test": "run-s test:*",
|
||||||
"test:lint": "eslint .",
|
"test:lint": "eslint .",
|
||||||
"test:typecheck": "tsc --noEmit",
|
"test:typecheck": "tsc --noEmit"
|
||||||
"test:unit": "vitest run"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentic/platform-core": "workspace:*",
|
"@agentic/platform-core": "workspace:*",
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
export * from './define-config'
|
export * from './define-config'
|
||||||
|
export * from './resolve-agentic-project-config'
|
||||||
export * from './validate-agentic-project-config'
|
export * from './validate-agentic-project-config'
|
||||||
export * from './validate-origin-adapter'
|
|
||||||
export * from './validate-tools'
|
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
import type {
|
||||||
|
MCPOriginAdapter,
|
||||||
|
MCPOriginAdapterConfig,
|
||||||
|
Tool
|
||||||
|
} from '@agentic/platform-schemas'
|
||||||
|
import { assert } from '@agentic/platform-core'
|
||||||
|
import { Client as McpClient } from '@modelcontextprotocol/sdk/client/index.js'
|
||||||
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
||||||
|
|
||||||
|
export async function resolveMCPOriginAdapter({
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
originUrl,
|
||||||
|
originAdapter,
|
||||||
|
label
|
||||||
|
}: {
|
||||||
|
name: string
|
||||||
|
originUrl: string
|
||||||
|
originAdapter: MCPOriginAdapterConfig
|
||||||
|
label: string
|
||||||
|
version: string
|
||||||
|
}): Promise<{
|
||||||
|
originAdapter: MCPOriginAdapter
|
||||||
|
tools?: Tool[]
|
||||||
|
}> {
|
||||||
|
assert(
|
||||||
|
originAdapter.type === 'mcp',
|
||||||
|
400,
|
||||||
|
`Invalid origin adapter type "${originAdapter.type}" for ${label}`
|
||||||
|
)
|
||||||
|
const transport = new SSEClientTransport(new URL(originUrl))
|
||||||
|
const client = new McpClient({ name, version })
|
||||||
|
await client.connect(transport)
|
||||||
|
|
||||||
|
const serverInfo = {
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
...client.getServerVersion(),
|
||||||
|
capabilities: client.getServerCapabilities(),
|
||||||
|
instructions: client.getInstructions()
|
||||||
|
}
|
||||||
|
|
||||||
|
const listToolsResponse = await client.listTools()
|
||||||
|
|
||||||
|
// TODO: Validate MCP tools
|
||||||
|
const tools = listToolsResponse.tools
|
||||||
|
|
||||||
|
return {
|
||||||
|
tools,
|
||||||
|
originAdapter: {
|
||||||
|
...originAdapter,
|
||||||
|
serverInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import type {
|
||||||
|
OpenAPIOriginAdapter,
|
||||||
|
OpenAPIOriginAdapterConfig,
|
||||||
|
Tool
|
||||||
|
} from '@agentic/platform-schemas'
|
||||||
|
import { assert, type Logger } from '@agentic/platform-core'
|
||||||
|
import {
|
||||||
|
getToolsFromOpenAPISpec,
|
||||||
|
validateOpenAPISpec
|
||||||
|
} from '@agentic/platform-openapi'
|
||||||
|
|
||||||
|
export async function resolveOpenAPIOriginAdapter({
|
||||||
|
originAdapter,
|
||||||
|
label,
|
||||||
|
cwd,
|
||||||
|
logger
|
||||||
|
}: {
|
||||||
|
originAdapter: OpenAPIOriginAdapterConfig
|
||||||
|
label: string
|
||||||
|
cwd?: URL
|
||||||
|
logger?: Logger
|
||||||
|
}): Promise<{
|
||||||
|
originAdapter: OpenAPIOriginAdapter
|
||||||
|
tools?: Tool[]
|
||||||
|
}> {
|
||||||
|
assert(
|
||||||
|
originAdapter.type === 'openapi',
|
||||||
|
400,
|
||||||
|
`Invalid origin adapter type "${originAdapter.type}" for ${label}`
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
originAdapter.spec,
|
||||||
|
400,
|
||||||
|
`OpenAPI spec is required for ${label} with origin adapter type set to "openapi"`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate and normalize the OpenAPI spec
|
||||||
|
const openapiSpec = await validateOpenAPISpec(originAdapter.spec, {
|
||||||
|
cwd,
|
||||||
|
logger
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove origin servers from the OpenAPI spec.
|
||||||
|
// TODO: Ensure that `originUrl` matches any origin servers in the openapi spec?
|
||||||
|
delete openapiSpec.servers
|
||||||
|
|
||||||
|
// TODO: Additional, agentic-specific validation of the OpenAPI spec's
|
||||||
|
// operations to ensure they are valid tools.
|
||||||
|
|
||||||
|
// TODO: Simplify OpenAPI spec by removing any query params and headers
|
||||||
|
// specific to the Agentic API gateway.
|
||||||
|
|
||||||
|
// TODO: Extract tool definitions from OpenAPI operationIds
|
||||||
|
|
||||||
|
const dereferencedOpenAPISpec = await validateOpenAPISpec(
|
||||||
|
originAdapter.spec,
|
||||||
|
{
|
||||||
|
cwd,
|
||||||
|
dereference: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { tools, toolToOperationMap } = await getToolsFromOpenAPISpec(
|
||||||
|
dereferencedOpenAPISpec
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
tools,
|
||||||
|
originAdapter: {
|
||||||
|
...originAdapter,
|
||||||
|
// Update the openapi spec with the normalized version
|
||||||
|
spec: JSON.stringify(openapiSpec),
|
||||||
|
toolToOperationMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { type Logger, parseZodSchema } from '@agentic/platform-core'
|
||||||
|
import {
|
||||||
|
type AgenticProjectConfig,
|
||||||
|
agenticProjectConfigSchema,
|
||||||
|
type ResolvedAgenticProjectConfig,
|
||||||
|
resolvedAgenticProjectConfigSchema
|
||||||
|
} from '@agentic/platform-schemas'
|
||||||
|
|
||||||
|
import { resolveMetadata } from './resolve-metadata'
|
||||||
|
import { resolveOriginAdapter } from './resolve-origin-adapter'
|
||||||
|
import { validatePricing } from './validate-pricing'
|
||||||
|
import { validateTools } from './validate-tools'
|
||||||
|
|
||||||
|
export async function resolveAgenticProjectConfig(
|
||||||
|
inputConfig: AgenticProjectConfig,
|
||||||
|
opts: { logger?: Logger; cwd?: URL; label?: string } = {}
|
||||||
|
): Promise<ResolvedAgenticProjectConfig> {
|
||||||
|
const config = parseZodSchema(agenticProjectConfigSchema.strip(), inputConfig)
|
||||||
|
|
||||||
|
const { name, version } = resolveMetadata(config)
|
||||||
|
validatePricing(config)
|
||||||
|
|
||||||
|
const { originAdapter, tools } = await resolveOriginAdapter({
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
label: `project "${name}"`,
|
||||||
|
...opts,
|
||||||
|
originUrl: config.originUrl,
|
||||||
|
originAdapter: config.originAdapter
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolvedConfig = parseZodSchema(resolvedAgenticProjectConfigSchema, {
|
||||||
|
...config,
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
originAdapter,
|
||||||
|
tools
|
||||||
|
})
|
||||||
|
|
||||||
|
validateTools({
|
||||||
|
label: `project "${name}"`,
|
||||||
|
...opts,
|
||||||
|
originAdapter: resolvedConfig.originAdapter,
|
||||||
|
tools: resolvedConfig.tools,
|
||||||
|
toolConfigs: resolvedConfig.toolConfigs || []
|
||||||
|
})
|
||||||
|
|
||||||
|
return resolvedConfig
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import type { AgenticProjectConfig } from '@agentic/platform-schemas'
|
||||||
|
import { assert } from '@agentic/platform-core'
|
||||||
|
import { validators } from '@agentic/platform-validators'
|
||||||
|
import { clean as cleanSemver, valid as isValidSemver } from 'semver'
|
||||||
|
|
||||||
|
export function resolveMetadata({
|
||||||
|
name,
|
||||||
|
version
|
||||||
|
}: Pick<AgenticProjectConfig, 'name' | 'version'>): Pick<
|
||||||
|
AgenticProjectConfig,
|
||||||
|
'name' | 'version'
|
||||||
|
> {
|
||||||
|
assert(
|
||||||
|
validators.projectName(name),
|
||||||
|
`Invalid project name "${name}". Must be lower kebab-case with no spaces between 2 and 64 characters. Example: "my-project" or "linkedin-resolver-23"`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (version) {
|
||||||
|
const normalizedVersion = cleanSemver(version)!
|
||||||
|
assert(version, `Invalid semver version "${version}" for project "${name}"`)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
isValidSemver(version),
|
||||||
|
`Invalid semver version "${version}" for project "${name}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update the config with the normalized semver version
|
||||||
|
version = normalizedVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
version
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
import type {
|
||||||
|
OriginAdapter,
|
||||||
|
OriginAdapterConfig,
|
||||||
|
Tool
|
||||||
|
} from '@agentic/platform-schemas'
|
||||||
|
import { assert, type Logger } from '@agentic/platform-core'
|
||||||
|
|
||||||
|
import { resolveMCPOriginAdapter } from './origin-adapters/mcp'
|
||||||
|
import { resolveOpenAPIOriginAdapter } from './origin-adapters/openapi'
|
||||||
|
import { validateOriginUrl } from './validate-origin-url'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates, normalizes, and resolves the origin adapter config for a project.
|
||||||
|
*/
|
||||||
|
export async function resolveOriginAdapter({
|
||||||
|
name,
|
||||||
|
version = '0.0.0',
|
||||||
|
originUrl,
|
||||||
|
originAdapter,
|
||||||
|
label,
|
||||||
|
cwd,
|
||||||
|
logger
|
||||||
|
}: {
|
||||||
|
name: string
|
||||||
|
originUrl: string
|
||||||
|
originAdapter: OriginAdapterConfig
|
||||||
|
label: string
|
||||||
|
version?: string
|
||||||
|
cwd?: URL
|
||||||
|
logger?: Logger
|
||||||
|
}): Promise<{
|
||||||
|
originAdapter: OriginAdapter
|
||||||
|
tools?: Tool[]
|
||||||
|
}> {
|
||||||
|
validateOriginUrl({ originUrl, label })
|
||||||
|
|
||||||
|
if (originAdapter.type === 'openapi') {
|
||||||
|
return resolveOpenAPIOriginAdapter({
|
||||||
|
originAdapter,
|
||||||
|
label,
|
||||||
|
cwd,
|
||||||
|
logger
|
||||||
|
})
|
||||||
|
} else if (originAdapter.type === 'mcp') {
|
||||||
|
return resolveMCPOriginAdapter({
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
originUrl,
|
||||||
|
originAdapter,
|
||||||
|
label
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
assert(
|
||||||
|
originAdapter.type === 'raw',
|
||||||
|
400,
|
||||||
|
`Invalid origin adapter type "${originAdapter.type}" for ${label}`
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
originAdapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,14 @@
|
||||||
import type { ZodTypeDef } from 'zod'
|
import type { ZodTypeDef } from 'zod'
|
||||||
import { assert, type Logger, parseZodSchema } from '@agentic/platform-core'
|
import { type Logger, parseZodSchema } from '@agentic/platform-core'
|
||||||
import {
|
import {
|
||||||
type AgenticProjectConfig,
|
type AgenticProjectConfig,
|
||||||
type AgenticProjectConfigInput,
|
type AgenticProjectConfigInput,
|
||||||
agenticProjectConfigSchema,
|
agenticProjectConfigSchema
|
||||||
getPricingPlansByInterval,
|
|
||||||
type PricingPlanLineItem,
|
|
||||||
type ResolvedAgenticProjectConfig,
|
|
||||||
resolvedAgenticProjectConfigSchema
|
|
||||||
} from '@agentic/platform-schemas'
|
} from '@agentic/platform-schemas'
|
||||||
import { validators } from '@agentic/platform-validators'
|
|
||||||
import { clean as cleanSemver, valid as isValidSemver } from 'semver'
|
|
||||||
|
|
||||||
|
import { resolveMetadata } from './resolve-metadata'
|
||||||
import { validateOriginAdapter } from './validate-origin-adapter'
|
import { validateOriginAdapter } from './validate-origin-adapter'
|
||||||
|
import { validatePricing } from './validate-pricing'
|
||||||
|
|
||||||
export async function validateAgenticProjectConfig(
|
export async function validateAgenticProjectConfig(
|
||||||
inputConfig: unknown,
|
inputConfig: unknown,
|
||||||
|
@ -20,7 +16,7 @@ export async function validateAgenticProjectConfig(
|
||||||
strip = false,
|
strip = false,
|
||||||
...opts
|
...opts
|
||||||
}: { logger?: Logger; cwd?: URL; strip?: boolean; label?: string } = {}
|
}: { logger?: Logger; cwd?: URL; strip?: boolean; label?: string } = {}
|
||||||
): Promise<ResolvedAgenticProjectConfig> {
|
): Promise<AgenticProjectConfig> {
|
||||||
const config = parseZodSchema<
|
const config = parseZodSchema<
|
||||||
AgenticProjectConfig,
|
AgenticProjectConfig,
|
||||||
ZodTypeDef,
|
ZodTypeDef,
|
||||||
|
@ -32,228 +28,31 @@ export async function validateAgenticProjectConfig(
|
||||||
inputConfig
|
inputConfig
|
||||||
)
|
)
|
||||||
|
|
||||||
const { name, pricingIntervals, pricingPlans, originUrl } = config
|
const { name, version } = resolveMetadata(config)
|
||||||
assert(
|
validatePricing(config)
|
||||||
validators.projectName(name),
|
|
||||||
`Invalid project name "${name}". Must be lower kebab-case with no spaces between 2 and 64 characters. Example: "my-project" or "linkedin-resolver-23"`
|
|
||||||
)
|
|
||||||
assert(
|
|
||||||
pricingPlans?.length,
|
|
||||||
'Invalid pricingPlans: must be a non-empty array'
|
|
||||||
)
|
|
||||||
assert(
|
|
||||||
pricingIntervals?.length,
|
|
||||||
'Invalid pricingIntervals: must be a non-empty array'
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
const originAdapter = await validateOriginAdapter({
|
||||||
const parsedOriginUrl = new URL(originUrl)
|
|
||||||
assert(
|
|
||||||
parsedOriginUrl.protocol === 'https:',
|
|
||||||
'Invalid originUrl: must be a valid https URL'
|
|
||||||
)
|
|
||||||
|
|
||||||
assert(parsedOriginUrl.hostname, 'Invalid originUrl: must be a valid URL')
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error('Invalid originUrl: must be a valid https URL', {
|
|
||||||
cause: err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.version) {
|
|
||||||
const version = cleanSemver(config.version)
|
|
||||||
assert(
|
|
||||||
version,
|
|
||||||
`Invalid semver version "${config.version}" for project "${name}"`
|
|
||||||
)
|
|
||||||
|
|
||||||
assert(
|
|
||||||
isValidSemver(version),
|
|
||||||
`Invalid semver version "${version}" for project "${name}"`
|
|
||||||
)
|
|
||||||
|
|
||||||
// Update the config with the normalized semver version
|
|
||||||
config.version = version
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// Validate pricing interval
|
|
||||||
const pricingIntervalsSet = new Set(pricingIntervals)
|
|
||||||
assert(
|
|
||||||
pricingIntervalsSet.size === pricingIntervals.length,
|
|
||||||
'Invalid pricingIntervals: duplicate pricing intervals'
|
|
||||||
)
|
|
||||||
assert(
|
|
||||||
pricingIntervals.length >= 1,
|
|
||||||
'Invalid pricingIntervals: must contain at least one pricing interval'
|
|
||||||
)
|
|
||||||
|
|
||||||
if (pricingIntervals.length > 1) {
|
|
||||||
for (const pricingPlan of pricingPlans) {
|
|
||||||
if (pricingPlan.interval) {
|
|
||||||
assert(
|
|
||||||
pricingIntervalsSet.has(pricingPlan.interval),
|
|
||||||
`Invalid pricingPlan "${pricingPlan.slug}": PricingPlan "${pricingPlan.slug}" has invalid interval "${pricingPlan.interval}" which is not included in the "pricingIntervals" array.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pricingPlan.slug === 'free') continue
|
|
||||||
|
|
||||||
assert(
|
|
||||||
pricingPlan.interval !== undefined,
|
|
||||||
`Invalid pricingPlan "${pricingPlan.slug}": non-free PricingPlan "${pricingPlan.slug}" must specify an "interval" because the project supports multiple pricing intervals.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Only a single pricing interval is supported, so default all pricing
|
|
||||||
// plans to use the default pricing interval.
|
|
||||||
const defaultPricingInterval = pricingIntervals[0]!
|
|
||||||
assert(
|
|
||||||
defaultPricingInterval,
|
|
||||||
'Invalid pricingIntervals: must contain at least one valid pricing interval'
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const pricingPlan of pricingPlans) {
|
|
||||||
if (pricingPlan.interval) {
|
|
||||||
assert(
|
|
||||||
pricingIntervalsSet.has(pricingPlan.interval),
|
|
||||||
`Invalid pricingPlan "${pricingPlan.slug}": PricingPlan "${pricingPlan.slug}" has invalid interval "${pricingPlan.interval}" which is not included in the "pricingIntervals" array.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pricingPlan.slug === 'free') continue
|
|
||||||
|
|
||||||
pricingPlan.interval ??= defaultPricingInterval
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// Validate pricingPlans
|
|
||||||
const pricingPlanSlugsSet = new Set(pricingPlans.map((p) => p.slug))
|
|
||||||
assert(
|
|
||||||
pricingPlanSlugsSet.size === pricingPlans.length,
|
|
||||||
'Invalid pricingPlans: duplicate PricingPlan slugs. All PricingPlan slugs must be unique (e.g. "free", "starter-monthly", "pro-annual", etc).'
|
|
||||||
)
|
|
||||||
|
|
||||||
const pricingPlanLineItemSlugMap: Record<string, PricingPlanLineItem[]> = {}
|
|
||||||
|
|
||||||
for (const pricingPlan of pricingPlans) {
|
|
||||||
const lineItemSlugsSet = new Set(
|
|
||||||
pricingPlan.lineItems.map((lineItem) => lineItem.slug)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert(
|
|
||||||
lineItemSlugsSet.size === pricingPlan.lineItems.length,
|
|
||||||
`Invalid pricingPlan "${pricingPlan.slug}": duplicate line-item slugs`
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const lineItem of pricingPlan.lineItems) {
|
|
||||||
if (!pricingPlanLineItemSlugMap[lineItem.slug]) {
|
|
||||||
pricingPlanLineItemSlugMap[lineItem.slug] = []
|
|
||||||
}
|
|
||||||
|
|
||||||
pricingPlanLineItemSlugMap[lineItem.slug]!.push(lineItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const lineItems of Object.values(pricingPlanLineItemSlugMap)) {
|
|
||||||
if (lineItems.length <= 1) continue
|
|
||||||
|
|
||||||
const lineItem0 = lineItems[0]!
|
|
||||||
|
|
||||||
for (let i = 1; i < lineItems.length; ++i) {
|
|
||||||
const lineItem = lineItems[i]!
|
|
||||||
|
|
||||||
assert(
|
|
||||||
lineItem.usageType === lineItem0.usageType,
|
|
||||||
`Invalid pricingPlans: all PricingPlans which contain the same LineItems (by slug "${lineItem.slug}") must have the same usage type ("licensed" or "metered").`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate PricingPlanLineItems
|
|
||||||
for (const pricingPlan of pricingPlans) {
|
|
||||||
for (const lineItem of pricingPlan.lineItems) {
|
|
||||||
if (lineItem.usageType === 'metered') {
|
|
||||||
switch (lineItem.billingScheme) {
|
|
||||||
case 'per_unit':
|
|
||||||
assert(
|
|
||||||
lineItem.unitAmount !== undefined,
|
|
||||||
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must specify a non-negative "unitAmount" when using "per_unit" billing scheme.`
|
|
||||||
)
|
|
||||||
|
|
||||||
assert(
|
|
||||||
lineItem.tiersMode === undefined,
|
|
||||||
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must not specify "tiersMode" when using "per_unit" billing scheme.`
|
|
||||||
)
|
|
||||||
|
|
||||||
assert(
|
|
||||||
lineItem.tiers === undefined,
|
|
||||||
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must not specify "tiers" when using "per_unit" billing scheme.`
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'tiered':
|
|
||||||
assert(
|
|
||||||
lineItem.unitAmount === undefined,
|
|
||||||
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must not specify "unitAmount" when using "tiered" billing scheme.`
|
|
||||||
)
|
|
||||||
|
|
||||||
assert(
|
|
||||||
lineItem.tiers?.length,
|
|
||||||
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must specify a non-empty "tiers" array when using "tiered" billing scheme.`
|
|
||||||
)
|
|
||||||
|
|
||||||
assert(
|
|
||||||
lineItem.tiersMode !== undefined,
|
|
||||||
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must specify a valid "tiersMode" when using "tiered" billing scheme.`
|
|
||||||
)
|
|
||||||
|
|
||||||
assert(
|
|
||||||
lineItem.transformQuantity === undefined,
|
|
||||||
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must not specify "transformQuantity" when using "tiered" billing scheme.`
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
assert(
|
|
||||||
false,
|
|
||||||
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must specify a valid "billingScheme".`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate deployment pricing plans to ensure they contain at least one valid
|
|
||||||
// plan per pricing interval configured on the project.
|
|
||||||
for (const pricingInterval of config.pricingIntervals) {
|
|
||||||
const pricingPlansForInterval = getPricingPlansByInterval({
|
|
||||||
pricingInterval,
|
|
||||||
pricingPlans
|
|
||||||
})
|
|
||||||
|
|
||||||
assert(
|
|
||||||
pricingPlansForInterval.length > 0,
|
|
||||||
400,
|
|
||||||
`Invalid pricing config: no pricing plans for pricing interval "${pricingInterval}"`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { originAdapter, tools } = await validateOriginAdapter({
|
|
||||||
name,
|
name,
|
||||||
version: config.version,
|
version,
|
||||||
label: `project "${name}"`,
|
label: `project "${name}"`,
|
||||||
...opts,
|
...opts,
|
||||||
originUrl,
|
originUrl: config.originUrl,
|
||||||
originAdapter: config.originAdapter
|
originAdapter: config.originAdapter
|
||||||
})
|
})
|
||||||
|
|
||||||
return parseZodSchema(resolvedAgenticProjectConfigSchema, {
|
return parseZodSchema<
|
||||||
|
AgenticProjectConfig,
|
||||||
|
ZodTypeDef,
|
||||||
|
AgenticProjectConfigInput
|
||||||
|
>(
|
||||||
|
strip
|
||||||
|
? agenticProjectConfigSchema.strip()
|
||||||
|
: agenticProjectConfigSchema.strict(),
|
||||||
|
{
|
||||||
...config,
|
...config,
|
||||||
originAdapter,
|
name,
|
||||||
tools
|
version,
|
||||||
})
|
originAdapter
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,12 @@
|
||||||
import type {
|
import type { OriginAdapterConfig } from '@agentic/platform-schemas'
|
||||||
OriginAdapter,
|
|
||||||
OriginAdapterConfig,
|
|
||||||
Tool
|
|
||||||
} from '@agentic/platform-schemas'
|
|
||||||
import { assert, type Logger } from '@agentic/platform-core'
|
import { assert, type Logger } from '@agentic/platform-core'
|
||||||
import {
|
|
||||||
getToolsFromOpenAPISpec,
|
import { resolveMCPOriginAdapter } from './origin-adapters/mcp'
|
||||||
validateOpenAPISpec
|
import { resolveOpenAPIOriginAdapter } from './origin-adapters/openapi'
|
||||||
} from '@agentic/platform-openapi'
|
import { validateOriginUrl } from './validate-origin-url'
|
||||||
import { Client as McpClient } from '@modelcontextprotocol/sdk/client/index.js'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates and normalizes the origin adapter config for a project.
|
* Validates and normalizes the origin adapter for a project.
|
||||||
*/
|
*/
|
||||||
export async function validateOriginAdapter({
|
export async function validateOriginAdapter({
|
||||||
name,
|
name,
|
||||||
|
@ -24,93 +19,43 @@ export async function validateOriginAdapter({
|
||||||
}: {
|
}: {
|
||||||
name: string
|
name: string
|
||||||
originUrl: string
|
originUrl: string
|
||||||
originAdapter: OriginAdapterConfig
|
originAdapter: Readonly<OriginAdapterConfig>
|
||||||
label: string
|
label: string
|
||||||
version?: string
|
version?: string
|
||||||
cwd?: URL
|
cwd?: URL
|
||||||
logger?: Logger
|
logger?: Logger
|
||||||
}): Promise<{
|
}): Promise<OriginAdapterConfig> {
|
||||||
originAdapter: OriginAdapter
|
validateOriginUrl({ originUrl, label })
|
||||||
tools?: Tool[]
|
|
||||||
}> {
|
|
||||||
assert(originUrl, 400, `Origin URL is required for ${label}`)
|
|
||||||
|
|
||||||
if (originAdapter.type === 'openapi') {
|
if (originAdapter.type === 'openapi') {
|
||||||
assert(
|
// We intentionally ignore the resolved tools here because the server will
|
||||||
originAdapter.spec,
|
// need to re-validate the OpenAPI spec and tools anyway. We do, however,
|
||||||
400,
|
// override the `spec` field with the parsed, normalized version because
|
||||||
`OpenAPI spec is required for ${label} with origin adapter type set to "openapi"`
|
// that may have been pointing to a local file or remote URL.
|
||||||
)
|
const { originAdapter: resolvedOriginAdapter } =
|
||||||
|
await resolveOpenAPIOriginAdapter({
|
||||||
// Validate and normalize the OpenAPI spec
|
originAdapter,
|
||||||
const openapiSpec = await validateOpenAPISpec(originAdapter.spec, {
|
label,
|
||||||
cwd,
|
cwd,
|
||||||
logger
|
logger
|
||||||
})
|
})
|
||||||
|
|
||||||
// Remove origin servers from the OpenAPI spec.
|
|
||||||
// TODO: Ensure that `originUrl` matches any origin servers in the openapi spec?
|
|
||||||
delete openapiSpec.servers
|
|
||||||
|
|
||||||
// TODO: Additional, agentic-specific validation of the OpenAPI spec's
|
|
||||||
// operations to ensure they are valid tools.
|
|
||||||
|
|
||||||
// TODO: Simplify OpenAPI spec by removing any query params and headers
|
|
||||||
// specific to the Agentic API gateway.
|
|
||||||
|
|
||||||
// TODO: Extract tool definitions from OpenAPI operationIds
|
|
||||||
|
|
||||||
const dereferencedOpenAPISpec = await validateOpenAPISpec(
|
|
||||||
originAdapter.spec,
|
|
||||||
{
|
|
||||||
cwd,
|
|
||||||
dereference: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const { tools, toolToOperationMap } = await getToolsFromOpenAPISpec(
|
|
||||||
dereferencedOpenAPISpec
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tools,
|
|
||||||
originAdapter: {
|
|
||||||
...originAdapter,
|
...originAdapter,
|
||||||
// Update the openapi spec with the normalized version
|
spec: resolvedOriginAdapter.spec
|
||||||
spec: JSON.stringify(openapiSpec),
|
|
||||||
toolToOperationMap
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (originAdapter.type === 'mcp') {
|
} else if (originAdapter.type === 'mcp') {
|
||||||
// TODO: Validate MCP server info and tools
|
// We intentionally ignore the resolved version and tools here because the
|
||||||
|
// server will need to re-validate the MCP server info and tools anyway.
|
||||||
const { SSEClientTransport } = await import(
|
await resolveMCPOriginAdapter({
|
||||||
'@modelcontextprotocol/sdk/client/sse.js'
|
|
||||||
)
|
|
||||||
const transport = new SSEClientTransport(new URL(originUrl))
|
|
||||||
const client = new McpClient({ name, version })
|
|
||||||
await client.connect(transport)
|
|
||||||
|
|
||||||
const serverInfo = {
|
|
||||||
name,
|
name,
|
||||||
version,
|
version,
|
||||||
...client.getServerVersion(),
|
originUrl,
|
||||||
capabilities: client.getServerCapabilities(),
|
originAdapter,
|
||||||
instructions: client.getInstructions()
|
label
|
||||||
}
|
})
|
||||||
|
|
||||||
const listToolsResponse = await client.listTools()
|
return originAdapter
|
||||||
|
|
||||||
// TODO: Validate MCP tools
|
|
||||||
const tools = listToolsResponse.tools
|
|
||||||
|
|
||||||
return {
|
|
||||||
originAdapter: {
|
|
||||||
...originAdapter,
|
|
||||||
serverInfo
|
|
||||||
},
|
|
||||||
tools
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
assert(
|
assert(
|
||||||
originAdapter.type === 'raw',
|
originAdapter.type === 'raw',
|
||||||
|
@ -118,8 +63,6 @@ export async function validateOriginAdapter({
|
||||||
`Invalid origin adapter type "${originAdapter.type}" for ${label}`
|
`Invalid origin adapter type "${originAdapter.type}" for ${label}`
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return originAdapter
|
||||||
originAdapter
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { assert } from '@agentic/platform-core'
|
||||||
|
|
||||||
|
export function validateOriginUrl({
|
||||||
|
originUrl,
|
||||||
|
label
|
||||||
|
}: {
|
||||||
|
originUrl: string
|
||||||
|
label: string
|
||||||
|
}) {
|
||||||
|
assert(originUrl, 400, `Origin URL is required for ${label}`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedOriginUrl = new URL(originUrl)
|
||||||
|
assert(
|
||||||
|
parsedOriginUrl.protocol === 'https:',
|
||||||
|
'Invalid originUrl: must be a valid https URL'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(parsedOriginUrl.hostname, 'Invalid originUrl: must be a valid URL')
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Invalid originUrl: must be a valid https URL', {
|
||||||
|
cause: err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,186 @@
|
||||||
|
import { assert } from '@agentic/platform-core'
|
||||||
|
import {
|
||||||
|
type AgenticProjectConfig,
|
||||||
|
getPricingPlansByInterval,
|
||||||
|
type PricingPlanLineItem
|
||||||
|
} from '@agentic/platform-schemas'
|
||||||
|
|
||||||
|
export function validatePricing({
|
||||||
|
pricingIntervals,
|
||||||
|
pricingPlans
|
||||||
|
}: Pick<AgenticProjectConfig, 'pricingIntervals' | 'pricingPlans'>) {
|
||||||
|
assert(
|
||||||
|
pricingPlans?.length,
|
||||||
|
'Invalid pricingPlans: must be a non-empty array'
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
pricingIntervals?.length,
|
||||||
|
'Invalid pricingIntervals: must be a non-empty array'
|
||||||
|
)
|
||||||
|
|
||||||
|
{
|
||||||
|
// Validate pricing interval
|
||||||
|
const pricingIntervalsSet = new Set(pricingIntervals)
|
||||||
|
assert(
|
||||||
|
pricingIntervalsSet.size === pricingIntervals.length,
|
||||||
|
'Invalid pricingIntervals: duplicate pricing intervals'
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
pricingIntervals.length >= 1,
|
||||||
|
'Invalid pricingIntervals: must contain at least one pricing interval'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (pricingIntervals.length > 1) {
|
||||||
|
for (const pricingPlan of pricingPlans) {
|
||||||
|
if (pricingPlan.interval) {
|
||||||
|
assert(
|
||||||
|
pricingIntervalsSet.has(pricingPlan.interval),
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": PricingPlan "${pricingPlan.slug}" has invalid interval "${pricingPlan.interval}" which is not included in the "pricingIntervals" array.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pricingPlan.slug === 'free') continue
|
||||||
|
|
||||||
|
assert(
|
||||||
|
pricingPlan.interval !== undefined,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": non-free PricingPlan "${pricingPlan.slug}" must specify an "interval" because the project supports multiple pricing intervals.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Only a single pricing interval is supported, so default all pricing
|
||||||
|
// plans to use the default pricing interval.
|
||||||
|
const defaultPricingInterval = pricingIntervals[0]!
|
||||||
|
assert(
|
||||||
|
defaultPricingInterval,
|
||||||
|
'Invalid pricingIntervals: must contain at least one valid pricing interval'
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const pricingPlan of pricingPlans) {
|
||||||
|
if (pricingPlan.interval) {
|
||||||
|
assert(
|
||||||
|
pricingIntervalsSet.has(pricingPlan.interval),
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": PricingPlan "${pricingPlan.slug}" has invalid interval "${pricingPlan.interval}" which is not included in the "pricingIntervals" array.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pricingPlan.slug === 'free') continue
|
||||||
|
|
||||||
|
pricingPlan.interval ??= defaultPricingInterval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Validate pricingPlans
|
||||||
|
const pricingPlanSlugsSet = new Set(pricingPlans.map((p) => p.slug))
|
||||||
|
assert(
|
||||||
|
pricingPlanSlugsSet.size === pricingPlans.length,
|
||||||
|
'Invalid pricingPlans: duplicate PricingPlan slugs. All PricingPlan slugs must be unique (e.g. "free", "starter-monthly", "pro-annual", etc).'
|
||||||
|
)
|
||||||
|
|
||||||
|
const pricingPlanLineItemSlugMap: Record<string, PricingPlanLineItem[]> = {}
|
||||||
|
|
||||||
|
for (const pricingPlan of pricingPlans) {
|
||||||
|
const lineItemSlugsSet = new Set(
|
||||||
|
pricingPlan.lineItems.map((lineItem) => lineItem.slug)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
lineItemSlugsSet.size === pricingPlan.lineItems.length,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": duplicate line-item slugs`
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const lineItem of pricingPlan.lineItems) {
|
||||||
|
if (!pricingPlanLineItemSlugMap[lineItem.slug]) {
|
||||||
|
pricingPlanLineItemSlugMap[lineItem.slug] = []
|
||||||
|
}
|
||||||
|
|
||||||
|
pricingPlanLineItemSlugMap[lineItem.slug]!.push(lineItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const lineItems of Object.values(pricingPlanLineItemSlugMap)) {
|
||||||
|
if (lineItems.length <= 1) continue
|
||||||
|
|
||||||
|
const lineItem0 = lineItems[0]!
|
||||||
|
|
||||||
|
for (let i = 1; i < lineItems.length; ++i) {
|
||||||
|
const lineItem = lineItems[i]!
|
||||||
|
|
||||||
|
assert(
|
||||||
|
lineItem.usageType === lineItem0.usageType,
|
||||||
|
`Invalid pricingPlans: all PricingPlans which contain the same LineItems (by slug "${lineItem.slug}") must have the same usage type ("licensed" or "metered").`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate PricingPlanLineItems
|
||||||
|
for (const pricingPlan of pricingPlans) {
|
||||||
|
for (const lineItem of pricingPlan.lineItems) {
|
||||||
|
if (lineItem.usageType === 'metered') {
|
||||||
|
switch (lineItem.billingScheme) {
|
||||||
|
case 'per_unit':
|
||||||
|
assert(
|
||||||
|
lineItem.unitAmount !== undefined,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must specify a non-negative "unitAmount" when using "per_unit" billing scheme.`
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
lineItem.tiersMode === undefined,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must not specify "tiersMode" when using "per_unit" billing scheme.`
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
lineItem.tiers === undefined,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must not specify "tiers" when using "per_unit" billing scheme.`
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'tiered':
|
||||||
|
assert(
|
||||||
|
lineItem.unitAmount === undefined,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must not specify "unitAmount" when using "tiered" billing scheme.`
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
lineItem.tiers?.length,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must specify a non-empty "tiers" array when using "tiered" billing scheme.`
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
lineItem.tiersMode !== undefined,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must specify a valid "tiersMode" when using "tiered" billing scheme.`
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
lineItem.transformQuantity === undefined,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must not specify "transformQuantity" when using "tiered" billing scheme.`
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
assert(
|
||||||
|
false,
|
||||||
|
`Invalid pricingPlan "${pricingPlan.slug}": metered LineItem "${lineItem.slug}" must specify a valid "billingScheme".`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate deployment pricing plans to ensure they contain at least one valid
|
||||||
|
// plan per pricing interval configured on the project.
|
||||||
|
for (const pricingInterval of pricingIntervals) {
|
||||||
|
const pricingPlansForInterval = getPricingPlansByInterval({
|
||||||
|
pricingInterval,
|
||||||
|
pricingPlans
|
||||||
|
})
|
||||||
|
|
||||||
|
assert(
|
||||||
|
pricingPlansForInterval.length > 0,
|
||||||
|
400,
|
||||||
|
`Invalid pricing config: no pricing plans for pricing interval "${pricingInterval}"`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,9 +2,9 @@ import type { OriginAdapter, Tool, ToolConfig } from '@agentic/platform-schemas'
|
||||||
import { assert } from '@agentic/platform-core'
|
import { assert } from '@agentic/platform-core'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates and normalizes the origin adapter config for a project.
|
* Validates the origin server's tools for a project.
|
||||||
*/
|
*/
|
||||||
export async function validateTools({
|
export function validateTools({
|
||||||
originAdapter,
|
originAdapter,
|
||||||
tools,
|
tools,
|
||||||
toolConfigs,
|
toolConfigs,
|
||||||
|
@ -14,8 +14,13 @@ export async function validateTools({
|
||||||
tools: Tool[]
|
tools: Tool[]
|
||||||
toolConfigs: ToolConfig[]
|
toolConfigs: ToolConfig[]
|
||||||
label: string
|
label: string
|
||||||
}): Promise<void> {
|
}) {
|
||||||
assert(tools.length > 0, 400, `No tools defined for ${label}`)
|
if (!tools.length) {
|
||||||
|
assert(
|
||||||
|
originAdapter.type === 'raw',
|
||||||
|
`No tools defined for ${label} with origin adapter type "${originAdapter.type}"`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const toolsMap: Record<string, Tool> = {}
|
const toolsMap: Record<string, Tool> = {}
|
||||||
for (const tool of tools) {
|
for (const tool of tools) {
|
||||||
|
@ -35,8 +40,4 @@ export async function validateTools({
|
||||||
`Tool "${toolConfig.name}" from \`toolConfigs\` not found in \`tools\` for ${label}`
|
`Tool "${toolConfig.name}" from \`toolConfigs\` not found in \`tools\` for ${label}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (originAdapter.type === 'openapi') {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,9 @@ export const openapiOriginAdapterConfigSchema = commonOriginAdapterSchema.merge(
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
export type OpenAPIOriginAdapterConfig = z.infer<
|
||||||
|
typeof openapiOriginAdapterConfigSchema
|
||||||
|
>
|
||||||
|
|
||||||
export const mcpOriginAdapterConfigSchema = commonOriginAdapterSchema.merge(
|
export const mcpOriginAdapterConfigSchema = commonOriginAdapterSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
|
@ -63,6 +66,9 @@ export const mcpOriginAdapterConfigSchema = commonOriginAdapterSchema.merge(
|
||||||
type: z.literal('mcp')
|
type: z.literal('mcp')
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
export type MCPOriginAdapterConfig = z.infer<
|
||||||
|
typeof mcpOriginAdapterConfigSchema
|
||||||
|
>
|
||||||
|
|
||||||
export const rawOriginAdapterConfigSchema = commonOriginAdapterSchema.merge(
|
export const rawOriginAdapterConfigSchema = commonOriginAdapterSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
|
@ -76,6 +82,9 @@ export const rawOriginAdapterConfigSchema = commonOriginAdapterSchema.merge(
|
||||||
type: z.literal('raw')
|
type: z.literal('raw')
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
export type RawOriginAdapterConfig = z.infer<
|
||||||
|
typeof rawOriginAdapterConfigSchema
|
||||||
|
>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Origin adapter is used to configure the origin API server downstream from
|
* Origin adapter is used to configure the origin API server downstream from
|
||||||
|
@ -176,6 +185,7 @@ export const openapiOriginAdapterSchema = commonOriginAdapterSchema.merge(
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
export type OpenAPIOriginAdapter = z.infer<typeof openapiOriginAdapterSchema>
|
||||||
|
|
||||||
export const mcpOriginAdapterSchema = commonOriginAdapterSchema.merge(
|
export const mcpOriginAdapterSchema = commonOriginAdapterSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
|
@ -190,6 +200,7 @@ export const mcpOriginAdapterSchema = commonOriginAdapterSchema.merge(
|
||||||
serverInfo: mcpServerInfoSchema
|
serverInfo: mcpServerInfoSchema
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
export type MCPOriginAdapter = z.infer<typeof mcpOriginAdapterSchema>
|
||||||
|
|
||||||
export const rawOriginAdapterSchema = commonOriginAdapterSchema.merge(
|
export const rawOriginAdapterSchema = commonOriginAdapterSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
|
@ -203,6 +214,7 @@ export const rawOriginAdapterSchema = commonOriginAdapterSchema.merge(
|
||||||
type: z.literal('raw')
|
type: z.literal('raw')
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
export type RawOriginAdapter = z.infer<typeof rawOriginAdapterSchema>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Origin adapter is used to configure the origin API server downstream from
|
* Origin adapter is used to configure the origin API server downstream from
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
- consider switching to [consola](https://github.com/unjs/consola) for logging?
|
- consider switching to [consola](https://github.com/unjs/consola) for logging?
|
||||||
- consider switching to `bun` (for `--hot` reloading!!)
|
- consider switching to `bun` (for `--hot` reloading!!)
|
||||||
- consider `projectName` and `projectSlug` or `projectIdentifier`?
|
- consider `projectName` and `projectSlug` or `projectIdentifier`?
|
||||||
|
- not sure I like the duplication between client and server AgenticProjectConfig and ResolvedAgenticProjectConfig, especially for openapi and mcp processing
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue