feat: 支持 provider profile auth.json CLI 写入
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user