From c685b749e489f78b8856914af315cd89eb9acf42 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 9 Jun 2026 00:28:32 +0800 Subject: [PATCH] fix: expose dynamic provider profiles in backends list --- docs/reference/spec-v01-cli.md | 2 +- scripts/src/cli.ts | 10 ++--- src/common/backend-profiles.ts | 21 +++++++++- src/mgr/provider-profiles.ts | 38 ++++++++++++++----- src/mgr/server.ts | 4 +- .../cases/45-provider-profile-management.ts | 10 ++++- 6 files changed, 65 insertions(+), 20 deletions(-) diff --git a/docs/reference/spec-v01-cli.md b/docs/reference/spec-v01-cli.md index 05f50d7..f1092e5 100644 --- a/docs/reference/spec-v01-cli.md +++ b/docs/reference/spec-v01-cli.md @@ -91,7 +91,7 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交 - `server stop` 必须按 pidFile 与端口进程清理本地 manager,并返回 before/after 状态;不得要求人工用 `ps/kill/ss` 组合命令清理常见临时服务。 - `secrets codex render --dry-run` 返回 Codex stdio profile Secret 创建计划、输入文件 bytes/hash、SecretRef、manifest 摘要和 apply 命令形状;`--profile codex` 默认 Secret name 为 `agentrun-v01-provider-codex`,`--profile deepseek` 默认 Secret name 为 `agentrun-v01-provider-deepseek`,`--profile minimax-m3` 默认 Secret name 为 `agentrun-v01-provider-minimax-m3`,`--profile dsflash-go` 默认 Secret name 为 `agentrun-v01-provider-dsflash-go` 并包含 `model-catalog.json`;它不得输出 Secret value 或执行 Kubernetes 写操作。 - `provider-profiles` 命令族调用 manager REST 管理 API,覆盖 profile status、删除、API Key 写入和 canary 验证。`set-key --key-stdin` 从 stdin 读取 API Key,响应只显示 SecretRef、resourceVersion、hash 后缀和 failureKind;不得输出 key、Codex auth/config 或 Secret data。 -- `backends list` 必须显示 `codex`、`deepseek`、`minimax-m3` 与 `dsflash-go` profile 的 backendKind、protocol、transport、command、requiredSecretKeys 和状态;`dsflash-go` 的 `requiredSecretKeys` 必须包含 `model-catalog.json`;不得因为某个 provider Secret 尚未配置就隐藏 capability。 +- `backends list` 必须显示 `codex`、`deepseek`、`minimax-m3` 与 `dsflash-go` profile 的 backendKind、protocol、transport、command、requiredSecretKeys 和状态;`dsflash-go` 的 `requiredSecretKeys` 必须包含 `model-catalog.json`;已配置的动态 provider profile(例如 `hy`)必须同样可见,并带动态 discovery 状态;不得因为某个 provider Secret 尚未配置就隐藏 capability。 - `queue dispatch` 是 Q2 的受控手动调度入口,只对单个 task 显式创建 attempt 和 Core run/command/runner job;不得伪装成自动 scheduler。 - `queue refresh` 只根据 Queue task 中保存的 Core run/command 引用回写 Queue attempt 状态,不读取 Core trace 反推 commander 或统计。 - `queue show` 必须返回 task/attempt summary、state、read cursor、stats 相关字段和 `sessionPath`;不得返回或代理完整 output/trace。 diff --git a/scripts/src/cli.ts b/scripts/src/cli.ts index 570fd20..8f47e1d 100644 --- a/scripts/src/cli.ts +++ b/scripts/src/cli.ts @@ -801,12 +801,12 @@ function help(args: ParsedArgs, group?: string): JsonRecord { "runs events --after-seq --limit ", "runs result [--command-id ]", "runs cancel [--reason ]", - "sessions ps [--state default|running|unread|terminal|idle|all] [--profile codex|deepseek|minimax-m3|dsflash-go|M3] [--reader-id ]", - "sessions create [sessionId] [--profile codex|deepseek|minimax-m3|dsflash-go|M3] [--expires-in-days ]", + "sessions ps [--state default|running|unread|terminal|idle|all] [--profile codex|deepseek|minimax-m3|dsflash-go||M3] [--reader-id ]", + "sessions create [sessionId] [--profile codex|deepseek|minimax-m3|dsflash-go||M3] [--expires-in-days ]", "sessions storage ", "sessions storage --delete", "sessions show [--reader-id ]", - "sessions turn [sessionId] --json-file --prompt-file [--profile minimax-m3|dsflash-go|M3] [--runner-json-file ]", + "sessions turn [sessionId] --json-file --prompt-file [--profile codex|deepseek|minimax-m3|dsflash-go||M3] [--runner-json-file ]", "sessions steer --prompt-file ", "sessions cancel [--reason ]", "sessions trace [--after-seq ] [--limit ] [--run-id ]", @@ -816,7 +816,7 @@ function help(args: ParsedArgs, group?: string): JsonRecord { "commands show --run-id ", "commands result --run-id ", "commands cancel [--reason ]", - "runner start --run-id [--backend codex|deepseek|minimax-m3|dsflash-go]", + "runner start --run-id [--backend codex|deepseek|minimax-m3|dsflash-go|]", "runner job --run-id --command-id [--image ] [--runner-manager-url ] [--idempotency-key ]", "runner job --dry-run --run-id --command-id --image ", "runner jobs --run-id [--command-id ]", @@ -830,7 +830,7 @@ function help(args: ParsedArgs, group?: string): JsonRecord { "queue cancel [--reason ]", "queue dispatch [--json-file ] [--idempotency-key ] [--image ] [--namespace ]", "queue refresh ", - "secrets codex render --dry-run [--profile codex|deepseek|minimax-m3|dsflash-go] [--codex-home ] [--model-catalog-file ] [--namespace agentrun-v01] [--secret-name ]", + "secrets codex render --dry-run [--profile codex|deepseek|minimax-m3|dsflash-go|] [--codex-home ] [--model-catalog-file ] [--namespace agentrun-v01] [--secret-name ]", "provider-profiles list", "provider-profiles show ", "provider-profiles config ", diff --git a/src/common/backend-profiles.ts b/src/common/backend-profiles.ts index e8d9874..3614348 100644 --- a/src/common/backend-profiles.ts +++ b/src/common/backend-profiles.ts @@ -70,6 +70,17 @@ export const backendProfileSpecs = builtinBackendProfileSpecs; export const backendProfiles = builtinBackendProfileSpecs.map((item) => item.profile) as readonly BackendProfile[]; +export function compareBackendProfiles(left: string, right: string): number { + const leftIndex = builtinBackendProfileSpecs.findIndex((item) => item.profile === left); + const rightIndex = builtinBackendProfileSpecs.findIndex((item) => item.profile === right); + if (leftIndex >= 0 || rightIndex >= 0) { + if (leftIndex < 0) return 1; + if (rightIndex < 0) return -1; + if (leftIndex !== rightIndex) return leftIndex - rightIndex; + } + return left.localeCompare(right); +} + export function defaultSecretNameForProfile(profile: string): string { return `agentrun-v01-provider-${profile}`; } @@ -138,8 +149,14 @@ export function mergeBackendCapability(profile: string, storedCapabilities: Json }; } -export function backendCapabilities(): JsonRecord[] { - return builtinBackendProfileSpecs.map(backendCapability); +export function backendCapabilities(profiles: readonly string[] = backendProfiles): JsonRecord[] { + const profileIds = new Set(backendProfiles); + for (const profile of profiles) { + if (isBackendProfileSlug(profile)) profileIds.add(profile as BackendProfile); + } + return [...profileIds] + .sort(compareBackendProfiles) + .map((profile) => backendCapability(backendProfileSpec(profile) as BackendProfileSpec)); } export function backendCapabilitiesSqlValues(profiles?: readonly BackendProfile[], options: { requiredSecretKeysByProfile?: Record } = {}): string { diff --git a/src/mgr/provider-profiles.ts b/src/mgr/provider-profiles.ts index 3ec25e7..becaa0b 100644 --- a/src/mgr/provider-profiles.ts +++ b/src/mgr/provider-profiles.ts @@ -1,7 +1,7 @@ import { createHash, randomUUID } from "node:crypto"; import { spawn } from "node:child_process"; import { AgentRunError } from "../common/errors.js"; -import { backendProfileSpec, backendProfileSpecs, isBackendProfileSlug } from "../common/backend-profiles.js"; +import { backendCapabilities, backendProfileSpec, backendProfileSpecs, compareBackendProfiles, isBackendProfileSlug } from "../common/backend-profiles.js"; import { dsflashGoModelCatalogFile, dsflashGoModelCatalogJson, dsflashGoModelSlug, dsflashGoRuntimeModelCatalogPath } from "../common/model-catalogs.js"; import type { AgentRunStore } from "./store.js"; import type { BackendProfile, ExecutionPolicy, JsonRecord, JsonValue } from "../common/types.js"; @@ -48,6 +48,33 @@ export async function listProviderProfiles(options: ProviderProfileOptions = {}) return { items, count: items.length, valuesPrinted: false }; } +export async function listBackendCapabilities(options: ProviderProfileOptions = {}): Promise { + const profiles = new Set(backendProfileSpecs.map((spec) => spec.profile)); + try { + for (const profile of await listProviderProfileIds(options)) profiles.add(profile); + const dynamicProfiles = [...profiles].filter((profile) => !isBuiltinProviderProfile(profile)).sort(compareBackendProfiles); + return { + items: backendCapabilities([...profiles]), + dynamicProfileDiscovery: { + status: "succeeded", + dynamicProfiles, + dynamicProfileCount: dynamicProfiles.length, + valuesPrinted: false, + }, + }; + } catch (error) { + return { + items: backendCapabilities([...profiles]), + dynamicProfileDiscovery: { + status: "failed", + failureKind: error instanceof AgentRunError ? error.failureKind : "infra-failed", + message: redactText(error instanceof Error ? error.message : String(error)), + valuesPrinted: false, + }, + }; + } +} + export async function showProviderProfile(profile: string, options: ProviderProfileOptions = {}): Promise { return providerProfileStatus(validateBackendProfile(profile), options); } @@ -328,14 +355,7 @@ async function listProviderProfileIds(options: ProviderProfileOptions): Promise< } function compareProviderProfiles(left: string, right: string): number { - const leftIndex = backendProfileSpecs.findIndex((item) => item.profile === left); - const rightIndex = backendProfileSpecs.findIndex((item) => item.profile === right); - if (leftIndex >= 0 || rightIndex >= 0) { - if (leftIndex < 0) return 1; - if (rightIndex < 0) return -1; - if (leftIndex !== rightIndex) return leftIndex - rightIndex; - } - return left.localeCompare(right); + return compareBackendProfiles(left, right); } function isBuiltinProviderProfile(profile: BackendProfile): boolean { diff --git a/src/mgr/server.ts b/src/mgr/server.ts index 0426c2f..3e9ea27 100644 --- a/src/mgr/server.ts +++ b/src/mgr/server.ts @@ -14,7 +14,7 @@ import { runnerJobStatusSummary } from "./runner-job-status.js"; import { createSessionPvc, deleteSessionPvc, getSessionPvcSummary, refreshSessionPvcSummary, runSessionStorageGc } from "./session-pvc.js"; import type { SessionPvcSummary } from "./session-pvc.js"; import type { SessionPvcOptions } from "./session-pvc.js"; -import { getProviderProfileConfig, getProviderProfileValidation, listProviderProfiles, removeProviderProfile, setProviderProfileConfig, setProviderProfileCredential, showProviderProfile, validateProviderProfile } from "./provider-profiles.js"; +import { getProviderProfileConfig, getProviderProfileValidation, listBackendCapabilities, listProviderProfiles, removeProviderProfile, setProviderProfileConfig, setProviderProfileCredential, showProviderProfile, validateProviderProfile } from "./provider-profiles.js"; function pvcOptions(defaults: { kubectlCommand?: string } | undefined): SessionPvcOptions { return defaults?.kubectlCommand ? { kubectlCommand: defaults.kubectlCommand } : {}; @@ -92,7 +92,7 @@ async function route({ method, url, body, store, sourceCommit, runnerJobDefaults const ready = path === "/health/live" ? true : database.ready; return { serviceId: "agentrun-mgr", live: true, ready, database, sourceCommit, secretRefs: { databaseUrl: database.adapter === "postgres" ? "redacted" : "not-used", valuesPrinted: false } }; } - if (method === "GET" && path === "/api/v1/backends") return { items: await store.backends() as unknown as JsonValue }; + if (method === "GET" && path === "/api/v1/backends") return await listBackendCapabilities(providerProfileDefaults) as JsonValue; if (method === "GET" && path === "/api/v1/provider-profiles") return await listProviderProfiles(providerProfileDefaults) as JsonValue; const providerProfileMatch = path.match(/^\/api\/v1\/provider-profiles\/([^/]+)$/u); if (method === "GET" && providerProfileMatch) return await showProviderProfile(providerProfileMatch[1] ?? "", providerProfileDefaults) as JsonValue; diff --git a/src/selftest/cases/45-provider-profile-management.ts b/src/selftest/cases/45-provider-profile-management.ts index c42bc26..0f0a882 100644 --- a/src/selftest/cases/45-provider-profile-management.ts +++ b/src/selftest/cases/45-provider-profile-management.ts @@ -296,6 +296,14 @@ process.exit(1); assert.equal(listAfterDynamic.count, 5); const dynamicListItems = (listAfterDynamic.items as JsonRecord[]) ?? []; assert.equal(dynamicListItems.some((item) => item.profile === dynamicProfile), true); + const backendsAfterDynamic = await client.get("/api/v1/backends") as JsonRecord; + const backendItems = (backendsAfterDynamic.items as JsonRecord[]) ?? []; + const dynamicBackend = backendItems.find((item) => item.profile === dynamicProfile) as JsonRecord | undefined; + assert.equal(dynamicBackend?.backendKind, "codex-app-server-stdio"); + assert.equal(((dynamicBackend?.defaultSecretRef as JsonRecord).name), `agentrun-v01-provider-${dynamicProfile}`); + assert.equal(((backendsAfterDynamic.dynamicProfileDiscovery as JsonRecord).status), "succeeded"); + assert.equal(((backendsAfterDynamic.dynamicProfileDiscovery as JsonRecord).dynamicProfiles as string[]).includes(dynamicProfile), true); + assertNoSecretLeak(backendsAfterDynamic); assertNoSecretLeak(dynamicCredential); const removedDeepseek = await client.delete("/api/v1/provider-profiles/deepseek") as JsonRecord; @@ -345,7 +353,7 @@ process.exit(1); assert.equal(finalValidation.status, "completed"); assert.equal(JSON.stringify(finalValidation).includes(secretText), false); assertNoSecretLeak(finalValidation); - return { name: "provider-profile-management", tests: ["provider-profiles-list-redacted", "provider-profile-config", "provider-profile-set-key-redacted", "provider-profile-set-auth-json-redacted", "provider-profile-secret-replace-annotation-cleanup", "provider-profile-secret-create-upsert", "provider-profile-config-only-create", "provider-profile-dsflash-go-model-catalog", "provider-profile-dynamic-slug-roundtrip", "provider-profile-remove-builtin", "provider-profile-remove-dynamic-slug", "provider-profile-deepseek-moon-bridge", "provider-profile-manager-secret-rbac", "provider-profile-validation-runner-job"] }; + return { name: "provider-profile-management", tests: ["provider-profiles-list-redacted", "provider-profile-config", "provider-profile-set-key-redacted", "provider-profile-set-auth-json-redacted", "provider-profile-secret-replace-annotation-cleanup", "provider-profile-secret-create-upsert", "provider-profile-config-only-create", "provider-profile-dsflash-go-model-catalog", "provider-profile-dynamic-slug-roundtrip", "backends-list-dynamic-profile", "provider-profile-remove-builtin", "provider-profile-remove-dynamic-slug", "provider-profile-deepseek-moon-bridge", "provider-profile-manager-secret-rbac", "provider-profile-validation-runner-job"] }; } finally { await new Promise((resolve) => server.server.close(() => resolve())); }