Skip to content

Commit 9cad199

Browse files
committed
feat(agent): validate by client apiKey auth type
1 parent 64335a6 commit 9cad199

30 files changed

Lines changed: 516 additions & 304 deletions

docs/agents/auth-change.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,18 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
5353

5454
命令不要直接解析 token、env 或 config。业务请求统一走 `ctx.client`;登录/配置命令通过 `ctx.authStore` / `ctx.configStore` 的窄接口操作落盘。
5555

56-
### 例外:agent 命令的 SDK 凭证桥接
56+
### 例外:agent 命令的 SDK 凭证内存注入
5757

58-
`bl managed-agent *` 命令声明 `auth: "none"`,凭证由 `@openagentpack/sdk` 自主从 env 解析(agents.yaml 的 `${DASHSCOPE_API_KEY}` / `${BAILIAN_WORKSPACE_ID}` 插值)。为让 bl 登录态复用,`packages/commands/src/commands/managed-agent/_engine/credentials.ts``bridgeBailianCredentials()`**直接 `readConfigFile()`**,把 config 的 `api_key` / `workspace_id` 作为最低优先级兜底填入对应 env,仅填空值,不覆盖已有。这是唯一允许命令层直接读 config 的场景(SDK 只认 env,不走 `ctx.client`);优先级链:`~/.agents/config.json` > shell env > `.env` > `~/.bailian/config.json`
58+
`bl managed-agent *` 的全部命令声明 `auth: "apiKey"`(含纯本地脚手架 `init` —— 统一登录门槛,无例外),bailian 凭证由 authStage 经 `resolveApiKey(sources)` 权威解析(flag > env > active profile config,缺失时抛统一 AUTH 错误)。
59+
60+
凭证不再以真实值写入 `process.env`,而是经 `packages/commands/src/commands/managed-agent/_engine/`**内存注入管道**(`resolveAgentProjectConfig`)注入 SDK,管道四步:
61+
62+
1. `prepareProviderEnv()` — 先 `bootstrapRuntimeCredentialsSync()`(SDK 把 `.env` / `~/.agents/config.json` 灌进 env,服务 claude/ark/qoder 等非 bailian provider),再把全部凭证类 env(`CREDENTIAL_ENV_KEYS`,含别名)中仍为 undefined 的占位为 `""`,使 agents.yaml 插值不因缺变量抛错
63+
2. `resolveProjectConfig` — 插值发生:bailian 插值拿到占位空串,claude/ark 拿到真实 env 值
64+
3. `injectProviderCredentials()` — 用 `ctx.client.exportApiCredential()`(lint 限定 `managed-agent/_engine/**` 可用)覆写内存 config 对象的 bailian 块:`api_key` 无条件覆写;`base_url`(拼 `/api/v1/agentstudio` 后缀)/`workspace_id`(取 `settings.workspaceId`)仅在引用且为空时填充
65+
4. `scrubCredentialEnv()` + `assertProviderCredentials()` — 从 `process.env` 删除全部凭证变量(真实凭证此后只存于 config 对象 → provider adapter 实例内存,不驻留 env / 不被子进程继承);任一已声明 provider 的 `api_key` 为空 → CLI 权威 `AUTH` 错误 + provider 专属 hint(取代 SDK 原始插值/zod 报错)
66+
67+
`bl auth login` 仅管理 bailian(DashScope)凭证;claude/ark/qoder 的 key 从 env(shell / `.env` / `~/.agents/config.json`)经插值进入 config 对象,同样被清扫。禁止命令层直接 `readConfigFile` 裸读凭证;bailian 字段以 CLI 鉴权链为唯一信源。
5968

6069
## 必查清单
6170

packages/commands/src/commands/auth/login.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,6 @@ export default defineCommand({
3131
valueHint: "<url>",
3232
description: "Model API base URL (used with --api-key for validation)",
3333
},
34-
agentstudioBaseUrl: {
35-
type: "string",
36-
valueHint: "<url>",
37-
description:
38-
"Bailian AgentStudio base URL for `bl managed-agent` commands (sets BAILIAN_BASE_URL; used with --api-key)",
39-
},
4034
console: {
4135
type: "switch",
4236
description:
@@ -78,9 +72,6 @@ export default defineCommand({
7872
if (!apiKeyMode && hasValue(f.baseUrl)) {
7973
return "Use --base-url only with --api-key";
8074
}
81-
if (!apiKeyMode && hasValue(f.agentstudioBaseUrl)) {
82-
return "Use --agentstudio-base-url only with --api-key";
83-
}
8475
if (!consoleMode && hasValue(f.consoleSite)) {
8576
return "Use --console-site only with --console";
8677
}
@@ -162,9 +153,6 @@ export default defineCommand({
162153
await validateAndPersistApiKey(deps, key, {
163154
baseUrl: resolvedBaseUrl,
164155
persistBaseUrl,
165-
persistPatch: flags.agentstudioBaseUrl
166-
? { agentstudio_base_url: flags.agentstudioBaseUrl }
167-
: undefined,
168156
defaultTextModel: profilePreset?.defaultTextModel,
169157
defaultVideoModel: profilePreset?.defaultVideoModel,
170158
defaultImageToVideoModel: profilePreset?.defaultImageToVideoModel,

packages/commands/src/commands/config/set.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@ export default defineCommand({
1111
type: "string",
1212
valueHint: "<key>",
1313
description:
14-
"Config key (base_url, output, output_dir, timeout, api_key, access_token, access_key_id, access_key_secret, security_token, default_*_model, workspace_id, agentstudio_base_url)",
14+
"Config key (base_url, output, output_dir, timeout, api_key, access_token, access_key_id, access_key_secret, security_token, default_*_model, workspace_id)",
15+
required: true,
16+
},
17+
value: {
18+
type: "string",
19+
valueHint: "<value>",
20+
description: "Value to set",
1521
required: true,
1622
},
17-
value: { type: "string", valueHint: "<value>", description: "Value to set", required: true },
1823
},
1924
exampleArgs: [
2025
"--key output --value json",
@@ -44,7 +49,9 @@ export default defineCommand({
4449
return;
4550
}
4651

47-
await ctx.configStore.write({ [resolvedKey]: coerced } as Partial<ConfigFile>);
52+
await ctx.configStore.write({
53+
[resolvedKey]: coerced,
54+
} as Partial<ConfigFile>);
4855

4956
if (!settings.quiet) {
5057
const shown = SECRET_KEYS.has(resolvedKey) ? maskToken(String(coerced)) : coerced;

packages/commands/src/commands/config/shared.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ export const VALID_KEYS = [
1919
"default_speech_model",
2020
"default_omni_model",
2121
"workspace_id",
22-
"agentstudio_base_url",
2322
] as const;
2423

2524
// Keys whose values are secrets. `config set` / `config show` mask these; the
@@ -50,7 +49,6 @@ export const KEY_ALIASES: Record<string, string> = {
5049
"default-speech-model": "default_speech_model",
5150
"default-omni-model": "default_omni_model",
5251
"workspace-id": "workspace_id",
53-
"agentstudio-base-url": "agentstudio_base_url",
5452
};
5553

5654
/** Resolve a hyphen alias to its underscore config key. */

packages/commands/src/commands/managed-agent/_engine/config-loader.ts

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,71 @@
11
import {
22
createProjectRuntime,
3+
type LoadedProjectConfig,
34
type ProjectRuntimeContext,
45
resolveProjectConfig,
56
UserError,
67
} from "@openagentpack/sdk";
7-
import { ensureCredentials } from "./credentials.ts";
8+
import {
9+
assertProviderCredentials,
10+
type CredentialHost,
11+
injectProviderCredentials,
12+
prepareProviderEnv,
13+
scrubCredentialEnv,
14+
} from "./credentials.ts";
815
import { loadFileState } from "./file-state-manager.ts";
916
import { type HostContext, installSdkTransport } from "./transport.ts";
1017

1118
export { CREDENTIALS_NOTE } from "./credentials.ts";
1219

20+
interface AgentConfigOptions {
21+
resolveEnv?: boolean;
22+
projectName?: string;
23+
statePath?: string;
24+
}
25+
26+
/**
27+
* Resolve agents.yaml with credentials injected the bl way and scrubbed from the
28+
* environment — the shared credential spine for every SDK-engine command:
29+
* 1. prepare env (SDK bootstrap for non-bailian + placeholders so interpolation
30+
* never throws on a value we're about to supply/reject)
31+
* 2. resolve + interpolate the config
32+
* 3. override the bailian block with the CLI auth chain's credential (in-memory)
33+
* 4. scrub all credential vars from process.env (real values now live only in
34+
* the config object → provider adapters, never the environment)
35+
* 5. fail with a CLI-authoritative AUTH error if any provider's key is empty
36+
*/
37+
export async function resolveAgentProjectConfig(
38+
host: CredentialHost,
39+
filePath: string,
40+
options: AgentConfigOptions = {},
41+
): Promise<LoadedProjectConfig> {
42+
prepareProviderEnv();
43+
const resolved = await resolveProjectConfig(filePath, options);
44+
injectProviderCredentials(resolved.config.providers, host);
45+
scrubCredentialEnv();
46+
assertProviderCredentials(resolved.config.providers);
47+
return resolved;
48+
}
49+
1350
/**
1451
* Build a full ProjectRuntimeContext from a config file path — the standard
1552
* entry point for agent commands that need the SDK engine. Mirrors OpenAgentPack
1653
* CLI's buildCliRuntime: resolve config → load local state → assemble runtime.
1754
* Takes the host context first so every SDK-engine command wires the
18-
* instrumented transport (UA / tracking headers / verbose) by construction.
55+
* instrumented transport (UA / tracking headers / verbose) and the bl-resolved,
56+
* in-memory-injected credential ({@link resolveAgentProjectConfig}) by construction.
1957
*/
2058
export async function buildAgentRuntime(
21-
host: HostContext,
59+
host: HostContext & CredentialHost,
2260
filePath: string,
23-
options: {
24-
resolveEnv?: boolean;
25-
projectName?: string;
26-
statePath?: string;
27-
} = {},
61+
options: AgentConfigOptions = {},
2862
): Promise<ProjectRuntimeContext & { configPath: string }> {
2963
installSdkTransport(host);
30-
ensureCredentials();
31-
const { config, configPath, projectName } = await resolveProjectConfig(filePath, options);
64+
const { config, configPath, projectName } = await resolveAgentProjectConfig(
65+
host,
66+
filePath,
67+
options,
68+
);
3269
const state = await loadFileState(configPath, options.statePath, projectName);
3370
const ctx = createProjectRuntime({
3471
projectName,
Lines changed: 122 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,141 @@
1-
import { bootstrapRuntimeCredentialsSync } from "@openagentpack/sdk";
2-
import { readConfigFile } from "bailian-cli-core";
1+
import { AGENTS_PROVIDER_FIELDS, bootstrapRuntimeCredentialsSync } from "@openagentpack/sdk";
2+
import { BailianError, type Client, ExitCode, type Settings } from "bailian-cli-core";
33

4-
let bootstrapped = false;
4+
/**
5+
* AgentStudio API path the SDK's BailianClient serves resources under. bl's
6+
* `base_url` is the bare model-service origin (e.g. https://dashscope.aliyuncs.com);
7+
* the SDK appends resource paths onto the bailian provider's `base_url` verbatim,
8+
* so the agent path must carry this suffix. See OpenAgentPack BailianClient.
9+
*/
10+
const AGENTSTUDIO_API_PATH = "/api/v1/agentstudio";
11+
12+
/**
13+
* Every env var the SDK recognizes as provider credential material (primary keys
14+
* from the SDK's own field map) plus bl-side interpolation aliases and bailian's
15+
* endpoint var (not part of AGENTS_PROVIDER_FIELDS). These are the only vars the
16+
* pipeline placeholders (to keep interpolation from throwing) and scrubs (so no
17+
* real credential persists in the environment).
18+
*/
19+
const CREDENTIAL_ENV_KEYS = [
20+
...new Set([
21+
...Object.values(AGENTS_PROVIDER_FIELDS).flatMap((fields) => fields.map((field) => field.key)),
22+
"BAILIAN_API_KEY",
23+
"BAILIAN_BASE_URL",
24+
"CLAUDE_API_KEY",
25+
"QODER_API_KEY",
26+
]),
27+
];
28+
29+
/** How to obtain each provider's key, surfaced in the CLI's own AUTH error when it is missing. */
30+
const CREDENTIAL_HINTS: Record<string, string> = {
31+
bailian: "Run `bl auth login --api-key <key>`, pass --api-key, or set DASHSCOPE_API_KEY.",
32+
claude: "Set ANTHROPIC_API_KEY (or CLAUDE_API_KEY) in your shell or .env.",
33+
ark: "Set ARK_API_KEY in your shell or .env.",
34+
qoder: "Set QODER_PAT (or QODER_API_KEY) in your shell or .env.",
35+
};
36+
37+
/** The slice of CommandContext the credential pipeline needs: authStage-resolved client + settings. */
38+
export interface CredentialHost {
39+
client: Client;
40+
settings: Settings;
41+
}
542

643
/**
744
* Shared `--help` note documenting where agent commands get provider
8-
* credentials. Mirrors the credential-source hint bl's native commands surface
9-
* (knowledge / usage / token-plan), adapted for the SDK's env-based resolution
10-
* and the {@link bridgeBailianCredentials} fallback. Attach to every command
11-
* that loads agents.yaml. `bl` prefix is safe: agent commands ship on `bl` only.
45+
* credentials. Bailian goes through bl's own auth chain (commands declare
46+
* `auth: "apiKey"`); other providers come from env. Either way the resolved
47+
* credential is injected into the SDK in-memory and scrubbed from the
48+
* environment. Attach to every command that loads agents.yaml. `bl` prefix is
49+
* safe: agent commands ship on `bl` only.
1250
*/
1351
export const CREDENTIALS_NOTE = [
14-
"Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_BASE_URL}).",
15-
"For the bailian provider, bl fills these from your login as a fallback: `bl auth login --api-key <key> --agentstudio-base-url <url>`.",
52+
"Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).",
53+
"Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.",
54+
"Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.",
1655
];
1756

1857
/**
19-
* Bridge bl's own login state into the env vars the OpenAgentPack SDK reads for
20-
* the bailian provider. bl persists `api_key` / `agentstudio_base_url` in
21-
* `~/.bailian/config.json` (via `bl auth login`); mirror them onto
22-
* `DASHSCOPE_API_KEY` / `BAILIAN_BASE_URL` so users don't have to re-declare the
23-
* same credentials for `bl managed-agent *`. `workspace_id` is still bridged for configs
24-
* that predate the base_url flow (the SDK accepts either).
58+
* Load the SDK's env-based credential sources (`.env`, `~/.agents/config.json`)
59+
* for non-bailian providers, then placeholder every credential var that is still
60+
* unset with "" so agents.yaml `${VAR}` interpolation never throws on a value the
61+
* pipeline is about to supply (bailian) or authoritatively reject ({@link
62+
* assertProviderCredentials}). Runs every call (no I/O cache) so a scrubbed
63+
* environment is repopulated if the same process resolves more than one config.
64+
*/
65+
export function prepareProviderEnv(): void {
66+
bootstrapRuntimeCredentialsSync();
67+
for (const key of CREDENTIAL_ENV_KEYS) {
68+
if (process.env[key] === undefined) process.env[key] = "";
69+
}
70+
}
71+
72+
/**
73+
* Override the bailian provider block with bl's authStage-resolved credential, so
74+
* the bailian API key is authoritatively the CLI auth chain's — never a config
75+
* file bare-read or a stale env value. `api_key` is replaced unconditionally;
76+
* `base_url` / `workspace_id` are filled only when the block references them and
77+
* the interpolated value is empty (a literal in agents.yaml is respected).
2578
*
26-
* Lowest priority: only fills a var that is still unset, so anything already in
27-
* the environment (shell export, `.env`, or `~/.agents/config.json` — which the
28-
* SDK bootstrap has already applied) wins. Missing values are left alone; the
29-
* SDK surfaces its own error when interpolation can't resolve, and non-bailian
30-
* providers (claude/qoder/ark) don't need DashScope credentials at all.
79+
* `base_url` carries {@link AGENTSTUDIO_API_PATH} because the SDK appends resource
80+
* paths onto it verbatim; a value already ending in the suffix is left as-is.
81+
* With no credential (only under --dry-run: authStage hard-gates otherwise) the
82+
* bailian block is left untouched. Non-bailian blocks keep their interpolated
83+
* (env-sourced) values.
3184
*/
32-
export function bridgeBailianCredentials(): void {
33-
const file = readConfigFile();
34-
if (!process.env.DASHSCOPE_API_KEY?.trim() && file.api_key) {
35-
process.env.DASHSCOPE_API_KEY = file.api_key;
85+
export function injectProviderCredentials(
86+
providers: Record<string, unknown>,
87+
host: CredentialHost,
88+
): void {
89+
const bailian = providers.bailian;
90+
if (!bailian || typeof bailian !== "object") return;
91+
const block = bailian as Record<string, unknown>;
92+
93+
const cred = host.client.exportApiCredential();
94+
if (cred) {
95+
block.api_key = cred.token;
96+
if ("base_url" in block && !block.base_url) {
97+
block.base_url = cred.baseUrl.endsWith(AGENTSTUDIO_API_PATH)
98+
? cred.baseUrl
99+
: `${cred.baseUrl}${AGENTSTUDIO_API_PATH}`;
100+
}
36101
}
37-
if (!process.env.BAILIAN_BASE_URL?.trim() && file.agentstudio_base_url) {
38-
process.env.BAILIAN_BASE_URL = file.agentstudio_base_url;
102+
if ("workspace_id" in block && !block.workspace_id && host.settings.workspaceId) {
103+
block.workspace_id = host.settings.workspaceId;
39104
}
40-
if (!process.env.BAILIAN_WORKSPACE_ID?.trim() && file.workspace_id) {
41-
process.env.BAILIAN_WORKSPACE_ID = file.workspace_id;
105+
}
106+
107+
/**
108+
* Remove every credential var from `process.env` after interpolation has run and
109+
* bailian has been overridden in-memory. From here on the real credentials live
110+
* only in the config object (and, after `createProjectRuntime`, in each provider
111+
* adapter instance) — nothing persists in the environment for the process
112+
* lifetime or any child process.
113+
*/
114+
export function scrubCredentialEnv(): void {
115+
for (const key of CREDENTIAL_ENV_KEYS) {
116+
delete process.env[key];
42117
}
43118
}
44119

45120
/**
46-
* Lazily load `.env` and `~/.agents/config.json` into `process.env` so the
47-
* OpenAgentPack SDK can resolve provider credentials (e.g. DASHSCOPE_API_KEY,
48-
* BAILIAN_WORKSPACE_ID), then bridge bl's own config as a fallback. Safe to call
49-
* repeatedly — only the first call does I/O.
50-
*
51-
* Effective precedence for bailian provider fields:
52-
* ~/.agents/config.json > shell env > .env > ~/.bailian/config.json
53-
*
54-
* NOTE: agent commands declare `auth: "none"` and let the SDK own credential
55-
* resolution. The bl-config bridge is a best-effort fallback; it never overrides
56-
* a value the SDK bootstrap already resolved.
121+
* After injection, fail with a CLI-authoritative AUTH error if any configured
122+
* provider's `api_key` resolved empty (missing env var, or no bl login for
123+
* bailian). Replaces the SDK's raw `Environment variable '...' is not set` /
124+
* zod config error with a clean message plus a provider-specific hint. Validates
125+
* every declared provider, so a project is only runnable once all its providers'
126+
* keys are available.
57127
*/
58-
export function ensureCredentials(): void {
59-
if (bootstrapped) return;
60-
bootstrapped = true;
61-
bootstrapRuntimeCredentialsSync();
62-
bridgeBailianCredentials();
128+
export function assertProviderCredentials(providers: Record<string, unknown>): void {
129+
for (const [name, raw] of Object.entries(providers)) {
130+
if (!raw || typeof raw !== "object") continue;
131+
const block = raw as Record<string, unknown>;
132+
if (!("api_key" in block)) continue;
133+
const apiKey = block.api_key;
134+
if (typeof apiKey === "string" && apiKey.trim()) continue;
135+
throw new BailianError(
136+
`Provider '${name}' is configured but its API key is empty.`,
137+
ExitCode.AUTH,
138+
CREDENTIAL_HINTS[name] ?? `Provide credentials for provider '${name}'.`,
139+
);
140+
}
63141
}

packages/commands/src/commands/managed-agent/apply.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const APPLY_FLAGS = {
4545

4646
export default defineCommand({
4747
description: "Apply planned changes to create/update/delete agent resources",
48-
auth: "none",
48+
auth: "apiKey",
4949
usageArgs: "[--file <path>] [--provider <name>] [--yes] [--concurrency <n>]",
5050
flags: APPLY_FLAGS,
5151
exampleArgs: ["--yes", "--provider bailian --yes"],

packages/commands/src/commands/managed-agent/destroy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const DESTROY_FLAGS = {
3030

3131
export default defineCommand({
3232
description: "Destroy all managed agent resources tracked in state",
33-
auth: "none",
33+
auth: "apiKey",
3434
usageArgs: "[--file <path>] [--yes] [--cascade]",
3535
flags: DESTROY_FLAGS,
3636
exampleArgs: ["--yes", "--yes --cascade"],

0 commit comments

Comments
 (0)