Merge pull request #107 from pikasTech/fix/issue105-dynamic-profile-visibility

v0.1:动态 provider profile 纳入 backends 可见性
This commit is contained in:
Lyon
2026-06-09 00:30:50 +08:00
committed by GitHub
6 changed files with 65 additions and 20 deletions
+1 -1
View File
@@ -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。
+5 -5
View File
@@ -801,12 +801,12 @@ function help(args: ParsedArgs, group?: string): JsonRecord {
"runs events <runId> --after-seq <n> --limit <n>",
"runs result <runId> [--command-id <commandId>]",
"runs cancel <runId> [--reason <text>]",
"sessions ps [--state default|running|unread|terminal|idle|all] [--profile codex|deepseek|minimax-m3|dsflash-go|M3] [--reader-id <reader>]",
"sessions create [sessionId] [--profile codex|deepseek|minimax-m3|dsflash-go|M3] [--expires-in-days <n>]",
"sessions ps [--state default|running|unread|terminal|idle|all] [--profile codex|deepseek|minimax-m3|dsflash-go|<dynamic-profile>|M3] [--reader-id <reader>]",
"sessions create [sessionId] [--profile codex|deepseek|minimax-m3|dsflash-go|<dynamic-profile>|M3] [--expires-in-days <n>]",
"sessions storage <sessionId>",
"sessions storage <sessionId> --delete",
"sessions show <sessionId> [--reader-id <reader>]",
"sessions turn [sessionId] --json-file <run-base.json> --prompt-file <file> [--profile minimax-m3|dsflash-go|M3] [--runner-json-file <job.json>]",
"sessions turn [sessionId] --json-file <run-base.json> --prompt-file <file> [--profile codex|deepseek|minimax-m3|dsflash-go|<dynamic-profile>|M3] [--runner-json-file <job.json>]",
"sessions steer <sessionId> --prompt-file <file>",
"sessions cancel <sessionId> [--reason <text>]",
"sessions trace <sessionId> [--after-seq <n>] [--limit <n>] [--run-id <runId>]",
@@ -816,7 +816,7 @@ function help(args: ParsedArgs, group?: string): JsonRecord {
"commands show <commandId> --run-id <runId>",
"commands result <commandId> --run-id <runId>",
"commands cancel <commandId> [--reason <text>]",
"runner start --run-id <runId> [--backend codex|deepseek|minimax-m3|dsflash-go]",
"runner start --run-id <runId> [--backend codex|deepseek|minimax-m3|dsflash-go|<dynamic-profile>]",
"runner job --run-id <runId> --command-id <commandId> [--image <image>] [--runner-manager-url <url>] [--idempotency-key <key>]",
"runner job --dry-run --run-id <runId> --command-id <commandId> --image <image>",
"runner jobs --run-id <runId> [--command-id <commandId>]",
@@ -830,7 +830,7 @@ function help(args: ParsedArgs, group?: string): JsonRecord {
"queue cancel <taskId> [--reason <text>]",
"queue dispatch <taskId> [--json-file <dispatch.json>] [--idempotency-key <key>] [--image <image>] [--namespace <namespace>]",
"queue refresh <taskId>",
"secrets codex render --dry-run [--profile codex|deepseek|minimax-m3|dsflash-go] [--codex-home <dir>] [--model-catalog-file <file>] [--namespace agentrun-v01] [--secret-name <name>]",
"secrets codex render --dry-run [--profile codex|deepseek|minimax-m3|dsflash-go|<dynamic-profile>] [--codex-home <dir>] [--model-catalog-file <file>] [--namespace agentrun-v01] [--secret-name <name>]",
"provider-profiles list",
"provider-profiles show <profile>",
"provider-profiles config <profile>",
+19 -2
View File
@@ -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<BackendProfile>(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, readonly string[]> } = {}): string {
+29 -9
View File
@@ -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<JsonRecord> {
const profiles = new Set<BackendProfile>(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<JsonRecord> {
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 {
+2 -2
View File
@@ -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;
@@ -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<void>((resolve) => server.server.close(() => resolve()));
}