feat: 支持 provider profile config.toml 管理

This commit is contained in:
Codex
2026-06-05 22:35:40 +08:00
parent 17e9cd3995
commit dd58cf9a8e
5 changed files with 163 additions and 3 deletions
+107
View File
@@ -48,6 +48,92 @@ export async function showProviderProfile(profile: string, options: ProviderProf
return providerProfileStatus(validateBackendProfile(profile), options);
}
export async function getProviderProfileConfig(profileValue: string, options: ProviderProfileOptions = {}): Promise<JsonRecord> {
const profile = validateBackendProfile(profileValue);
const spec = requiredSpec(profile);
const namespace = profileNamespace(options);
const secret = await kubectlGetSecret(spec.defaultSecretName, namespace, options.kubectlCommand ?? "kubectl");
if (!secret) throw new AgentRunError("secret-unavailable", `provider profile ${profile} Secret is not configured`, { httpStatus: 404, details: { profile, secretRef: secretRefSummary(profile, namespace) } });
const data = asOptionalRecord(secret.data);
const annotations = asOptionalRecord(asOptionalRecord(secret.metadata)?.annotations);
const configToml = configTomlFromData(data, profile);
return {
action: "provider-profile-config-read",
profile,
configured: hasRequiredKeys(data, spec.requiredSecretKeys),
secretRef: secretRefSummary(profile, namespace),
resourceVersion: stringPath(secret, ["metadata", "resourceVersion"]),
credentialHashSuffix: hashDataKey(data, "auth.json") ?? stringPath(annotations, [`${credentialAnnotationPrefix}-credential-hash-suffix`]),
configHashSuffix: shortHash(configToml),
updatedAt: stringPath(annotations, [`${credentialAnnotationPrefix}-updated-at`]) ?? stringPath(secret, ["metadata", "creationTimestamp"]),
configToml,
configTomlPrinted: true,
credentialValuesPrinted: false,
valuesPrinted: false,
};
}
export async function setProviderProfileConfig(profileValue: string, body: unknown, options: ProviderProfileOptions = {}): Promise<JsonRecord> {
const profile = validateBackendProfile(profileValue);
const spec = requiredSpec(profile);
const record = asRecord(body ?? {}, "providerProfileConfig");
const configToml = configTomlField(record);
const delegatedBy = delegatedBySummary(record.delegatedBy);
const namespace = profileNamespace(options);
const existingSecret = await kubectlGetSecret(spec.defaultSecretName, namespace, options.kubectlCommand ?? "kubectl");
if (!existingSecret) throw new AgentRunError("secret-unavailable", `provider profile ${profile} Secret is not configured`, { httpStatus: 404, details: { profile, secretRef: secretRefSummary(profile, namespace) } });
const existingData = asOptionalRecord(existingSecret.data);
const authJsonData = dataKey(existingData, "auth.json");
if (!authJsonData) throw new AgentRunError("secret-unavailable", `provider profile ${profile} auth.json is not configured`, { httpStatus: 400, details: { profile, secretRef: secretRefSummary(profile, namespace), key: "auth.json" } });
const credentialHashSuffix = hashDataKey(existingData, "auth.json");
const secretManifest: JsonRecord = {
apiVersion: "v1",
kind: "Secret",
metadata: {
name: spec.defaultSecretName,
namespace,
labels: {
"app.kubernetes.io/part-of": "agentrun",
"agentrun.pikastech.local/profile": profile,
},
annotations: {
[`${credentialAnnotationPrefix}-profile`]: profile,
...(credentialHashSuffix ? { [`${credentialAnnotationPrefix}-credential-hash-suffix`]: credentialHashSuffix } : {}),
[`${credentialAnnotationPrefix}-config-hash-suffix`]: shortHash(configToml),
[`${credentialAnnotationPrefix}-updated-at`]: new Date().toISOString(),
...(delegatedBy ? { [`${credentialAnnotationPrefix}-delegated-system`]: delegatedBy.system, [`${credentialAnnotationPrefix}-delegated-request-id`]: delegatedBy.requestId ?? "" } : {}),
},
},
type: "Opaque",
data: {
"auth.json": authJsonData,
"config.toml": base64Data(configToml),
},
};
const applied = await kubectlReplaceSecret(secretManifest, options.kubectlCommand ?? "kubectl");
return {
action: "provider-profile-config-updated",
mutation: true,
profile,
configured: true,
secretRef: secretRefSummary(profile, namespace),
resourceVersion: objectPath(applied, ["metadata", "resourceVersion"]),
credentialHashSuffix,
configHashSuffix: shortHash(configToml),
updatedAt: objectPath(applied, ["metadata", "annotations", `${credentialAnnotationPrefix}-updated-at`]) ?? new Date().toISOString(),
delegatedBy,
requiresExternalBridgeUpdate: profile === "deepseek",
configTomlPrinted: false,
credentialValuesPrinted: false,
valuesPrinted: false,
pollCommands: {
config: `./scripts/agentrun provider-profiles config ${profile}`,
show: `./scripts/agentrun provider-profiles show ${profile}`,
validate: `./scripts/agentrun provider-profiles validate ${profile} --wait --timeout-ms 120000`,
},
};
}
export async function setProviderProfileCredential(profileValue: string, body: unknown, options: ProviderProfileOptions = {}): Promise<JsonRecord> {
const profile = validateBackendProfile(profileValue);
const spec = requiredSpec(profile);
@@ -287,6 +373,22 @@ function existingConfigAllowed(profile: BackendProfile, configToml: string): boo
return true;
}
function configTomlFromData(data: JsonRecord | null, profile: BackendProfile): string {
const encoded = dataKey(data, "config.toml");
if (!encoded) throw new AgentRunError("secret-unavailable", `provider profile ${profile} config.toml is not configured`, { httpStatus: 404, details: { profile, key: "config.toml" } });
try {
return Buffer.from(encoded, "base64").toString("utf8");
} catch {
throw new AgentRunError("schema-invalid", `provider profile ${profile} config.toml is not valid base64`, { httpStatus: 400, details: { profile, key: "config.toml" } });
}
}
function configTomlField(record: JsonRecord): string {
const value = record.configToml;
if (typeof value !== "string" || value.trim().length === 0) throw new AgentRunError("schema-invalid", "configToml is required", { httpStatus: 400 });
return value;
}
function configField(value: unknown, profile: BackendProfile): ProfileConfig {
const defaults = defaultConfig(profile);
if (value === undefined || value === null) return defaults;
@@ -493,6 +595,11 @@ function base64Data(value: string): string {
return Buffer.from(value, "utf8").toString("base64");
}
function dataKey(data: JsonRecord | null, key: string): string | null {
const value = data?.[key];
return typeof value === "string" && value.length > 0 ? value : null;
}
function shortHash(value: string): string {
return createHash("sha256").update(value).digest("hex").slice(0, 12);
}
+4 -1
View File
@@ -13,7 +13,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 { getProviderProfileValidation, listProviderProfiles, setProviderProfileCredential, showProviderProfile, validateProviderProfile } from "./provider-profiles.js";
import { getProviderProfileConfig, getProviderProfileValidation, listProviderProfiles, setProviderProfileConfig, setProviderProfileCredential, showProviderProfile, validateProviderProfile } from "./provider-profiles.js";
function pvcOptions(defaults: { kubectlCommand?: string } | undefined): SessionPvcOptions {
return defaults?.kubectlCommand ? { kubectlCommand: defaults.kubectlCommand } : {};
@@ -95,6 +95,9 @@ async function route({ method, url, body, store, sourceCommit, runnerJobDefaults
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;
const providerConfigMatch = path.match(/^\/api\/v1\/provider-profiles\/([^/]+)\/config$/u);
if (method === "GET" && providerConfigMatch) return await getProviderProfileConfig(providerConfigMatch[1] ?? "", providerProfileDefaults) as JsonValue;
if (method === "PUT" && providerConfigMatch) return await setProviderProfileConfig(providerConfigMatch[1] ?? "", body, providerProfileDefaults) as JsonValue;
const providerCredentialMatch = path.match(/^\/api\/v1\/provider-profiles\/([^/]+)\/credential$/u);
if (method === "PUT" && providerCredentialMatch) return await setProviderProfileCredential(providerCredentialMatch[1] ?? "", body, providerProfileDefaults) as JsonValue;
const providerValidationCreateMatch = path.match(/^\/api\/v1\/provider-profiles\/([^/]+)\/validate$/u);
@@ -92,6 +92,27 @@ process.exit(1);
assert.equal(JSON.stringify(list).includes("auth.json"), true);
assert.equal(JSON.stringify(list).includes("redacted-fixture"), false);
const config = await client.get("/api/v1/provider-profiles/deepseek/config") as JsonRecord;
assert.equal(config.profile, "deepseek");
assert.equal(config.configToml, "model = \"fixture\"\n");
assert.equal(config.configTomlPrinted, true);
assert.equal(JSON.stringify(config).includes("redacted-fixture"), false);
const updatedConfigToml = "model = \"fixture-updated\"\n";
const updatedConfig = await client.put("/api/v1/provider-profiles/deepseek/config", {
configToml: updatedConfigToml,
delegatedBy: { system: "hwlab-v02", userId: "u1", username: "tester", requestId: "req-config-selftest" },
reason: "self-test",
}) as JsonRecord;
assert.equal(updatedConfig.profile, "deepseek");
assert.equal(updatedConfig.configTomlPrinted, false);
assert.equal(JSON.stringify(updatedConfig).includes(updatedConfigToml), false);
const configReplaceRecord = JSON.parse(await readFile(replacedSecretPath, "utf8")) as JsonRecord;
const configSecretManifest = configReplaceRecord.manifest as JsonRecord;
const configData = configSecretManifest.data as JsonRecord;
assert.equal(Buffer.from(String(configData["auth.json"]), "base64").toString("utf8").includes("redacted-fixture"), true);
assert.equal(Buffer.from(String(configData["config.toml"]), "base64").toString("utf8"), updatedConfigToml);
const updated = await client.put("/api/v1/provider-profiles/deepseek/credential", {
apiKey: secretText,
delegatedBy: { system: "hwlab-v02", userId: "u1", username: "tester", requestId: "req-selftest" },
@@ -147,7 +168,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-set-key-redacted", "provider-profile-secret-replace-annotation-cleanup", "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-secret-replace-annotation-cleanup", "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()));
}