feat: 支持 provider profile auth.json CLI 写入

This commit is contained in:
Codex Agent
2026-06-08 10:59:38 +08:00
parent c0b3939624
commit c52ba0dac2
4 changed files with 94 additions and 6 deletions
@@ -139,6 +139,44 @@ test("provider profile set-key delegates through HWLAB without echoing API key",
}
});
test("provider profile auth-json delegates through HWLAB without echoing auth json", async () => {
const calls: Array<{ method?: string; path?: string; body?: any }> = [];
const agentRunServer = await startAgentRunServer(calls);
const { port: agentRunPort } = agentRunServer.address() as { port: number };
const server = createCloudApiServer({
env: testEnv(agentRunPort),
accessController: fakeAccessController({ actor: adminActor, authMethod: "api-key" })
});
await listen(server);
const { port } = server.address() as { port: number };
const authJson = `${JSON.stringify({ OPENAI_API_KEY: "sk-test-provider-secret-123456" })}\n`;
try {
const response = await fetch(`http://127.0.0.1:${port}/v1/admin/provider-profiles/hy/credential`, {
method: "PUT",
headers: { "content-type": "application/json", authorization: "Bearer hwlab-test" },
body: JSON.stringify({ authJson })
});
const body = await response.json();
const text = JSON.stringify(body);
assert.equal(response.status, 200);
assert.equal(body.ok, true);
assert.equal(body.status, "updated");
assert.equal(body.profile, "hy");
assert.equal(text.includes("OPENAI_API_KEY"), false);
assert.equal(text.includes("sk-test-provider-secret-123456"), false);
assert.equal(calls[0].method, "PUT");
assert.equal(calls[0].path, "/api/v1/provider-profiles/hy/credential");
assert.equal(calls[0].body.authJson, authJson);
assert.equal(calls[0].body.apiKey, undefined);
assert.equal(calls[0].body.delegatedBy.system, "hwlab-v02");
assert.equal(calls[0].body.delegatedBy.authMethod, "api-key");
} finally {
await close(server);
await close(agentRunServer);
}
});
test("provider profile config reads and writes config.toml through AgentRun", async () => {
const calls: Array<{ method?: string; path?: string; body?: any }> = [];
const agentRunServer = await startAgentRunServer(calls);
@@ -256,6 +294,10 @@ async function startAgentRunServer(calls: Array<{ method?: string; path?: string
response.end(`${JSON.stringify({ ok: true, traceId: "trc_set_key", data: { action: "provider-profile-credential-updated", mutation: true, profile: "deepseek", configured: true, secretRef: { namespace: "agentrun-v01", name: "agentrun-v01-provider-deepseek", keys: ["auth.json", "config.toml"] }, credentialHashSuffix: "abc12345", configHashSuffix: "def67890", updatedAt: "2026-06-05T08:00:00.000Z", requiresExternalBridgeUpdate: true, valuesPrinted: false } })}\n`);
return;
}
if (request.method === "PUT" && url.pathname === "/api/v1/provider-profiles/hy/credential") {
response.end(`${JSON.stringify({ ok: true, traceId: "trc_set_auth_json", data: { action: "provider-profile-credential-updated", mutation: true, profile: "hy", configured: true, credentialSource: "auth-json", secretRef: { namespace: "agentrun-v01", name: "agentrun-v01-provider-hy", keys: ["auth.json", "config.toml"] }, credentialHashSuffix: "1234abcd", configHashSuffix: "def67890", updatedAt: "2026-06-05T08:00:00.000Z", valuesPrinted: false } })}\n`);
return;
}
if (request.method === "GET" && url.pathname === "/api/v1/provider-profiles/deepseek/config") {
response.end(`${JSON.stringify({ ok: true, traceId: "trc_config", data: { action: "provider-profile-config", profile: "deepseek", configured: true, secretRef: { namespace: "agentrun-v01", name: "agentrun-v01-provider-deepseek", keys: ["auth.json", "config.toml"] }, credentialHashSuffix: "abc12345", configHashSuffix: "def67890", updatedAt: "2026-06-05T08:00:00.000Z", configToml: 'model = "deepseek-chat"\nbase_url = "http://bridge/v1"\n', configTomlPrinted: true, credentialValuesPrinted: false, valuesPrinted: false } })}\n`);
return;
@@ -65,9 +65,10 @@ export async function handleProviderProfilesHttp(request, response, url, options
if (route.kind === "credential") {
const body = await readJsonObject(request, options.bodyLimitBytes);
const apiKey = text(body.apiKey);
if (apiKey.length < 8) return sendError(response, 400, "api_key_required", "apiKey is required and must not be empty");
const authJson = typeof body.authJson === "string" ? body.authJson : "";
if (apiKey.length < 8 && !authJson.trim()) return sendError(response, 400, "credential_required", "apiKey or authJson is required and must not be empty");
const delegatedBody = {
apiKey,
...(authJson.trim() ? { authJson } : { apiKey }),
...(body.config && typeof body.config === "object" ? { config: body.config } : {}),
delegatedBy: delegatedBy(auth, requestId),
reason: text(body.reason) || "hwlab-provider-management"
+23
View File
@@ -126,6 +126,29 @@ test("hwlab-cli client provider-profiles set-key reads stdin and never prints ke
assert.equal(JSON.stringify(result.payload).includes(secret), false);
});
test("hwlab-cli client provider-profiles set-auth-json reads stdin and never prints auth json", async () => {
const calls: any[] = [];
const authJson = `${JSON.stringify({ OPENAI_API_KEY: "sk-test-provider-secret-123456" })}\n`;
const result = await runHwlabCli(["client", "provider-profiles", "set-auth-json", "hy", "--auth-json-stdin", "--base-url", "http://web.test"], {
stdinText: authJson,
env: { HWLAB_API_KEY: "hwl_live_admin_test_key" },
fetchImpl: async (url, init) => {
calls.push({ url: String(url), init, body: JSON.parse(String(init?.body ?? "{}")) });
return new Response(JSON.stringify({ ok: true, status: "updated", profile: "hy", data: { profile: "hy", configured: true, credentialSource: "auth-json", secretRef: { namespace: "agentrun-v01", name: "agentrun-v01-provider-hy", keys: ["auth.json", "config.toml"] }, credentialHashSuffix: "abc12345", configHashSuffix: "def67890", valuesPrinted: false }, valuesPrinted: false }), { status: 200 });
}
});
assert.equal(result.exitCode, 0);
assert.equal(calls[0].url, "http://web.test/v1/admin/provider-profiles/hy/credential");
assert.equal(calls[0].init.method, "PUT");
assert.equal(calls[0].body.authJson, authJson);
assert.equal(calls[0].body.apiKey, undefined);
assert.equal(result.payload.action, "client.provider-profiles.set-auth-json");
assert.equal(result.payload.body.profileStatus.profile, "hy");
assert.equal(JSON.stringify(result.payload).includes("OPENAI_API_KEY"), false);
assert.equal(JSON.stringify(result.payload).includes("sk-test-provider-secret-123456"), false);
});
test("hwlab-cli client provider-profiles config reads and writes config.toml", async () => {
const calls: any[] = [];
const configToml = 'model = "deepseek-chat"\nbase_url = "http://bridge/v1"\n';
+26 -4
View File
@@ -319,11 +319,20 @@ async function providerProfilesCommand(context: any) {
}
if (subcommand === "set-key" || subcommand === "set" || subcommand === "credential") {
const profile = normalizeProviderProfile(context.parsed.profileName ?? context.parsed.profileId ?? context.rest[1]);
if (context.parsed.keyStdin !== true) throw cliError("key_stdin_required", "set-key requires --key-stdin so the API key is not passed in argv", { command: "provider-profiles set-key PROFILE --key-stdin" });
const apiKey = await providerProfileApiKeyFromStdin(context);
const authJsonMode = context.parsed.authJsonStdin === true;
if (!authJsonMode && context.parsed.keyStdin !== true) throw cliError("credential_stdin_required", "set-key requires --key-stdin, or use set-auth-json PROFILE --auth-json-stdin for Codex auth.json", { command: "provider-profiles set-key PROFILE --key-stdin" });
const credentialBody = authJsonMode ? { authJson: await providerProfileAuthJsonFromStdin(context) } : { apiKey: await providerProfileApiKeyFromStdin(context) };
const pathName = `/v1/admin/provider-profiles/${encodeURIComponent(profile)}/credential`;
const response = await requestJson({ ...context, method: "PUT", path: pathName, body: { apiKey }, timeoutMs: numberOption(context.parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS });
return responsePayload("client.provider-profiles.set-key", response, context, { route: route("PUT", pathName), profile, body: providerProfileBodyForCli(response.body, context.parsed), valuesPrinted: false });
const response = await requestJson({ ...context, method: "PUT", path: pathName, body: credentialBody, timeoutMs: numberOption(context.parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS });
return responsePayload(authJsonMode ? "client.provider-profiles.set-auth-json" : "client.provider-profiles.set-key", response, context, { route: route("PUT", pathName), profile, body: providerProfileBodyForCli(response.body, context.parsed), valuesPrinted: false });
}
if (subcommand === "set-auth-json" || subcommand === "auth-json" || subcommand === "set-auth") {
const profile = normalizeProviderProfile(context.parsed.profileName ?? context.parsed.profileId ?? context.rest[1]);
if (context.parsed.authJsonStdin !== true) throw cliError("auth_json_stdin_required", "set-auth-json requires --auth-json-stdin so auth.json is not passed in argv", { command: "provider-profiles set-auth-json PROFILE --auth-json-stdin" });
const authJson = await providerProfileAuthJsonFromStdin(context);
const pathName = `/v1/admin/provider-profiles/${encodeURIComponent(profile)}/credential`;
const response = await requestJson({ ...context, method: "PUT", path: pathName, body: { authJson }, timeoutMs: numberOption(context.parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS });
return responsePayload("client.provider-profiles.set-auth-json", response, context, { route: route("PUT", pathName), profile, body: providerProfileBodyForCli(response.body, context.parsed), valuesPrinted: false });
}
if (subcommand === "config") {
const profile = normalizeProviderProfile(context.parsed.profileName ?? context.parsed.profileId ?? context.rest[1]);
@@ -406,6 +415,7 @@ function providerProfilesHelp() {
"remove PROFILE",
"set-config PROFILE --config-stdin",
"set-key PROFILE --key-stdin",
"set-auth-json PROFILE --auth-json-stdin",
"validate PROFILE [--wait] [--timeout-ms N] [--poll-interval-ms N]",
"validation PROFILE VALIDATION_ID"
],
@@ -433,6 +443,18 @@ async function providerProfileConfigFromStdin(context: any) {
return configToml;
}
async function providerProfileAuthJsonFromStdin(context: any) {
const authJson = context.stdinText !== undefined ? String(context.stdinText) : await readRawStdin();
if (!authJson.trim()) throw cliError("auth_json_empty", "provider profile auth.json from stdin must not be empty");
try {
const parsed = JSON.parse(authJson);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("auth.json object expected");
} catch {
throw cliError("auth_json_invalid", "provider profile auth.json from stdin must be a JSON object");
}
return authJson;
}
function normalizeProviderProfile(value: unknown) {
const profile = text(value).toLowerCase();
const normalized = profile === "codex" ? "codex-api" : profile;