Skip to content

Commit 9819eb6

Browse files
committed
feat(agent): 非bailian provider也走鉴权逻辑
1 parent a03ee0c commit 9819eb6

22 files changed

Lines changed: 91 additions & 228 deletions

docs/agents/auth-change.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,18 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
5555

5656
### 例外:agent 命令的分层鉴权与 SDK 凭证内存注入
5757

58-
`bl managed-agent *` 按调用链分两层,不再全命令硬门禁:
58+
`bl managed-agent *` 按调用链分两层:
5959

60-
- **离线命令**`init``validate``state list/show/rm`:`auth: "none"`,只读写本地文件,无需登录;引擎侧传 `credentials: "none"` 跳过凭证断言`plan --no-refresh``plan --dry-run` 同样传 `"none"` 并强制 `refresh: false`:不联网、不回写 state)
61-
- **provider-aware 命令**`plan`(默认)`apply``destroy``state import``skill-list`、全部 `session *`:仍声明 `auth: "apiKey"` 但加 `authOptional: true` —— authStage 照常经 `resolveApiKey(sources)` 解析 bailian 凭证(flag > env > active profile config)并注入 `ctx.client`,但缺失不在 authStage 抛;真正的门禁在引擎层 `assertProviderCredentials`,只校验本次运行涉及的 provider`CredentialScope`:`--provider` / state 地址里的 provider / 配置默认 provider 链)。配了四个 provider 只跑 claude 时,缺 bailian key 不阻塞
60+
- **离线命令**`init``validate``state list/show/rm`:`auth: "none"`,只读写本地文件,无需登录;引擎侧传 `credentials: "none"` 跳过凭证断言
61+
- **联网命令**`plan``apply``destroy``state import``skill-list`、全部 `session *`:统一声明 `auth: "apiKey"` 硬门禁 —— 无论目标 provider 是谁,authStage 都经 `resolveApiKey(sources)` 解析 bailian 凭证(flag > env > active profile config),缺失报统一 AUTH;引擎层 `assertProviderCredentials` 再对 agents.yaml 里**全部已声明 provider** 的空 key 拦截并给 provider 专属 hint。例外:`plan --no-refresh` / `plan --dry-run``credentials: "none"` 并强制 `refresh: false`(不联网、不回写 state,不查 provider key),其中 `--dry-run` 连登录也不要求(authStage 的 dry-run 豁免),`--no-refresh` 仍需登录
6262

6363
凭证不以真实值写入 `process.env`,而是经 `packages/commands/src/commands/managed-agent/_engine/`**内存注入管道**(`resolveAgentProjectConfig`)注入 SDK,管道五步:
6464

6565
1. `prepareProviderEnv()` — 先 `bootstrapRuntimeCredentialsSync()`(SDK 把 `.env` / `~/.agents/config.json` 灌进 env,服务 claude/ark/qoder 等非 bailian provider),再把全部凭证类 env(`CREDENTIAL_ENV_KEYS`,含别名)中仍为 undefined 的占位为 `""`,使 agents.yaml 插值不因缺变量抛错
66-
2. `resolveProjectConfig` — 插值发生:bailian 插值拿到占位空串,claude/ark 拿到真实 env 值;随后 `normalizeInterpolatedProviderBlocks()` 把插值为空导致的 YAML `null` 归一为 `""`(避免范围外 provider 在 SDK zod 层报 "received null")
66+
2. `resolveProjectConfig` — 插值发生:bailian 插值拿到占位空串,claude/ark 拿到真实 env 值;随后 `normalizeInterpolatedProviderBlocks()` 把插值为空导致的 YAML `null` 归一为 `""`(避免离线命令下空 key 在 SDK zod 层报 "received null")
6767
3. `injectProviderCredentials()` — 用 `ctx.client.exportApiCredential()`(lint 限定 `managed-agent/_engine/**` 可用)覆写内存 config 对象的 bailian 块:有凭证时 `api_key` 无条件覆写;`base_url`(拼 `/api/v1/agentstudio` 后缀,无凭证时用 client 默认域名补齐以满足 schema)/`workspace_id`(取 `settings.workspaceId`)仅在引用且为空时填充
6868
4. `scrubCredentialEnv()` — 从 `process.env` 删除全部凭证变量(真实凭证此后只存于 config 对象 → provider adapter 实例内存,不驻留 env / 不被子进程继承)
69-
5. `assertProviderCredentials(providers, required)``CredentialScope` 算出的 `required` 范围校验:范围内 provider 的 `api_key` 为空 → CLI 权威 `AUTH` 错误 + provider 专属 hint(取代 SDK 原始插值/zod 报错);范围外 provider 允许空 key
69+
5. `assertProviderCredentials(providers)`任一已声明 provider 的 `api_key` 为空 → CLI 权威 `AUTH` 错误 + provider 专属 hint(取代 SDK 原始插值/zod 报错);离线命令传 `credentials: "none"` 整体跳过
7070

7171
`bl auth login` 仅管理 bailian(DashScope)凭证;claude/ark/qoder 的 key 从 env(shell / `.env` / `~/.agents/config.json`)经插值进入 config 对象,同样被清扫。禁止命令层直接 `readConfigFile` 裸读凭证;bailian 字段以 CLI 鉴权链为唯一信源。
7272

packages/cli/agents.yaml

Lines changed: 0 additions & 27 deletions
This file was deleted.

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

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
injectProviderCredentials,
1212
normalizeInterpolatedProviderBlocks,
1313
prepareProviderEnv,
14-
resolveTargetProviderNames,
1514
scrubCredentialEnv,
1615
} from "./credentials.ts";
1716
import { loadFileState } from "./file-state-manager.ts";
@@ -20,15 +19,12 @@ import { type HostContext, installSdkTransport } from "./transport.ts";
2019
export { CREDENTIALS_NOTE, OFFLINE_NOTE } from "./credentials.ts";
2120

2221
/**
23-
* Which providers this run requires a non-empty key for:
24-
* - "targets" (default) — the run's target providers per the config's
25-
* default provider chain (mirrors the SDK's plan/apply targeting)
22+
* Whether this run requires provider keys:
23+
* - "all" (default) — online command: every provider declared in agents.yaml
24+
* must have a non-empty key after injection
2625
* - "none" — offline command (local config/state only), skip the check
27-
* - "all" — every configured provider (`--provider all`)
28-
* - any other name — the run was narrowed to that provider
29-
* (`--provider <name>` / a provider-qualified state address)
3026
*/
31-
export type CredentialScope = "targets" | "none" | "all" | (string & {});
27+
export type CredentialScope = "all" | "none";
3228

3329
interface AgentConfigOptions {
3430
resolveEnv?: boolean;
@@ -46,8 +42,8 @@ interface AgentConfigOptions {
4642
* 3. override the bailian block with the CLI auth chain's credential (in-memory)
4743
* 4. scrub all credential vars from process.env (real values now live only in
4844
* the config object → provider adapters, never the environment)
49-
* 5. fail with a CLI-authoritative AUTH error if a provider within this run's
50-
* {@link CredentialScope} has an empty key (offline commands pass "none")
45+
* 5. fail with a CLI-authoritative AUTH error if any provider's key is empty
46+
* (offline commands pass `credentials: "none"` to skip the check)
5147
*/
5248
export async function resolveAgentProjectConfig(
5349
host: CredentialHost,
@@ -59,16 +55,8 @@ export async function resolveAgentProjectConfig(
5955
normalizeInterpolatedProviderBlocks(resolved.config.providers);
6056
injectProviderCredentials(resolved.config.providers, host);
6157
scrubCredentialEnv();
62-
const scope = options.credentials ?? "targets";
63-
if (scope !== "none") {
64-
assertProviderCredentials(
65-
resolved.config.providers,
66-
scope === "targets"
67-
? resolveTargetProviderNames(resolved.config)
68-
: scope === "all"
69-
? Object.keys(resolved.config.providers)
70-
: [scope],
71-
);
58+
if ((options.credentials ?? "all") !== "none") {
59+
assertProviderCredentials(resolved.config.providers);
7260
}
7361
return resolved;
7462
}

packages/commands/src/commands/managed-agent/_engine/credentials.ts

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ export interface CredentialHost {
5151
export const CREDENTIALS_NOTE = [
5252
"Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).",
5353
"Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.",
54-
"Only the providers this run involves (--provider, or the config's default provider chain) need credentials; other configured providers are not checked.",
5554
"Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.",
5655
];
5756

@@ -89,10 +88,10 @@ export function prepareProviderEnv(): void {
8988
* `base_url` carries {@link AGENTSTUDIO_API_PATH} because the SDK appends resource
9089
* paths onto it verbatim; a value already ending in the suffix is left as-is.
9190
* It is filled even without a credential — `client.baseUrl` is readable
92-
* credential-less (defaults to the CLI's model-domain base URL) — so offline /
93-
* out-of-scope runs still satisfy the SDK's "workspace_id or base_url" schema.
94-
* With no credential the `api_key` is left untouched: an in-scope empty key is
95-
* rejected by {@link assertProviderCredentials}, out-of-scope ones may stay empty.
91+
* credential-less (defaults to the CLI's model-domain base URL) — so offline
92+
* commands (which skip the credential assert) still satisfy the SDK's
93+
* "workspace_id or base_url" schema. With no credential the `api_key` is left
94+
* untouched: online commands reject it via {@link assertProviderCredentials}.
9695
*/
9796
export function injectProviderCredentials(
9897
providers: Record<string, unknown>,
@@ -133,10 +132,11 @@ export function scrubCredentialEnv(): void {
133132
/**
134133
* The SDK interpolates `${VAR}` into the raw YAML text, so an empty env var
135134
* leaves `api_key:` with nothing after it — YAML parses that as null. Normalize
136-
* every null provider field back to "" so the pipeline stays uniform: an empty
137-
* api_key is caught by {@link assertProviderCredentials} when the provider is
138-
* in scope, and out-of-scope blocks still satisfy the SDK's string schemas
139-
* instead of failing zod with "received null" before the run even starts.
135+
* every null provider field back to "" so the pipeline stays uniform: for
136+
* online commands an empty api_key is caught by {@link
137+
* assertProviderCredentials}; for offline commands (which skip the assert) the
138+
* blocks still satisfy the SDK's string schemas instead of failing zod with
139+
* "received null" before the run even starts.
140140
*/
141141
export function normalizeInterpolatedProviderBlocks(providers: Record<string, unknown>): void {
142142
for (const raw of Object.values(providers)) {
@@ -149,37 +149,15 @@ export function normalizeInterpolatedProviderBlocks(providers: Record<string, un
149149
}
150150

151151
/**
152-
* The providers a run targets when no explicit `--provider` narrows it: the
153-
* config's default provider, or every configured provider when the default is
154-
* absent or "all". Mirrors the SDK's config-based `resolveTargetProviders`
155-
* (not exported from the SDK's public surface).
156-
*/
157-
export function resolveTargetProviderNames(config: {
158-
providers: Record<string, unknown>;
159-
defaults?: { provider?: string };
160-
}): string[] {
161-
const defaultProvider = config.defaults?.provider;
162-
if (!defaultProvider || defaultProvider === "all") return Object.keys(config.providers);
163-
return [defaultProvider];
164-
}
165-
166-
/**
167-
* After injection, fail with a CLI-authoritative AUTH error if a required
152+
* After injection, fail with a CLI-authoritative AUTH error if any configured
168153
* provider's `api_key` resolved empty (missing env var, or no bl login for
169154
* bailian). Replaces the SDK's raw `Environment variable '...' is not set` /
170-
* zod config error with a clean message plus a provider-specific hint.
171-
* `required` limits the check to the providers this run actually involves
172-
* (← --provider / state address / config default chain); providers outside
173-
* that scope may keep empty keys — a project stays runnable per provider.
174-
* Names without a matching config block are skipped: "provider not
175-
* configured" is the engine's error to raise, not a credential problem.
155+
* zod config error with a clean message plus a provider-specific hint. Validates
156+
* every declared provider, so a project is only runnable once all its providers'
157+
* keys are available; offline commands skip the check entirely.
176158
*/
177-
export function assertProviderCredentials(
178-
providers: Record<string, unknown>,
179-
required?: readonly string[],
180-
): void {
181-
for (const name of required ?? Object.keys(providers)) {
182-
const raw = providers[name];
159+
export function assertProviderCredentials(providers: Record<string, unknown>): void {
160+
for (const [name, raw] of Object.entries(providers)) {
183161
if (!raw || typeof raw !== "object") continue;
184162
const block = raw as Record<string, unknown>;
185163
if (!("api_key" in block)) continue;

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,6 @@ const APPLY_FLAGS = {
4646
export default defineCommand({
4747
description: "Apply planned changes to create/update/delete agent resources",
4848
auth: "apiKey",
49-
// Provider-aware gate: only the providers this apply targets need credentials.
50-
authOptional: true,
5149
usageArgs: "[--file <path>] [--provider <name>] [--yes] [--concurrency <n>]",
5250
flags: APPLY_FLAGS,
5351
exampleArgs: ["--yes", "--provider bailian --yes"],
@@ -75,9 +73,7 @@ export default defineCommand({
7573

7674
const planned = await withAgentErrors(() =>
7775
withStdoutProtected(async () => {
78-
const runtime = await buildAgentRuntime(ctx, file, {
79-
credentials: flags.provider ?? "targets",
80-
});
76+
const runtime = await buildAgentRuntime(ctx, file);
8177
assertProviderConfigured(runtime, flags.provider);
8278
return planProjectContext(runtime, {
8379
provider: flags.provider,

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ const DESTROY_FLAGS = {
3131
export default defineCommand({
3232
description: "Destroy all managed agent resources tracked in state",
3333
auth: "apiKey",
34-
// Provider-aware gate: only the run's target providers need credentials.
35-
authOptional: true,
3634
usageArgs: "[--file <path>] [--yes] [--cascade]",
3735
flags: DESTROY_FLAGS,
3836
exampleArgs: ["--yes", "--yes --cascade"],
@@ -56,9 +54,7 @@ export default defineCommand({
5654

5755
const planned = await withAgentErrors(() =>
5856
withStdoutProtected(async () => {
59-
const runtime = await buildAgentRuntime(ctx, file, {
60-
credentials: "targets",
61-
});
57+
const runtime = await buildAgentRuntime(ctx, file);
6258
return planDestroyProjectContext(runtime);
6359
}),
6460
);

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,29 +41,27 @@ const PLAN_FLAGS = {
4141
export default defineCommand({
4242
description: "Show what changes would be applied to agent infrastructure",
4343
auth: "apiKey",
44-
// Provider-aware gate: --no-refresh / --dry-run plan fully offline; a
45-
// refreshing run only needs credentials for the providers it targets
46-
// (see CredentialScope).
47-
authOptional: true,
4844
usageArgs: "[--file <path>] [--provider <name>] [--no-refresh] [--refresh-only]",
4945
flags: PLAN_FLAGS,
5046
exampleArgs: ["", "--provider bailian", "--no-refresh"],
5147
notes: [
5248
...CREDENTIALS_NOTE,
53-
"--no-refresh and --dry-run plan offline from local config and state: no credentials, no remote requests, no state writes.",
49+
"--no-refresh and --dry-run plan offline from local config and state: no remote requests, no state writes, provider keys are not checked.",
5450
],
5551
async run(ctx) {
5652
const { settings, flags } = ctx;
5753
const format = detectOutputFormat(settings.output);
5854
const file = flags.file ?? "agents.yaml";
5955
// Offline mode never talks to a provider and never saves refreshed state:
6056
// --no-refresh by explicit request, --dry-run by contract (read-only run).
57+
// Provider keys are skipped then; the bl login gate (auth: "apiKey") still
58+
// applies except under --dry-run (authStage's dry-run exemption).
6159
const offline = Boolean(flags.noRefresh) || settings.dryRun;
6260

6361
const planned = await withAgentErrors(() =>
6462
withStdoutProtected(async () => {
6563
const runtime = await buildAgentRuntime(ctx, file, {
66-
credentials: offline ? "none" : (flags.provider ?? "targets"),
64+
credentials: offline ? "none" : "all",
6765
});
6866
assertProviderConfigured(runtime, flags.provider);
6967
return planProjectContext(runtime, {

packages/commands/src/commands/managed-agent/session-create.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ const SESSION_CREATE_FLAGS = {
4343
export default defineCommand({
4444
description: "Create a new session for an agent",
4545
auth: "apiKey",
46-
// Provider-aware gate: only the session's provider needs credentials.
47-
authOptional: true,
4846
usageArgs: "[--agent <name>] [--environment <name>] [--title <title>] [--file <path>]",
4947
flags: SESSION_CREATE_FLAGS,
5048
exampleArgs: ["", "--agent assistant", "--agent assistant --title 'debug run'"],
@@ -74,9 +72,7 @@ export default defineCommand({
7472

7573
const run = await withAgentErrors(() =>
7674
withStdoutProtected(async () => {
77-
const runtime = await buildAgentRuntime(ctx, file, {
78-
credentials: flags.provider ?? "targets",
79-
});
75+
const runtime = await buildAgentRuntime(ctx, file);
8076
return createSessionForAgent(runtime, {
8177
agent: flags.agent,
8278
provider: flags.provider,

packages/commands/src/commands/managed-agent/session-delete.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ const SESSION_DELETE_FLAGS = {
2727
export default defineCommand({
2828
description: "Delete a session",
2929
auth: "apiKey",
30-
// Provider-aware gate: only the session's provider needs credentials.
31-
authOptional: true,
3230
usageArgs: "--session-id <id> [--provider <name>] [--file <path>]",
3331
flags: SESSION_DELETE_FLAGS,
3432
exampleArgs: ["--session-id sess_abc123"],
@@ -52,9 +50,7 @@ export default defineCommand({
5250

5351
await withAgentErrors(() =>
5452
withStdoutProtected(async () => {
55-
const runtime = await buildAgentRuntime(ctx, file, {
56-
credentials: flags.provider ?? "targets",
57-
});
53+
const runtime = await buildAgentRuntime(ctx, file);
5854
await deleteSession(runtime, flags.sessionId, flags.provider);
5955
}),
6056
);

packages/commands/src/commands/managed-agent/session-events.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ const SESSION_EVENTS_FLAGS = {
3838
export default defineCommand({
3939
description: "List event history for a session",
4040
auth: "apiKey",
41-
// Provider-aware gate: only the session's provider needs credentials.
42-
authOptional: true,
4341
usageArgs: "--session-id <id> [--limit <n>] [--all] [--file <path>]",
4442
flags: SESSION_EVENTS_FLAGS,
4543
exampleArgs: ["--session-id sess_abc123", "--session-id sess_abc123 --all"],
@@ -51,9 +49,7 @@ export default defineCommand({
5149

5250
const { items: events, hasMore } = await withAgentErrors(() =>
5351
withStdoutProtected(async () => {
54-
const runtime = await buildAgentRuntime(ctx, file, {
55-
credentials: flags.provider ?? "targets",
56-
});
52+
const runtime = await buildAgentRuntime(ctx, file);
5753
return fetchAllPages(async (page) => {
5854
const result = await listSessionEvents(runtime, flags.sessionId, {
5955
provider: flags.provider,

0 commit comments

Comments
 (0)