|
1 | | -import { bootstrapRuntimeCredentialsSync } from "@openagentpack/sdk"; |
2 | | -import { readConfigFile } from "bailian-cli-core"; |
| 1 | +import { AGENTS_PROVIDER_FIELDS, bootstrapRuntimeCredentialsSync } from "@openagentpack/sdk"; |
| 2 | +import { BailianError, type Client, ExitCode, type Settings } from "bailian-cli-core"; |
3 | 3 |
|
4 | | -let bootstrapped = false; |
| 4 | +/** |
| 5 | + * AgentStudio API path the SDK's BailianClient serves resources under. bl's |
| 6 | + * `base_url` is the bare model-service origin (e.g. https://dashscope.aliyuncs.com); |
| 7 | + * the SDK appends resource paths onto the bailian provider's `base_url` verbatim, |
| 8 | + * so the agent path must carry this suffix. See OpenAgentPack BailianClient. |
| 9 | + */ |
| 10 | +const AGENTSTUDIO_API_PATH = "/api/v1/agentstudio"; |
| 11 | + |
| 12 | +/** |
| 13 | + * Every env var the SDK recognizes as provider credential material (primary keys |
| 14 | + * from the SDK's own field map) plus bl-side interpolation aliases and bailian's |
| 15 | + * endpoint var (not part of AGENTS_PROVIDER_FIELDS). These are the only vars the |
| 16 | + * pipeline placeholders (to keep interpolation from throwing) and scrubs (so no |
| 17 | + * real credential persists in the environment). |
| 18 | + */ |
| 19 | +const CREDENTIAL_ENV_KEYS = [ |
| 20 | + ...new Set([ |
| 21 | + ...Object.values(AGENTS_PROVIDER_FIELDS).flatMap((fields) => fields.map((field) => field.key)), |
| 22 | + "BAILIAN_API_KEY", |
| 23 | + "BAILIAN_BASE_URL", |
| 24 | + "CLAUDE_API_KEY", |
| 25 | + "QODER_API_KEY", |
| 26 | + ]), |
| 27 | +]; |
| 28 | + |
| 29 | +/** How to obtain each provider's key, surfaced in the CLI's own AUTH error when it is missing. */ |
| 30 | +const CREDENTIAL_HINTS: Record<string, string> = { |
| 31 | + bailian: "Run `bl auth login --api-key <key>`, pass --api-key, or set DASHSCOPE_API_KEY.", |
| 32 | + claude: "Set ANTHROPIC_API_KEY (or CLAUDE_API_KEY) in your shell or .env.", |
| 33 | + ark: "Set ARK_API_KEY in your shell or .env.", |
| 34 | + qoder: "Set QODER_PAT (or QODER_API_KEY) in your shell or .env.", |
| 35 | +}; |
| 36 | + |
| 37 | +/** The slice of CommandContext the credential pipeline needs: authStage-resolved client + settings. */ |
| 38 | +export interface CredentialHost { |
| 39 | + client: Client; |
| 40 | + settings: Settings; |
| 41 | +} |
5 | 42 |
|
6 | 43 | /** |
7 | 44 | * Shared `--help` note documenting where agent commands get provider |
8 | | - * credentials. Mirrors the credential-source hint bl's native commands surface |
9 | | - * (knowledge / usage / token-plan), adapted for the SDK's env-based resolution |
10 | | - * and the {@link bridgeBailianCredentials} fallback. Attach to every command |
11 | | - * that loads agents.yaml. `bl` prefix is safe: agent commands ship on `bl` only. |
| 45 | + * credentials. Bailian goes through bl's own auth chain (commands declare |
| 46 | + * `auth: "apiKey"`); other providers come from env. Either way the resolved |
| 47 | + * credential is injected into the SDK in-memory and scrubbed from the |
| 48 | + * environment. Attach to every command that loads agents.yaml. `bl` prefix is |
| 49 | + * safe: agent commands ship on `bl` only. |
12 | 50 | */ |
13 | 51 | export const CREDENTIALS_NOTE = [ |
14 | | - "Credentials come from the env vars referenced in agents.yaml (e.g. ${DASHSCOPE_API_KEY}, ${BAILIAN_BASE_URL}).", |
15 | | - "For the bailian provider, bl fills these from your login as a fallback: `bl auth login --api-key <key> --agentstudio-base-url <url>`.", |
| 52 | + "Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).", |
| 53 | + "Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.", |
| 54 | + "Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.", |
16 | 55 | ]; |
17 | 56 |
|
18 | 57 | /** |
19 | | - * Bridge bl's own login state into the env vars the OpenAgentPack SDK reads for |
20 | | - * the bailian provider. bl persists `api_key` / `agentstudio_base_url` in |
21 | | - * `~/.bailian/config.json` (via `bl auth login`); mirror them onto |
22 | | - * `DASHSCOPE_API_KEY` / `BAILIAN_BASE_URL` so users don't have to re-declare the |
23 | | - * same credentials for `bl managed-agent *`. `workspace_id` is still bridged for configs |
24 | | - * that predate the base_url flow (the SDK accepts either). |
| 58 | + * Load the SDK's env-based credential sources (`.env`, `~/.agents/config.json`) |
| 59 | + * for non-bailian providers, then placeholder every credential var that is still |
| 60 | + * unset with "" so agents.yaml `${VAR}` interpolation never throws on a value the |
| 61 | + * pipeline is about to supply (bailian) or authoritatively reject ({@link |
| 62 | + * assertProviderCredentials}). Runs every call (no I/O cache) so a scrubbed |
| 63 | + * environment is repopulated if the same process resolves more than one config. |
| 64 | + */ |
| 65 | +export function prepareProviderEnv(): void { |
| 66 | + bootstrapRuntimeCredentialsSync(); |
| 67 | + for (const key of CREDENTIAL_ENV_KEYS) { |
| 68 | + if (process.env[key] === undefined) process.env[key] = ""; |
| 69 | + } |
| 70 | +} |
| 71 | + |
| 72 | +/** |
| 73 | + * Override the bailian provider block with bl's authStage-resolved credential, so |
| 74 | + * the bailian API key is authoritatively the CLI auth chain's — never a config |
| 75 | + * file bare-read or a stale env value. `api_key` is replaced unconditionally; |
| 76 | + * `base_url` / `workspace_id` are filled only when the block references them and |
| 77 | + * the interpolated value is empty (a literal in agents.yaml is respected). |
25 | 78 | * |
26 | | - * Lowest priority: only fills a var that is still unset, so anything already in |
27 | | - * the environment (shell export, `.env`, or `~/.agents/config.json` — which the |
28 | | - * SDK bootstrap has already applied) wins. Missing values are left alone; the |
29 | | - * SDK surfaces its own error when interpolation can't resolve, and non-bailian |
30 | | - * providers (claude/qoder/ark) don't need DashScope credentials at all. |
| 79 | + * `base_url` carries {@link AGENTSTUDIO_API_PATH} because the SDK appends resource |
| 80 | + * paths onto it verbatim; a value already ending in the suffix is left as-is. |
| 81 | + * With no credential (only under --dry-run: authStage hard-gates otherwise) the |
| 82 | + * bailian block is left untouched. Non-bailian blocks keep their interpolated |
| 83 | + * (env-sourced) values. |
31 | 84 | */ |
32 | | -export function bridgeBailianCredentials(): void { |
33 | | - const file = readConfigFile(); |
34 | | - if (!process.env.DASHSCOPE_API_KEY?.trim() && file.api_key) { |
35 | | - process.env.DASHSCOPE_API_KEY = file.api_key; |
| 85 | +export function injectProviderCredentials( |
| 86 | + providers: Record<string, unknown>, |
| 87 | + host: CredentialHost, |
| 88 | +): void { |
| 89 | + const bailian = providers.bailian; |
| 90 | + if (!bailian || typeof bailian !== "object") return; |
| 91 | + const block = bailian as Record<string, unknown>; |
| 92 | + |
| 93 | + const cred = host.client.exportApiCredential(); |
| 94 | + if (cred) { |
| 95 | + block.api_key = cred.token; |
| 96 | + if ("base_url" in block && !block.base_url) { |
| 97 | + block.base_url = cred.baseUrl.endsWith(AGENTSTUDIO_API_PATH) |
| 98 | + ? cred.baseUrl |
| 99 | + : `${cred.baseUrl}${AGENTSTUDIO_API_PATH}`; |
| 100 | + } |
36 | 101 | } |
37 | | - if (!process.env.BAILIAN_BASE_URL?.trim() && file.agentstudio_base_url) { |
38 | | - process.env.BAILIAN_BASE_URL = file.agentstudio_base_url; |
| 102 | + if ("workspace_id" in block && !block.workspace_id && host.settings.workspaceId) { |
| 103 | + block.workspace_id = host.settings.workspaceId; |
39 | 104 | } |
40 | | - if (!process.env.BAILIAN_WORKSPACE_ID?.trim() && file.workspace_id) { |
41 | | - process.env.BAILIAN_WORKSPACE_ID = file.workspace_id; |
| 105 | +} |
| 106 | + |
| 107 | +/** |
| 108 | + * Remove every credential var from `process.env` after interpolation has run and |
| 109 | + * bailian has been overridden in-memory. From here on the real credentials live |
| 110 | + * only in the config object (and, after `createProjectRuntime`, in each provider |
| 111 | + * adapter instance) — nothing persists in the environment for the process |
| 112 | + * lifetime or any child process. |
| 113 | + */ |
| 114 | +export function scrubCredentialEnv(): void { |
| 115 | + for (const key of CREDENTIAL_ENV_KEYS) { |
| 116 | + delete process.env[key]; |
42 | 117 | } |
43 | 118 | } |
44 | 119 |
|
45 | 120 | /** |
46 | | - * Lazily load `.env` and `~/.agents/config.json` into `process.env` so the |
47 | | - * OpenAgentPack SDK can resolve provider credentials (e.g. DASHSCOPE_API_KEY, |
48 | | - * BAILIAN_WORKSPACE_ID), then bridge bl's own config as a fallback. Safe to call |
49 | | - * repeatedly — only the first call does I/O. |
50 | | - * |
51 | | - * Effective precedence for bailian provider fields: |
52 | | - * ~/.agents/config.json > shell env > .env > ~/.bailian/config.json |
53 | | - * |
54 | | - * NOTE: agent commands declare `auth: "none"` and let the SDK own credential |
55 | | - * resolution. The bl-config bridge is a best-effort fallback; it never overrides |
56 | | - * a value the SDK bootstrap already resolved. |
| 121 | + * After injection, fail with a CLI-authoritative AUTH error if any configured |
| 122 | + * provider's `api_key` resolved empty (missing env var, or no bl login for |
| 123 | + * bailian). Replaces the SDK's raw `Environment variable '...' is not set` / |
| 124 | + * zod config error with a clean message plus a provider-specific hint. Validates |
| 125 | + * every declared provider, so a project is only runnable once all its providers' |
| 126 | + * keys are available. |
57 | 127 | */ |
58 | | -export function ensureCredentials(): void { |
59 | | - if (bootstrapped) return; |
60 | | - bootstrapped = true; |
61 | | - bootstrapRuntimeCredentialsSync(); |
62 | | - bridgeBailianCredentials(); |
| 128 | +export function assertProviderCredentials(providers: Record<string, unknown>): void { |
| 129 | + for (const [name, raw] of Object.entries(providers)) { |
| 130 | + if (!raw || typeof raw !== "object") continue; |
| 131 | + const block = raw as Record<string, unknown>; |
| 132 | + if (!("api_key" in block)) continue; |
| 133 | + const apiKey = block.api_key; |
| 134 | + if (typeof apiKey === "string" && apiKey.trim()) continue; |
| 135 | + throw new BailianError( |
| 136 | + `Provider '${name}' is configured but its API key is empty.`, |
| 137 | + ExitCode.AUTH, |
| 138 | + CREDENTIAL_HINTS[name] ?? `Provide credentials for provider '${name}'.`, |
| 139 | + ); |
| 140 | + } |
63 | 141 | } |
0 commit comments