feat(agent): 鉴权分离线命令和在线命令,仅对bailian provider鉴权

This commit is contained in:
chenanran555
2026-07-27 19:51:56 +08:00
parent 6f9e006fef
commit 93c9149e45
28 changed files with 424 additions and 128 deletions
+12 -8
View File
@@ -53,18 +53,22 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
命令不要直接解析 token、env 或 config。业务请求统一走 `ctx.client`;登录/配置命令通过 `ctx.authStore` / `ctx.configStore` 的窄接口操作落盘。
### 例外:agent 命令的 SDK 凭证内存注入
### 例外:agent 命令的分层鉴权与 SDK 凭证内存注入
`bl managed-agent *` 的全部命令声明 `auth: "apiKey"`(含纯本地脚手架 `init` —— 统一登录门槛,无例外),bailian 凭证由 authStage 经 `resolveApiKey(sources)` 权威解析(flag > env > active profile config,缺失时抛统一 AUTH 错误)。
`bl managed-agent *` 按调用链分两层,不再全命令硬门禁:
凭证不再以真实值写入 `process.env`,而是经 `packages/commands/src/commands/managed-agent/_engine/` 的**内存注入管道**(`resolveAgentProjectConfig`)注入 SDK,管道四步:
- **离线命令** — `init`、`validate`、`state list/show/rm`:`auth: "none"`,只读写本地文件,无需登录;引擎侧传 `credentials: "none"` 跳过凭证断言(`plan --no-refresh` 同样传 `"none"`)
- **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 不阻塞。
1. `prepareProviderEnv()` — 先 `bootstrapRuntimeCredentialsSync()`(SDK 把 `.env` / `~/.agents/config.json` 灌进 env,服务 claude/ark/qoder 等非 bailian provider),再把全部凭证类 env(`CREDENTIAL_ENV_KEYS`,含别名)中仍为 undefined 的占位为 `""`,使 agents.yaml 插值不因缺变量抛错
2. `resolveProjectConfig` — 插值发生:bailian 插值拿到占位空串,claude/ark 拿到真实 env 值
3. `injectProviderCredentials()` — 用 `ctx.client.exportApiCredential()`(lint 限定 `managed-agent/_engine/**` 可用)覆写内存 config 对象的 bailian 块:`api_key` 无条件覆写;`base_url`(拼 `/api/v1/agentstudio` 后缀)/`workspace_id`(取 `settings.workspaceId`)仅在引用且为空时填充
4. `scrubCredentialEnv()` + `assertProviderCredentials()` — 从 `process.env` 删除全部凭证变量(真实凭证此后只存于 config 对象 → provider adapter 实例内存,不驻留 env / 不被子进程继承);任一已声明 provider 的 `api_key` 为空 → CLI 权威 `AUTH` 错误 + provider 专属 hint(取代 SDK 原始插值/zod 报错)
凭证不以真实值写入 `process.env`,而是经 `packages/commands/src/commands/managed-agent/_engine/` 的**内存注入管道**(`resolveAgentProjectConfig`)注入 SDK,管道五步:
`bl auth login` 仅管理 bailian(DashScope)凭证;claude/ark/qoder 的 key 从 env(shell / `.env` / `~/.agents/config.json`)经插值进入 config 对象,同样被清扫。禁止命令层直接 `readConfigFile` 裸读凭证;bailian 字段以 CLI 鉴权链为唯一信源。
1. `prepareProviderEnv()` — 先 `bootstrapRuntimeCredentialsSync()`(SDK 把 `.env` / `~/.agents/config.json` 灌进 env,服务 claude/ark/qoder 等非 bailian provider),再把全部凭证类 env(`CREDENTIAL_ENV_KEYS`,含别名)中仍为 undefined 的占位为 `""`,使 agents.yaml 插值不因缺变量抛错
2. `resolveProjectConfig` — 插值发生:bailian 插值拿到占位空串,claude/ark 拿到真实 env 值;随后 `normalizeInterpolatedProviderBlocks()` 把插值为空导致的 YAML `null` 归一为 `""`(避免范围外 provider 在 SDK zod 层报 "received null")
3. `injectProviderCredentials()` — 用 `ctx.client.exportApiCredential()`(lint 限定 `managed-agent/_engine/**` 可用)覆写内存 config 对象的 bailian 块:有凭证时 `api_key` 无条件覆写;`base_url`(拼 `/api/v1/agentstudio` 后缀,无凭证时用 client 默认域名补齐以满足 schema)/`workspace_id`(取 `settings.workspaceId`)仅在引用且为空时填充
4. `scrubCredentialEnv()` — 从 `process.env` 删除全部凭证变量(真实凭证此后只存于 config 对象 → provider adapter 实例内存,不驻留 env / 不被子进程继承)
5. `assertProviderCredentials(providers, required)` — 按 `CredentialScope` 算出的 `required` 范围校验:范围内 provider 的 `api_key` 为空 → CLI 权威 `AUTH` 错误 + provider 专属 hint(取代 SDK 原始插值/zod 报错);范围外 provider 允许空 key
`bl auth login` 仅管理 bailian(DashScope)凭证;claude/ark/qoder 的 key 从 env(shell / `.env` / `~/.agents/config.json`)经插值进入 config 对象,同样被清扫。禁止命令层直接 `readConfigFile` 裸读凭证;bailian 字段以 CLI 鉴权链为唯一信源。
## 必查清单
@@ -9,18 +9,32 @@ import {
assertProviderCredentials,
type CredentialHost,
injectProviderCredentials,
normalizeInterpolatedProviderBlocks,
prepareProviderEnv,
resolveTargetProviderNames,
scrubCredentialEnv,
} from "./credentials.ts";
import { loadFileState } from "./file-state-manager.ts";
import { type HostContext, installSdkTransport } from "./transport.ts";
export { CREDENTIALS_NOTE } from "./credentials.ts";
export { CREDENTIALS_NOTE, OFFLINE_NOTE } from "./credentials.ts";
/**
* Which providers this run requires a non-empty key for:
* - "targets" (default) — the run's target providers per the config's
* default provider chain (mirrors the SDK's plan/apply targeting)
* - "none" — offline command (local config/state only), skip the check
* - "all" — every configured provider (`--provider all`)
* - any other name — the run was narrowed to that provider
* (`--provider <name>` / a provider-qualified state address)
*/
export type CredentialScope = "targets" | "none" | "all" | (string & {});
interface AgentConfigOptions {
resolveEnv?: boolean;
projectName?: string;
statePath?: string;
credentials?: CredentialScope;
}
/**
@@ -32,7 +46,8 @@ interface AgentConfigOptions {
* 3. override the bailian block with the CLI auth chain's credential (in-memory)
* 4. scrub all credential vars from process.env (real values now live only in
* the config object → provider adapters, never the environment)
* 5. fail with a CLI-authoritative AUTH error if any provider's key is empty
* 5. fail with a CLI-authoritative AUTH error if a provider within this run's
* {@link CredentialScope} has an empty key (offline commands pass "none")
*/
export async function resolveAgentProjectConfig(
host: CredentialHost,
@@ -41,9 +56,20 @@ export async function resolveAgentProjectConfig(
): Promise<LoadedProjectConfig> {
prepareProviderEnv();
const resolved = await resolveProjectConfig(filePath, options);
normalizeInterpolatedProviderBlocks(resolved.config.providers);
injectProviderCredentials(resolved.config.providers, host);
scrubCredentialEnv();
assertProviderCredentials(resolved.config.providers);
const scope = options.credentials ?? "targets";
if (scope !== "none") {
assertProviderCredentials(
resolved.config.providers,
scope === "targets"
? resolveTargetProviderNames(resolved.config)
: scope === "all"
? Object.keys(resolved.config.providers)
: [scope],
);
}
return resolved;
}
@@ -51,9 +51,18 @@ export interface CredentialHost {
export const CREDENTIALS_NOTE = [
"Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).",
"Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.",
"Only the providers this run involves (--provider, or the config's default provider chain) need credentials; other configured providers are not checked.",
"Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.",
];
/**
* Shared `--help` note for commands that never talk to a provider: they load
* agents.yaml / local state only, so no login or provider key is required.
*/
export const OFFLINE_NOTE = [
"Runs fully offline against local files: no login or provider credentials required.",
];
/**
* Load the SDK's env-based credential sources (`.env`, `~/.agents/config.json`)
* for non-bailian providers, then placeholder every credential var that is still
@@ -72,15 +81,18 @@ export function prepareProviderEnv(): void {
/**
* Override the bailian provider block with bl's authStage-resolved credential, so
* the bailian API key is authoritatively the CLI auth chain's — never a config
* file bare-read or a stale env value. `api_key` is replaced unconditionally;
* `base_url` / `workspace_id` are filled only when the block references them and
* the interpolated value is empty (a literal in agents.yaml is respected).
* file bare-read or a stale env value. `api_key` is replaced unconditionally
* when a credential resolved; `base_url` / `workspace_id` are filled only when
* the block references them and the interpolated value is empty (a literal in
* agents.yaml is respected).
*
* `base_url` carries {@link AGENTSTUDIO_API_PATH} because the SDK appends resource
* paths onto it verbatim; a value already ending in the suffix is left as-is.
* With no credential (only under --dry-run: authStage hard-gates otherwise) the
* bailian block is left untouched. Non-bailian blocks keep their interpolated
* (env-sourced) values.
* It is filled even without a credential — `client.baseUrl` is readable
* credential-less (defaults to the CLI's model-domain base URL) — so offline /
* out-of-scope runs still satisfy the SDK's "workspace_id or base_url" schema.
* With no credential the `api_key` is left untouched: an in-scope empty key is
* rejected by {@link assertProviderCredentials}, out-of-scope ones may stay empty.
*/
export function injectProviderCredentials(
providers: Record<string, unknown>,
@@ -91,16 +103,14 @@ export function injectProviderCredentials(
const block = bailian as Record<string, unknown>;
const cred = host.client.exportApiCredential();
if (cred) {
block.api_key = cred.token;
if ("base_url" in block && !block.base_url) {
// Defensive normalization: the auth chain already normalizes base_url to
// an origin, but never let a trailing slash produce "//api/v1/agentstudio".
const origin = cred.baseUrl.replace(/\/+$/, "");
block.base_url = origin.endsWith(AGENTSTUDIO_API_PATH)
? origin
: `${origin}${AGENTSTUDIO_API_PATH}`;
}
if (cred) block.api_key = cred.token;
if ("base_url" in block && !block.base_url) {
// Defensive normalization: the auth chain already normalizes base_url to
// an origin, but never let a trailing slash produce "//api/v1/agentstudio".
const origin = host.client.baseUrl.replace(/\/+$/, "");
block.base_url = origin.endsWith(AGENTSTUDIO_API_PATH)
? origin
: `${origin}${AGENTSTUDIO_API_PATH}`;
}
if ("workspace_id" in block && !block.workspace_id && host.settings.workspaceId) {
block.workspace_id = host.settings.workspaceId;
@@ -121,15 +131,55 @@ export function scrubCredentialEnv(): void {
}
/**
* After injection, fail with a CLI-authoritative AUTH error if any configured
* The SDK interpolates `${VAR}` into the raw YAML text, so an empty env var
* leaves `api_key:` with nothing after it — YAML parses that as null. Normalize
* every null provider field back to "" so the pipeline stays uniform: an empty
* api_key is caught by {@link assertProviderCredentials} when the provider is
* in scope, and out-of-scope blocks still satisfy the SDK's string schemas
* instead of failing zod with "received null" before the run even starts.
*/
export function normalizeInterpolatedProviderBlocks(providers: Record<string, unknown>): void {
for (const raw of Object.values(providers)) {
if (!raw || typeof raw !== "object") continue;
const block = raw as Record<string, unknown>;
for (const [fieldName, value] of Object.entries(block)) {
if (value === null) block[fieldName] = "";
}
}
}
/**
* The providers a run targets when no explicit `--provider` narrows it: the
* config's default provider, or every configured provider when the default is
* absent or "all". Mirrors the SDK's config-based `resolveTargetProviders`
* (not exported from the SDK's public surface).
*/
export function resolveTargetProviderNames(config: {
providers: Record<string, unknown>;
defaults?: { provider?: string };
}): string[] {
const defaultProvider = config.defaults?.provider;
if (!defaultProvider || defaultProvider === "all") return Object.keys(config.providers);
return [defaultProvider];
}
/**
* After injection, fail with a CLI-authoritative AUTH error if a required
* provider's `api_key` resolved empty (missing env var, or no bl login for
* bailian). Replaces the SDK's raw `Environment variable '...' is not set` /
* zod config error with a clean message plus a provider-specific hint. Validates
* every declared provider, so a project is only runnable once all its providers'
* keys are available.
* zod config error with a clean message plus a provider-specific hint.
* `required` limits the check to the providers this run actually involves
* (← --provider / state address / config default chain); providers outside
* that scope may keep empty keys — a project stays runnable per provider.
* Names without a matching config block are skipped: "provider not
* configured" is the engine's error to raise, not a credential problem.
*/
export function assertProviderCredentials(providers: Record<string, unknown>): void {
for (const [name, raw] of Object.entries(providers)) {
export function assertProviderCredentials(
providers: Record<string, unknown>,
required?: readonly string[],
): void {
for (const name of required ?? Object.keys(providers)) {
const raw = providers[name];
if (!raw || typeof raw !== "object") continue;
const block = raw as Record<string, unknown>;
if (!("api_key" in block)) continue;
@@ -46,6 +46,8 @@ const APPLY_FLAGS = {
export default defineCommand({
description: "Apply planned changes to create/update/delete agent resources",
auth: "apiKey",
// Provider-aware gate: only the providers this apply targets need credentials.
authOptional: true,
usageArgs: "[--file <path>] [--provider <name>] [--yes] [--concurrency <n>]",
flags: APPLY_FLAGS,
exampleArgs: ["--yes", "--provider bailian --yes"],
@@ -73,7 +75,9 @@ export default defineCommand({
const planned = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
const runtime = await buildAgentRuntime(ctx, file, {
credentials: flags.provider ?? "targets",
});
assertProviderConfigured(runtime, flags.provider);
return planProjectContext(runtime, {
provider: flags.provider,
@@ -31,6 +31,8 @@ const DESTROY_FLAGS = {
export default defineCommand({
description: "Destroy all managed agent resources tracked in state",
auth: "apiKey",
// Provider-aware gate: only the run's target providers need credentials.
authOptional: true,
usageArgs: "[--file <path>] [--yes] [--cascade]",
flags: DESTROY_FLAGS,
exampleArgs: ["--yes", "--yes --cascade"],
@@ -54,7 +56,9 @@ export default defineCommand({
const planned = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
const runtime = await buildAgentRuntime(ctx, file, {
credentials: "targets",
});
return planDestroyProjectContext(runtime);
}),
);
@@ -99,7 +99,7 @@ const INIT_FLAGS = {
export default defineCommand({
description: "Create a new agents.yaml template",
auth: "apiKey",
auth: "none",
usageArgs: "[--provider <name>] [--agent-name <name>] [--file <path>] [--force]",
flags: INIT_FLAGS,
exampleArgs: ["", "--provider bailian --agent-name assistant", "--provider all"],
@@ -41,6 +41,9 @@ const PLAN_FLAGS = {
export default defineCommand({
description: "Show what changes would be applied to agent infrastructure",
auth: "apiKey",
// Provider-aware gate: --no-refresh plans fully offline; a refreshing run
// only needs credentials for the providers it targets (see CredentialScope).
authOptional: true,
usageArgs: "[--file <path>] [--provider <name>] [--no-refresh] [--refresh-only]",
flags: PLAN_FLAGS,
exampleArgs: ["", "--provider bailian", "--no-refresh"],
@@ -52,7 +55,10 @@ export default defineCommand({
const planned = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
// --no-refresh never talks to a provider → no credentials required.
const runtime = await buildAgentRuntime(ctx, file, {
credentials: flags.noRefresh ? "none" : (flags.provider ?? "targets"),
});
assertProviderConfigured(runtime, flags.provider);
return planProjectContext(runtime, {
provider: flags.provider,
@@ -43,6 +43,8 @@ const SESSION_CREATE_FLAGS = {
export default defineCommand({
description: "Create a new session for an agent",
auth: "apiKey",
// Provider-aware gate: only the session's provider needs credentials.
authOptional: true,
usageArgs: "[--agent <name>] [--environment <name>] [--title <title>] [--file <path>]",
flags: SESSION_CREATE_FLAGS,
exampleArgs: ["", "--agent assistant", "--agent assistant --title 'debug run'"],
@@ -72,7 +74,9 @@ export default defineCommand({
const run = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
const runtime = await buildAgentRuntime(ctx, file, {
credentials: flags.provider ?? "targets",
});
return createSessionForAgent(runtime, {
agent: flags.agent,
provider: flags.provider,
@@ -27,6 +27,8 @@ const SESSION_DELETE_FLAGS = {
export default defineCommand({
description: "Delete a session",
auth: "apiKey",
// Provider-aware gate: only the session's provider needs credentials.
authOptional: true,
usageArgs: "--session-id <id> [--provider <name>] [--file <path>]",
flags: SESSION_DELETE_FLAGS,
exampleArgs: ["--session-id sess_abc123"],
@@ -50,7 +52,9 @@ export default defineCommand({
await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
const runtime = await buildAgentRuntime(ctx, file, {
credentials: flags.provider ?? "targets",
});
await deleteSession(runtime, flags.sessionId, flags.provider);
}),
);
@@ -38,6 +38,8 @@ const SESSION_EVENTS_FLAGS = {
export default defineCommand({
description: "List event history for a session",
auth: "apiKey",
// Provider-aware gate: only the session's provider needs credentials.
authOptional: true,
usageArgs: "--session-id <id> [--limit <n>] [--all] [--file <path>]",
flags: SESSION_EVENTS_FLAGS,
exampleArgs: ["--session-id sess_abc123", "--session-id sess_abc123 --all"],
@@ -49,7 +51,9 @@ export default defineCommand({
const { items: events, hasMore } = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
const runtime = await buildAgentRuntime(ctx, file, {
credentials: flags.provider ?? "targets",
});
return fetchAllPages(async (page) => {
const result = await listSessionEvents(runtime, flags.sessionId, {
provider: flags.provider,
@@ -27,6 +27,8 @@ const SESSION_GET_FLAGS = {
export default defineCommand({
description: "Get details of a session",
auth: "apiKey",
// Provider-aware gate: only the session's provider needs credentials.
authOptional: true,
usageArgs: "--session-id <id> [--provider <name>] [--file <path>]",
flags: SESSION_GET_FLAGS,
exampleArgs: ["--session-id sess_abc123"],
@@ -38,7 +40,9 @@ export default defineCommand({
const session = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
const runtime = await buildAgentRuntime(ctx, file, {
credentials: flags.provider ?? "targets",
});
return getSession(runtime, flags.sessionId, flags.provider);
}),
);
@@ -31,6 +31,8 @@ const SESSION_LIST_FLAGS = {
export default defineCommand({
description: "List sessions from the provider",
auth: "apiKey",
// Provider-aware gate: only the session's provider needs credentials.
authOptional: true,
usageArgs: "[--agent <name>] [--all] [--provider <name>] [--file <path>]",
flags: SESSION_LIST_FLAGS,
exampleArgs: ["", "--agent assistant", "--all"],
@@ -42,7 +44,9 @@ export default defineCommand({
const { items: summaries, hasMore } = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
const runtime = await buildAgentRuntime(ctx, file, {
credentials: flags.provider ?? "targets",
});
return fetchAllPages(async (page) => {
const result = await listSessionSummaries(runtime, {
agent: flags.agent,
@@ -57,6 +57,8 @@ const SESSION_RUN_FLAGS = {
export default defineCommand({
description: "Create a session, send a message, and stream the response",
auth: "apiKey",
// Provider-aware gate: only the session's provider needs credentials.
authOptional: true,
usageArgs: "--prompt <text> [--agent <name>] [--no-stream] [--file <path>]",
flags: SESSION_RUN_FLAGS,
exampleArgs: ['--prompt "hello"', '--agent assistant --prompt "summarize this repo"'],
@@ -98,7 +100,9 @@ export default defineCommand({
await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
const runtime = await buildAgentRuntime(ctx, file, {
credentials: flags.provider ?? "targets",
});
if (flags.noStream) {
const run = await startSessionRunPolling(runtime, flags.prompt, runOptions);
if (!asJson) process.stderr.write(`Session created: ${run.session.id}\n`);
@@ -38,6 +38,8 @@ const SESSION_SEND_FLAGS = {
export default defineCommand({
description: "Send a message to an existing session and stream the response",
auth: "apiKey",
// Provider-aware gate: only the session's provider needs credentials.
authOptional: true,
usageArgs: "--session-id <id> --message <text> [--no-stream] [--file <path>]",
flags: SESSION_SEND_FLAGS,
exampleArgs: ['--session-id sess_abc123 --message "continue"'],
@@ -66,7 +68,9 @@ export default defineCommand({
await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
const runtime = await buildAgentRuntime(ctx, file, {
credentials: flags.provider ?? "targets",
});
if (flags.noStream) {
const result = await sendSessionMessagePolling(runtime, flags.sessionId, flags.message, {
provider: flags.provider,
@@ -30,6 +30,8 @@ const SKILL_LIST_FLAGS = {
export default defineCommand({
description: "List skills from the provider's skill catalog",
auth: "apiKey",
// Provider-aware gate: only the resolved catalog provider needs credentials.
authOptional: true,
usageArgs: "[--source custom|official|all] [--provider <name>] [--file <path>]",
flags: SKILL_LIST_FLAGS,
exampleArgs: [
@@ -56,7 +58,9 @@ export default defineCommand({
const skills = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
const runtime = await buildAgentRuntime(ctx, file, {
credentials: flags.provider ?? "targets",
});
if (source !== "all") {
return listSkills(runtime, { provider: flags.provider, source });
}
@@ -33,6 +33,8 @@ const STATE_IMPORT_FLAGS = {
export default defineCommand({
description: "Import an existing remote resource into agents state",
auth: "apiKey",
// Provider-aware gate: only the address's provider needs credentials.
authOptional: true,
usageArgs:
"--address <provider.type.name> --remote-id <id> [--resource-version <n>] [--file <path>]",
flags: STATE_IMPORT_FLAGS,
@@ -62,10 +64,13 @@ export default defineCommand({
await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
// Parse first: the address names the one provider this import touches.
const parsed = parseStateAddress(flags.address, {
requireProvider: true,
});
const runtime = await buildAgentRuntime(ctx, file, {
credentials: parsed.provider ?? "targets",
});
await importResource(runtime, parsed, flags.remoteId, {
resourceVersion: flags.resourceVersion,
});
@@ -1,6 +1,6 @@
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { buildAgentRuntime, OFFLINE_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
@@ -14,11 +14,11 @@ const STATE_LIST_FLAGS = {
export default defineCommand({
description: "List resources tracked in agents state",
auth: "apiKey",
auth: "none",
usageArgs: "[--file <path>]",
flags: STATE_LIST_FLAGS,
exampleArgs: ["", "--file agents.yaml"],
notes: CREDENTIALS_NOTE,
notes: OFFLINE_NOTE,
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
@@ -26,7 +26,9 @@ export default defineCommand({
const resources = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
const runtime = await buildAgentRuntime(ctx, file, {
credentials: "none",
});
return runtime.state.listResources();
}),
);
@@ -7,7 +7,7 @@ import {
} from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { parseStateAddress } from "@openagentpack/sdk";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { buildAgentRuntime, OFFLINE_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
@@ -27,11 +27,11 @@ const STATE_RM_FLAGS = {
export default defineCommand({
description: "Remove a resource from state without destroying it remotely",
auth: "apiKey",
auth: "none",
usageArgs: "--address <provider.type.name> [--file <path>]",
flags: STATE_RM_FLAGS,
exampleArgs: ["--address bailian.agent.assistant"],
notes: CREDENTIALS_NOTE,
notes: OFFLINE_NOTE,
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
@@ -48,7 +48,9 @@ export default defineCommand({
await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
const runtime = await buildAgentRuntime(ctx, file, {
credentials: "none",
});
const parsed = parseStateAddress(flags.address, {
requireProvider: false,
});
@@ -7,7 +7,7 @@ import {
} from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { parseStateAddress } from "@openagentpack/sdk";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { buildAgentRuntime, OFFLINE_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
@@ -27,11 +27,11 @@ const STATE_SHOW_FLAGS = {
export default defineCommand({
description: "Show details of a resource in agents state",
auth: "apiKey",
auth: "none",
usageArgs: "--address <provider.type.name> [--file <path>]",
flags: STATE_SHOW_FLAGS,
exampleArgs: ["--address bailian.agent.assistant"],
notes: CREDENTIALS_NOTE,
notes: OFFLINE_NOTE,
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
@@ -39,7 +39,9 @@ export default defineCommand({
const found = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
const runtime = await buildAgentRuntime(ctx, file, {
credentials: "none",
});
const parsed = parseStateAddress(flags.address, {
requireProvider: false,
});
@@ -7,7 +7,7 @@ import {
} from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { validateProjectConfig } from "@openagentpack/sdk";
import { CREDENTIALS_NOTE, resolveAgentProjectConfig } from "./_engine/config-loader.ts";
import { OFFLINE_NOTE, resolveAgentProjectConfig } from "./_engine/config-loader.ts";
import { withAgentErrors } from "./_engine/errors.ts";
const VALIDATE_FLAGS = {
@@ -20,18 +20,20 @@ const VALIDATE_FLAGS = {
export default defineCommand({
description: "Validate an agents.yaml configuration (offline)",
auth: "apiKey",
auth: "none",
usageArgs: "[--file <path>]",
flags: VALIDATE_FLAGS,
exampleArgs: ["", "--file agents.yaml"],
notes: CREDENTIALS_NOTE,
notes: OFFLINE_NOTE,
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const file = flags.file ?? "agents.yaml";
const diagnostics = await withAgentErrors(async () => {
const { config } = await resolveAgentProjectConfig(ctx, file);
const { config } = await resolveAgentProjectConfig(ctx, file, {
credentials: "none",
});
return validateProjectConfig(config);
});
@@ -13,13 +13,15 @@ import {
type CredentialHost,
injectProviderCredentials,
prepareProviderEnv,
resolveTargetProviderNames,
scrubCredentialEnv,
} from "../src/commands/managed-agent/_engine/credentials.ts";
/**
* 凭证内存注入管道:injectProviderCredentials 把 authStage 解析进 Client 的凭证
* 权威覆写 bailian 配置块(不落 env),scrubCredentialEnv 清空所有凭证 env,
* assertProviderCredentials 对空 key 给 CLI 权威 AUTH 错误。用快照隔离凭证 env。
* assertProviderCredentials 按本次运行涉及的 provider 范围对空 key 给 CLI 权威
* AUTH 错误。用快照隔离凭证 env。
*/
const TRACKED_ENV = [
"DASHSCOPE_API_KEY",
@@ -139,11 +141,11 @@ test("inject:workspace_id 引用且为空时用 settings 填充;有字面量则
expect(literal.bailian.workspace_id).toBe("ws-yaml");
});
test("inject:无凭证(dry-run)时 bailian 块保持不变", () => {
test("inject:无凭证时 api_key 保持不变,base_url 仍用 client 默认域名补齐(离线/范围外 schema 可用)", () => {
const providers = { bailian: { api_key: "", base_url: "" } };
injectProviderCredentials(providers, makeHost({}));
expect(providers.bailian.api_key).toBe("");
expect(providers.bailian.base_url).toBe("");
expect(providers.bailian.base_url).toBe("https://dashscope.aliyuncs.com/api/v1/agentstudio");
});
test("inject:非 bailian provider 块不被触碰", () => {
@@ -190,6 +192,33 @@ test("assert:bailian key 为空(dry-run/未登录)抛 AUTH 且 hint 指向 bl au
expect(err.hint).toContain("bl auth login");
});
test("assert:required 限定范围后,范围外 provider 的空 key 不拦截", () => {
const providers = {
bailian: { api_key: "" },
claude: { api_key: "sk-ant" },
};
// 本次只涉及 claude(如 --provider claude):bailian 未登录不应阻塞
expect(() => assertProviderCredentials(providers, ["claude"])).not.toThrow();
// 反向:范围内的空 key 仍拦截
expect(() => assertProviderCredentials(providers, ["bailian"])).toThrow();
});
test("assert:required 里未配置的 provider 名被跳过(由引擎报未配置错误)", () => {
expect(() => assertProviderCredentials({ bailian: { api_key: "" } }, ["qoder"])).not.toThrow();
});
test("targets:默认 provider 链镜像 SDK —— default 为单个时只涉及它,缺失/all 时为全部", () => {
const providers = { bailian: {}, claude: {} };
expect(resolveTargetProviderNames({ providers, defaults: { provider: "claude" } })).toEqual([
"claude",
]);
expect(resolveTargetProviderNames({ providers })).toEqual(["bailian", "claude"]);
expect(resolveTargetProviderNames({ providers, defaults: { provider: "all" } })).toEqual([
"bailian",
"claude",
]);
});
test("scrub:所有凭证 env 变量被删除", () => {
process.env.DASHSCOPE_API_KEY = "x";
process.env.ANTHROPIC_API_KEY = "y";
@@ -0,0 +1,28 @@
version: "1"
providers:
bailian:
api_key: ${DASHSCOPE_API_KEY}
base_url: ${BAILIAN_BASE_URL}
claude:
api_key: ${ANTHROPIC_API_KEY}
defaults:
provider: all
environments:
dev:
config:
type: cloud
networking:
type: unrestricted
agents:
assistant:
description: "E2E multi-provider fixture"
model:
bailian: qwen3.7-max
claude: claude-sonnet-4-6
instructions: |
You are a helpful assistant.
environment: dev
@@ -8,8 +8,9 @@ import { MANAGED_AGENT_ROUTES } from "./topic-routes.ts";
/**
* managed-agent 凭证链 e2e:验证 bl 自有配置体系(config 写入 / 命名 Profile /
* logout)与错误映射如何流入 SDK 引擎。全部离线:`managed-agent validate` 会走
* authStage 凭证解析 + 引擎注入 + agents.yaml 校验,但不发任何网络请求。
* logout)与错误映射如何流入 SDK 引擎。全部离线:凭证门禁用 `managed-agent plan`
* 验证(provider-aware:空 state 不发网络请求,但仍按目标 provider 校验凭证);
* `validate` / `state list` / `plan --no-refresh` 属离线命令,无凭证也必须可用。
* 配置一律通过 BAILIAN_CONFIG_DIR 指向临时目录,绝不触碰真实用户配置。
*/
@@ -20,6 +21,7 @@ const ROUTES = {
const AGENTS_YAML = join(e2eFixturesDir, "managed-agent", "agents.yaml");
const AGENTS_YAML_INVALID = join(e2eFixturesDir, "managed-agent", "agents-invalid.yaml");
const AGENTS_YAML_MULTI = join(e2eFixturesDir, "managed-agent", "agents-multi.yaml");
const tempDirs: string[] = [];
@@ -45,6 +47,16 @@ function validateArgs(file: string): string[] {
return ["managed-agent", "validate", "--file", file, "--quiet"];
}
/** plan 是凭证门禁命令:空 state 下不发网络,但仍按目标 provider 校验凭证。 */
function planArgs(file: string): string[] {
return ["managed-agent", "plan", "--file", file, "--quiet"];
}
/** 隔离宿主机的 ~/.agents/config.json,避免它强制覆盖 provider 凭证 env。 */
function isolatedAgentsConfigEnv(): NodeJS.ProcessEnv {
return { AGENTS_CONFIG_PATH: join(tmpdir(), "bl-e2e-no-agents-config.json") };
}
/** 分配一个刚释放的本地端口,连接必然 ECONNREFUSED,用于网络错误场景。 */
async function closedPort(): Promise<number> {
const server = createServer();
@@ -61,9 +73,9 @@ async function closedPort(): Promise<number> {
}
describe("e2e: managed-agent 凭证链(config 写入 / Profile / logout / 错误映射)", () => {
test("config.json 写入的 api_key 流入引擎,validate 离线通过", async () => {
test("config.json 写入的 api_key 流入引擎,plan 离线通过", async () => {
const env = makeConfigEnv({ api_key: "sk-e2e-config-write" });
const { stderr, exitCode } = await runCommandE2e(ROUTES, validateArgs(AGENTS_YAML), env);
const { stderr, exitCode } = await runCommandE2e(ROUTES, planArgs(AGENTS_YAML), env);
expect(exitCode, stderr).toBe(0);
});
@@ -72,7 +84,7 @@ describe("e2e: managed-agent 凭证链(config 写入 / Profile / logout / 错
work: { api_key: "sk-e2e-profile-work" },
active_config: "work",
});
const { stderr, exitCode } = await runCommandE2e(ROUTES, validateArgs(AGENTS_YAML), env);
const { stderr, exitCode } = await runCommandE2e(ROUTES, planArgs(AGENTS_YAML), env);
expect(exitCode, stderr).toBe(0);
});
@@ -82,27 +94,27 @@ describe("e2e: managed-agent 凭证链(config 写入 / Profile / logout / 错
empty: {},
active_config: "empty",
});
const { stderr, exitCode } = await runCommandE2e(ROUTES, validateArgs(AGENTS_YAML), env);
const { stderr, exitCode } = await runCommandE2e(ROUTES, planArgs(AGENTS_YAML), env);
expect(exitCode).toBe(3);
expect(stderr).toMatch(/auth login|API key/i);
});
test("auth logout 清除凭证后 validate 报 AUTH,而非用残留凭证", async () => {
test("auth logout 清除凭证后 plan 报 AUTH,而非用残留凭证", async () => {
const env = makeConfigEnv({ api_key: "sk-e2e-before-logout" });
const before = await runCommandE2e(ROUTES, validateArgs(AGENTS_YAML), env);
const before = await runCommandE2e(ROUTES, planArgs(AGENTS_YAML), env);
expect(before.exitCode, before.stderr).toBe(0);
const logout = await runCommandE2e(ROUTES, ["auth", "logout"], env);
expect(logout.exitCode, logout.stderr).toBe(0);
const after = await runCommandE2e(ROUTES, validateArgs(AGENTS_YAML), env);
const after = await runCommandE2e(ROUTES, planArgs(AGENTS_YAML), env);
expect(after.exitCode).toBe(3);
expect(after.stderr).toMatch(/auth login|API key/i);
});
test("agents.yaml schema 错误映射为 USAGE (2),不透传原始 zod dump", async () => {
const env = makeConfigEnv({ api_key: "sk-e2e-config-write" });
const env = makeConfigEnv({});
const { stderr, exitCode } = await runCommandE2e(
ROUTES,
validateArgs(AGENTS_YAML_INVALID),
@@ -142,3 +154,64 @@ describe("e2e: managed-agent 凭证链(config 写入 / Profile / logout / 错
expect(stderr).toMatch(/ECONNREFUSED|refused/i);
});
});
describe("e2e: managed-agent 鉴权分层(离线命令免登录 / provider-aware 按需校验)", () => {
test("validate 无任何凭证也离线通过 (0)", async () => {
const env = makeConfigEnv({});
const { stderr, exitCode } = await runCommandE2e(ROUTES, validateArgs(AGENTS_YAML), env);
expect(exitCode, stderr).toBe(0);
});
test("state list 无任何凭证也离线通过 (0),stdout 为合法 JSON", async () => {
const env = makeConfigEnv({});
const { stdout, stderr, exitCode } = await runCommandE2e(
ROUTES,
["managed-agent", "state", "list", "--file", AGENTS_YAML, "--output", "json"],
env,
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ resources?: unknown[] }>(stdout);
expect(Array.isArray(data.resources)).toBe(true);
});
test("plan --no-refresh 无任何凭证也离线通过 (0)", async () => {
const env = makeConfigEnv({});
const { stderr, exitCode } = await runCommandE2e(
ROUTES,
[...planArgs(AGENTS_YAML), "--no-refresh"],
env,
);
expect(exitCode, stderr).toBe(0);
});
test("多 provider 下 plan --provider claude 只需 claude 凭证,bailian 未登录不阻塞 (0)", async () => {
const env = {
...makeConfigEnv({}),
...isolatedAgentsConfigEnv(),
ANTHROPIC_API_KEY: "sk-ant-e2e-scope",
CLAUDE_API_KEY: "",
};
const { stderr, exitCode } = await runCommandE2e(
ROUTES,
[...planArgs(AGENTS_YAML_MULTI), "--provider", "claude"],
env,
);
expect(exitCode, stderr).toBe(0);
});
test("plan --provider claude 缺 claude key 时报 AUTH (3),hint 指向 ANTHROPIC_API_KEY", async () => {
const env = {
...makeConfigEnv({ api_key: "sk-e2e-bailian-present" }),
...isolatedAgentsConfigEnv(),
ANTHROPIC_API_KEY: "",
CLAUDE_API_KEY: "",
};
const { stderr, exitCode } = await runCommandE2e(
ROUTES,
[...planArgs(AGENTS_YAML_MULTI), "--provider", "claude"],
env,
);
expect(exitCode).toBe(3);
expect(stderr).toMatch(/ANTHROPIC_API_KEY/);
});
});
@@ -5,8 +5,10 @@ import { MANAGED_AGENT_ROUTES } from "./topic-routes.ts";
/**
* managed-agent:help / 缺参不依赖密钥;所有 mutation 命令的 --dry-run
* 必须在构建 SDK runtime(凭证注入 / 联网 / 写盘)之前短路,因此同样不需要密钥。
* 鉴权分层:离线命令(init/validate/state list|show|rm)auth: "none";联网命令
* provider-aware,只校验本次涉及的 provider(见 managed-agent-auth-chain e2e)。
* 真实集成(apply/destroy/session 流程)依赖工作区内的 agents.yaml 与远端资源,
* 属于批量场景,暂仅覆盖 dry-run 契约。
* 属批量场景,暂仅覆盖 dry-run 契约。
*/
describe("e2e: managed-agent", () => {
@@ -67,21 +69,17 @@ describe("e2e: managed-agent", () => {
});
test("managed-agent skill-list --source all 通过参数校验(缺配置文件时才失败)", async () => {
// auth: "apiKey" 的凭证解析先于 run() 执行;注入假 key 让用例不依赖环境凭证,
// 命令仍会在配置加载阶段因文件缺失短路,不产生任何网络请求。
const { stderr, exitCode } = await runCommandE2e(
MANAGED_AGENT_ROUTES,
[
"managed-agent",
"skill-list",
"--source",
"all",
"--file",
"agents.e2e-missing.yaml",
"--quiet",
],
{ DASHSCOPE_API_KEY: "sk-e2e-skill-list" },
);
// provider-aware 鉴权不再前置硬门禁:无需注入假 key,命令在配置加载阶段
// 因文件缺失短路,不产生任何网络请求。
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"skill-list",
"--source",
"all",
"--file",
"agents.e2e-missing.yaml",
"--quiet",
]);
// all 是合法值:不应报 --source 用法错误,而是走到配置加载后因文件缺失退出
expect(exitCode).toBe(2);
expect(stderr).not.toMatch(/--source must be one of/i);
@@ -159,8 +159,10 @@ export const TOKEN_PLAN_ROUTES: E2eRouteExports = {
export const MANAGED_AGENT_ROUTES: E2eRouteExports = {
"managed-agent init": "managedAgentInit",
"managed-agent validate": "managedAgentValidate",
"managed-agent plan": "managedAgentPlan",
"managed-agent apply": "managedAgentApply",
"managed-agent destroy": "managedAgentDestroy",
"managed-agent state list": "managedAgentStateList",
"managed-agent state rm": "managedAgentStateRm",
"managed-agent state import": "managedAgentStateImport",
"managed-agent session create": "managedAgentSessionCreate",
+26 -4
View File
@@ -67,10 +67,21 @@ export type AuthRequirement = "apiKey" | "console" | "openapi" | "none";
// ── Flag 分组:全局(所有命令) + 凭证域(按命令的 auth 可见) ────────────────────
/** 所有命令都可用的全局 flag。 */
export const GLOBAL_FLAGS = {
output: { type: "string", valueHint: "<format>", description: "Output format: text, json" },
timeout: { type: "number", valueHint: "<seconds>", description: "Request timeout" },
output: {
type: "string",
valueHint: "<format>",
description: "Output format: text, json",
},
timeout: {
type: "number",
valueHint: "<seconds>",
description: "Request timeout",
},
quiet: { type: "switch", description: "Suppress non-essential output" },
verbose: { type: "switch", description: "Print HTTP request/response details" },
verbose: {
type: "switch",
description: "Print HTTP request/response details",
},
dryRun: { type: "switch", description: "Dry run mode" },
config: {
type: "string",
@@ -92,7 +103,10 @@ export const CONCURRENT_FLAG = {
/** Command-scoped flag for task-based commands that can return without polling. */
export const ASYNC_FLAG = {
async: { type: "switch", description: "Return async task id without waiting" },
async: {
type: "switch",
description: "Return async task id without waiting",
},
} satisfies FlagsDef;
/** Model 域凭证/连接 flag,`auth: "apiKey"` 命令可见。 */
@@ -193,6 +207,14 @@ export interface Command<F extends FlagsDef = FlagsDef> {
description: string;
/** Credential this command requires. See {@link AuthRequirement}. */
auth: AuthRequirement;
/**
* Soften the auth gate: authStage still resolves the `auth` domain's
* credential into `ctx.client` when available, but a missing credential no
* longer fails before `run`. For commands that enforce their own scoped
* credential requirements (e.g. managed-agent commands, where a run may only
* involve third-party providers and must not be blocked on a Bailian key).
*/
authOptional?: boolean;
/** Usage line arg portion, e.g. "--prompt <text> [flags]". Manually written. */
usageArgs?: string;
/** Example arg strings (without the `<bin> <path>` prefix). */
+16 -5
View File
@@ -75,17 +75,24 @@ export function compose(stack: Middleware[]): (ctx: RunContext) => Promise<void>
* Bake the credential for the command's declared `auth` into `ctx.client`, and
* gate: no credential → throw before the command runs. dry-run 例外:凭证解析失败
* 不抛(dry-run 只打印请求,无需凭证;console 的 dry-run 展示读 settings.console*)。
* `authOptional` 例外:凭证可用则注入,缺失不在此处抛 —— 命令自行按实际涉及范围
* 校验(如 managed-agent 只校验本次运行涉及的 provider)。
* `auth: "none"` commands keep a credential-less client.
*/
export const authStage: Middleware = async (ctx, next) => {
const { command, settings, sources } = ctx;
const base = { identity: ctx.identity, settings, baseUrl: resolveModelBaseUrl(sources) };
const base = {
identity: ctx.identity,
settings,
baseUrl: resolveModelBaseUrl(sources),
};
const tolerateMissing = settings.dryRun || command.authOptional === true;
if (command.auth === "apiKey") {
let cred: ApiKeyCredential | undefined;
try {
cred = resolveApiKey(sources);
} catch (err) {
if (!settings.dryRun) throw err;
if (!tolerateMissing) throw err;
}
ctx.client = new Client({ ...base, apiCred: cred });
if (cred) maybeShowStatusBar(settings, cred.token, cred);
@@ -94,7 +101,7 @@ export const authStage: Middleware = async (ctx, next) => {
try {
cred = resolveConsole(sources);
} catch (err) {
if (!settings.dryRun) throw err;
if (!tolerateMissing) throw err;
}
if (cred) ctx.client = new Client({ ...base, consoleCred: cred });
} else if (command.auth === "openapi") {
@@ -102,7 +109,7 @@ export const authStage: Middleware = async (ctx, next) => {
try {
cred = resolveOpenApi(sources);
} catch (err) {
if (!settings.dryRun) throw err;
if (!tolerateMissing) throw err;
}
ctx.client = new Client({ ...base, openApiCred: cred });
}
@@ -112,7 +119,11 @@ export const authStage: Middleware = async (ctx, next) => {
/** Record command execution (start / success / failure) around the command. */
export const telemetryStage: Middleware = (ctx, next) => {
return trackCommandExecution(
{ identity: ctx.identity, settings: ctx.settings, authMethod: ctx.command.auth },
{
identity: ctx.identity,
settings: ctx.settings,
authMethod: ctx.command.auth,
},
ctx.path,
ctx.flags,
next,
+22 -28
View File
@@ -53,6 +53,7 @@ Index: [index.md](index.md)
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Only the providers this run involves (--provider, or the config's default provider chain) need credentials; other configured providers are not checked.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
#### Examples
@@ -87,6 +88,7 @@ bl managed-agent apply --provider bailian --yes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Only the providers this run involves (--provider, or the config's default provider chain) need credentials; other configured providers are not checked.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
#### Examples
@@ -115,8 +117,6 @@ bl managed-agent destroy --yes --cascade
| `--agent-name <name>` | string | no | Name of the first agent (default: assistant) |
| `--file <path>` | string | no | Output config path (default: agents.yaml) |
| `--force` | switch | no | Overwrite an existing config file |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Examples
@@ -155,6 +155,7 @@ bl managed-agent init --provider all
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Only the providers this run involves (--provider, or the config's default provider chain) need credentials; other configured providers are not checked.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
#### Examples
@@ -197,6 +198,7 @@ bl managed-agent plan --no-refresh
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Only the providers this run involves (--provider, or the config's default provider chain) need credentials; other configured providers are not checked.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
#### Examples
@@ -235,6 +237,7 @@ bl managed-agent session create --agent assistant --title 'debug run'
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Only the providers this run involves (--provider, or the config's default provider chain) need credentials; other configured providers are not checked.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
#### Examples
@@ -267,6 +270,7 @@ bl managed-agent session delete --session-id sess_abc123
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Only the providers this run involves (--provider, or the config's default provider chain) need credentials; other configured providers are not checked.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
#### Examples
@@ -301,6 +305,7 @@ bl managed-agent session events --session-id sess_abc123 --all
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Only the providers this run involves (--provider, or the config's default provider chain) need credentials; other configured providers are not checked.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
#### Examples
@@ -332,6 +337,7 @@ bl managed-agent session get --session-id sess_abc123
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Only the providers this run involves (--provider, or the config's default provider chain) need credentials; other configured providers are not checked.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
#### Examples
@@ -376,6 +382,7 @@ bl managed-agent session list --all
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Only the providers this run involves (--provider, or the config's default provider chain) need credentials; other configured providers are not checked.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
#### Examples
@@ -412,6 +419,7 @@ bl managed-agent session run --agent assistant --prompt "summarize this repo"
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Only the providers this run involves (--provider, or the config's default provider chain) need credentials; other configured providers are not checked.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
#### Examples
@@ -442,6 +450,7 @@ bl managed-agent session send --session-id sess_abc123 --message "continue"
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Only the providers this run involves (--provider, or the config's default provider chain) need credentials; other configured providers are not checked.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
- Providers without a skill listing API (e.g. ark) return an empty list.
- For agent-driven skill selection, use `--source all --output json`: one call returns both catalogs with per-skill `source` and `description` fields to pick from.
@@ -488,6 +497,7 @@ bl managed-agent skill-list --source custom --provider bailian
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Only the providers this run involves (--provider, or the config's default provider chain) need credentials; other configured providers are not checked.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
#### Examples
@@ -506,17 +516,13 @@ bl managed-agent state import --address bailian.agent.assistant --remote-id agen
#### Flags
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | --------------------------------------- |
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| --------------- | ------ | -------- | --------------------------------------- |
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
- Runs fully offline against local files: no login or provider credentials required.
#### Examples
@@ -542,14 +548,10 @@ bl managed-agent state list --file agents.yaml
| -------------------------------- | ------ | -------- | --------------------------------------- |
| `--address <provider.type.name>` | string | yes | Resource state address (required) |
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
- Runs fully offline against local files: no login or provider credentials required.
#### Examples
@@ -571,14 +573,10 @@ bl managed-agent state rm --address bailian.agent.assistant
| -------------------------------- | ------ | -------- | --------------------------------------- |
| `--address <provider.type.name>` | string | yes | Resource state address (required) |
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
- Runs fully offline against local files: no login or provider credentials required.
#### Examples
@@ -596,17 +594,13 @@ bl managed-agent state show --address bailian.agent.assistant
#### Flags
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | --------------------------------------- |
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| --------------- | ------ | -------- | --------------------------------------- |
| `--file <path>` | string | no | Config file path (default: agents.yaml) |
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
- Runs fully offline against local files: no login or provider credentials required.
#### Examples