mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat(agent): 鉴权分离线命令和在线命令,仅对bailian provider鉴权
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user