Skip to content

Commit 4c5688a

Browse files
committed
feat(managed-agent)!: restrict CLI to Bailian provider
Remove multi-provider CLI flags and reject non-Bailian provider configs while preserving SDK support.
1 parent 176961d commit 4c5688a

54 files changed

Lines changed: 615 additions & 810 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/agents/auth-change.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,19 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
6262
`bl managed-agent *` 按调用链分两层:
6363

6464
- **离线命令**`init``validate``state list/show/rm`:`auth: "none"`,只读写本地文件,无需登录;引擎侧传 `credentials: "none"` 跳过凭证断言
65-
- **联网命令**`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` 仍需登录。
65+
- **联网命令**`plan``apply``destroy``state import``skill-list`、全部 `session *`:统一声明 `auth: "apiKey"` 硬门禁,authStage 经 `resolveApiKey(sources)` 解析 Bailian 凭证(flag > env > active profile config),缺失报统一 AUTH;引擎层再断言 Bailian key 非空。例外:`plan --no-refresh` / `plan --dry-run``credentials: "none"` 并强制 `refresh: false`(不联网、不回写 state,不查 provider key),其中 `--dry-run` 连登录也不要求(authStage 的 dry-run 豁免),`--no-refresh` 仍需登录。
66+
67+
`bl managed-agent` 是 Bailian-only 产品入口:命令不暴露 `--provider``init` 只生成 `providers.bailian`,所有远端调用固定传 `provider: "bailian"``resolveAgentProjectConfig` 在创建 SDK runtime 前通过 `assertBailianOnlyProviders` 拒绝包含非 Bailian Provider 的手写配置;共享 `@openagentpack/sdk` 仍可保留多 Provider 能力。
6668

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

69-
1. `prepareProviderEnv()``bootstrapRuntimeCredentialsSync()`(SDK `.env` / `~/.agents/config.json` 灌进 env,服务 claude/ark/qoder 等非 bailian provider),再把全部凭证类 env(`CREDENTIAL_ENV_KEYS`含别名)中仍为 undefined 的占位为 `""`,使 agents.yaml 插值不因缺变量抛错
70-
2. `resolveProjectConfig`插值发生:bailian 插值拿到占位空串,claude/ark 拿到真实 env 值;随后 `normalizeInterpolatedProviderBlocks()` 把插值为空导致的 YAML `null` 归一为 `""`(避免离线命令下空 key 在 SDK zod 层报 "received null")
71+
1. `prepareProviderEnv()`调用 SDK 的凭证 bootstrap,再把凭证类 env(`CREDENTIAL_ENV_KEYS`含兼容别名)中仍为 undefined 的项占位为 `""`,使 agents.yaml 插值阶段能够完成并由 CLI 输出明确的 Bailian-only 配置错误
72+
2. `resolveProjectConfig`完成插值;随后 `normalizeInterpolatedProviderBlocks()` 把插值为空导致的 YAML `null` 归一为 `""`,避免空 key 在 SDK zod 层提前报 "received null"
7173
3. `injectProviderCredentials()` — 用 `ctx.client.exportApiCredential()`(lint 限定 `managed-agent/_engine/**` 可用)覆写内存 config 对象的 bailian 块:有凭证时 `api_key` 无条件覆写;`base_url`(拼 `/api/v1/agentstudio` 后缀,无凭证时用 client 默认域名补齐以满足 schema)/`workspace_id`(取 `settings.workspaceId`)仅在引用且为空时填充
7274
4. `scrubCredentialEnv()` — 从 `process.env` 删除全部凭证变量(真实凭证此后只存于 config 对象 → provider adapter 实例内存,不驻留 env / 不被子进程继承)
73-
5. `assertProviderCredentials(providers)`任一已声明 provider 的 `api_key` 为空 → CLI 权威 `AUTH` 错误 + provider 专属 hint(取代 SDK 原始插值/zod 报错);离线命令传 `credentials: "none"` 整体跳过
75+
5. `assertBailianOnlyProviders(providers)`拒绝非 Bailian Provider;随后 `assertProviderCredentials(providers)` 在 Bailian `api_key` 为空时给出 CLI 权威 `AUTH` 错误和登录 hint离线命令传 `credentials: "none"` 跳过 key 断言,但仍执行 Bailian-only 配置校验
7476

75-
`bl auth login` 仅管理 bailian(DashScope)凭证;claude/ark/qoder 的 key 从 env(shell / `.env` / `~/.agents/config.json`)经插值进入 config 对象,同样被清扫。禁止命令层直接 `readConfigFile` 裸读凭证;bailian 字段以 CLI 鉴权链为唯一信源。
77+
禁止命令层直接 `readConfigFile` 裸读凭证;Bailian 字段以 CLI 鉴权链为唯一信源。SDK bootstrap 期间读取到的兼容凭证变量也会在配置解析后统一清扫
7678

7779
## 必查清单
7880

packages/commands/src/commands/managed-agent/_engine/api-helpers.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@ export const API_TARGET_FLAGS = {
1111
"zh-CN": "配置文件路径(默认:agents.yaml)",
1212
},
1313
},
14-
provider: {
15-
type: "string",
16-
valueHint: "<name>",
17-
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
18-
},
1914
} satisfies FlagsDef;
2015

2116
export const CURSOR_FLAGS = {

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

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import {
33
type LoadedProjectConfig,
44
type ProjectRuntimeContext,
55
resolveProjectConfig,
6-
UserError,
76
} from "@openagentpack/sdk";
7+
import { BailianError, ExitCode } from "bailian-cli-core";
88
import {
99
assertProviderCredentials,
1010
type CredentialHost,
@@ -20,12 +20,14 @@ export { CREDENTIALS_NOTE, OFFLINE_NOTE } from "./credentials.ts";
2020

2121
/**
2222
* 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
23+
* - "all" (default) — online command: the Bailian provider must have a
24+
* non-empty key after injection
2525
* - "none" — offline command (local config/state only), skip the check
2626
*/
2727
export type CredentialScope = "all" | "none";
2828

29+
export const BAILIAN_PROVIDER = "bailian";
30+
2931
interface AgentConfigOptions {
3032
resolveEnv?: boolean;
3133
projectName?: string;
@@ -42,7 +44,8 @@ interface AgentConfigOptions {
4244
* 3. override the bailian block with the CLI auth chain's credential (in-memory)
4345
* 4. scrub all credential vars from process.env (real values now live only in
4446
* the config object → provider adapters, never the environment)
45-
* 5. fail with a CLI-authoritative AUTH error if any provider's key is empty
47+
* 5. enforce Bailian-only config and fail with a CLI-authoritative AUTH error
48+
* if its key is empty
4649
* (offline commands pass `credentials: "none"` to skip the check)
4750
*/
4851
export async function resolveAgentProjectConfig(
@@ -55,6 +58,7 @@ export async function resolveAgentProjectConfig(
5558
normalizeInterpolatedProviderBlocks(resolved.config.providers);
5659
injectProviderCredentials(resolved.config.providers, host);
5760
scrubCredentialEnv();
61+
assertBailianOnlyProviders(resolved.config.providers);
5862
if ((options.credentials ?? "all") !== "none") {
5963
assertProviderCredentials(resolved.config.providers);
6064
}
@@ -91,15 +95,23 @@ export async function buildAgentRuntime(
9195
return { ...ctx, configPath };
9296
}
9397

94-
/** Ensure a user-supplied --provider value is actually configured in agents.yaml. */
95-
export function assertProviderConfigured(
96-
ctx: ProjectRuntimeContext,
97-
provider: string | undefined,
98-
): void {
99-
if (!provider || provider === "all") return;
100-
if (ctx.providers.has(provider)) return;
101-
const available = Array.from(ctx.providers.keys()).join(", ") || "none";
102-
throw new UserError(
103-
`Provider '${provider}' is not configured. Available providers: ${available}.`,
98+
/** Enforce the bl product boundary while the shared SDK remains multi-provider capable. */
99+
export function assertBailianOnlyProviders(providers: Record<string, unknown>): void {
100+
const configuredProviders = Object.keys(providers);
101+
const unsupportedProviders = configuredProviders.filter(
102+
(provider) => provider !== BAILIAN_PROVIDER,
104103
);
104+
if (unsupportedProviders.length > 0) {
105+
const names = unsupportedProviders.join(", ");
106+
throw new BailianError(
107+
`bl managed-agent only supports provider '${BAILIAN_PROVIDER}'; remove unsupported providers: ${names}. / bl managed-agent 仅支持 Provider '${BAILIAN_PROVIDER}';请移除不支持的 Provider:${names}。`,
108+
ExitCode.USAGE,
109+
);
110+
}
111+
if (!(BAILIAN_PROVIDER in providers)) {
112+
throw new BailianError(
113+
`bl managed-agent requires a '${BAILIAN_PROVIDER}' provider configuration. / bl managed-agent 需要配置 Provider '${BAILIAN_PROVIDER}'。`,
114+
ExitCode.USAGE,
115+
);
116+
}
105117
}

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

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ const CREDENTIAL_HINTS: Record<string, string> = {
3434
qoder: "Set QODER_PAT (or QODER_API_KEY) in your shell or .env.",
3535
};
3636

37+
const BAILIAN_ONLY_NOTE = {
38+
"en-US":
39+
"bl managed-agent supports the Bailian provider only; configurations containing other providers are rejected.",
40+
"zh-CN": "bl managed-agent 仅支持百炼 Provider;包含其他 Provider 的配置会被拒绝。",
41+
} as const;
42+
3743
/** The slice of CommandContext the credential pipeline needs: authStage-resolved client + settings. */
3844
export interface CredentialHost {
3945
client: Client;
@@ -43,24 +49,18 @@ export interface CredentialHost {
4349
/**
4450
* Shared `--help` note documenting where agent commands get provider
4551
* 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.
52+
* `auth: "apiKey"`). The resolved credential is injected into the SDK in-memory
53+
* and scrubbed from the environment. Attach to every command that loads
54+
* agents.yaml. `bl` prefix is safe: agent commands ship on `bl` only.
5055
*/
5156
export const CREDENTIALS_NOTE = [
57+
BAILIAN_ONLY_NOTE,
5258
{
5359
"en-US":
5460
"Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).",
5561
"zh-CN":
5662
"百炼凭证来自 bl 鉴权链:--api-key > DASHSCOPE_API_KEY > `bl auth login`(当前激活的配置 Profile)。",
5763
},
58-
{
59-
"en-US":
60-
"Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.",
61-
"zh-CN":
62-
"其他 Provider 读取 agents.yaml 中引用的环境变量(例如 ${ANTHROPIC_API_KEY}),包括 .env 和 ~/.agents/config.json。",
63-
},
6464
{
6565
"en-US":
6666
"Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.",
@@ -73,6 +73,7 @@ export const CREDENTIALS_NOTE = [
7373
* agents.yaml / local state only, so no login or provider key is required.
7474
*/
7575
export const OFFLINE_NOTE = [
76+
BAILIAN_ONLY_NOTE,
7677
{
7778
"en-US": "Runs fully offline against local files: no login or provider credentials required.",
7879
"zh-CN": "完全离线处理本地文件:无需登录或提供 Provider 凭证。",
@@ -166,13 +167,9 @@ export function normalizeInterpolatedProviderBlocks(providers: Record<string, un
166167
}
167168

168169
/**
169-
* After injection, fail with a CLI-authoritative AUTH error if any configured
170-
* provider's `api_key` resolved empty (missing env var, or no bl login for
171-
* bailian). Replaces the SDK's raw `Environment variable '...' is not set` /
172-
* zod config error with a clean message plus a provider-specific hint. Validates
173-
* every declared provider by default. A scoped operation may pass its exact
174-
* provider list so unrelated credentials cannot block it; offline commands skip
175-
* the check entirely.
170+
* After injection, fail with a CLI-authoritative AUTH error if Bailian's
171+
* `api_key` resolved empty. Replaces the SDK's raw config error with a clean
172+
* message and hint. Offline commands skip the check entirely.
176173
*/
177174
export function assertProviderCredentials(
178175
providers: Record<string, unknown>,

packages/commands/src/commands/managed-agent/_engine/scoped-create.ts

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ export function selectResourceKey(options: {
125125
export async function loadScopedCreateProject(
126126
host: ScopedCreateHost,
127127
file: string,
128-
requestedProvider: string | undefined,
129128
): Promise<LoadedScopedCreateProject> {
130129
const sourceBeforeLoad = await readFile(resolve(file), "utf8").catch((error) => {
131130
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
@@ -148,7 +147,7 @@ export async function loadScopedCreateProject(
148147
"Re-run the command against the latest file.",
149148
);
150149
}
151-
const provider = resolveTargetProvider(loaded.config, requestedProvider);
150+
const provider = "bailian";
152151
if (!host.settings.dryRun) assertProviderCredentials(loaded.config.providers, [provider]);
153152
installSdkTransport(host);
154153
const stateBackend = new LocalFileStateBackend({ configPath: loaded.configPath });
@@ -161,34 +160,6 @@ export async function loadScopedCreateProject(
161160
};
162161
}
163162

164-
export function resolveTargetProvider(
165-
config: ResolvedProjectConfig,
166-
requested: string | undefined,
167-
): string {
168-
if (requested === "all") {
169-
throw new BailianError(
170-
"--provider all is not valid for a single-resource create.",
171-
ExitCode.USAGE,
172-
);
173-
}
174-
if (requested) {
175-
if (requested in config.providers) return requested;
176-
throw new BailianError(
177-
`Provider '${requested}' is not configured in agents.yaml.`,
178-
ExitCode.USAGE,
179-
);
180-
}
181-
const defaultProvider = config.defaults?.provider;
182-
if (defaultProvider && defaultProvider !== "all") return defaultProvider;
183-
const configuredProviders = Object.keys(config.providers);
184-
if (configuredProviders.length === 1) return configuredProviders[0]!;
185-
throw new BailianError(
186-
"Cannot infer one target provider for this create command.",
187-
ExitCode.USAGE,
188-
"Pass --provider <name> when defaults.provider is 'all' or multiple providers are configured.",
189-
);
190-
}
191-
192163
export async function resolveCandidateDeclaration(options: {
193164
project: LoadedScopedCreateProject;
194165
group: ScopedCreateGroup;

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,6 @@ const CREATE_FLAGS = {
6666
valueHint: "<text>",
6767
description: { "en-US": "Agent description", "zh-CN": "Agent 描述" },
6868
},
69-
provider: {
70-
type: "string",
71-
valueHint: "<name>",
72-
description: {
73-
"en-US": "Target provider; inferred when the config has one effective provider",
74-
"zh-CN": "目标 Provider;配置只有一个有效 Provider 时自动推断",
75-
},
76-
},
7769
skill: {
7870
type: "array",
7971
valueHint: "<id>",
@@ -344,7 +336,7 @@ export default defineCommand({
344336
},
345337
auth: "apiKey",
346338
usageArgs:
347-
"--name <name> --model <model> --instructions <text|path> [--description <text>] [--provider <name>] [--skill <id>...] [--type custom|official] [--skill-dir <path>...] [--tool <name>...] [--file <path>] [--yes]",
339+
"--name <name> --model <model> --instructions <text|path> [--description <text>] [--skill <id>...] [--type custom|official] [--skill-dir <path>...] [--tool <name>...] [--file <path>] [--yes]",
348340
flags: CREATE_FLAGS,
349341
exampleArgs: [
350342
'--name assistant --model qwen3.8-max --instructions "You are helpful."',
@@ -390,7 +382,7 @@ export default defineCommand({
390382
const { flags, settings } = ctx;
391383
const format = detectOutputFormat(settings.output);
392384
const file = flags.file ?? "agents.yaml";
393-
const project = await loadScopedCreateProject(ctx, file, flags.provider);
385+
const project = await loadScopedCreateProject(ctx, file);
394386
const provider = project.provider;
395387
const externalSkillRefs = buildAgentSkillRefs(flags.skill, agentSkillType(flags.type));
396388
const localSkills = await prepareLocalSkillSources(project, flags.skillDir);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default defineCommand({
2525
withStdoutProtected(async () => {
2626
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
2727
return getRemoteAgent(runtime, ctx.flags.agentId, {
28-
provider: ctx.flags.provider,
28+
provider: "bailian",
2929
version: ctx.flags.agentVersion,
3030
});
3131
}),

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export default defineCommand({
2323
return fetchAllPages(
2424
async (page) => {
2525
const response = await listRemoteAgents(runtime, {
26-
provider: ctx.flags.provider,
26+
provider: "bailian",
2727
limit: ctx.flags.limit,
2828
page,
2929
include_archived: ctx.flags.includeArchived,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default defineCommand({
2727
return searchCursorPages(
2828
async (page) => {
2929
const response = await listRemoteAgents(runtime, {
30-
provider: ctx.flags.provider,
30+
provider: "bailian",
3131
limit: ctx.flags.limit ?? 100,
3232
page,
3333
include_archived: ctx.flags.includeArchived,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export default defineCommand({
2323
return fetchAllPages(
2424
async (page) => {
2525
const response = await listRemoteAgentVersions(runtime, ctx.flags.agentId, {
26-
provider: ctx.flags.provider,
26+
provider: "bailian",
2727
limit: ctx.flags.limit,
2828
page,
2929
});

0 commit comments

Comments
 (0)