From 0c90b88e08081cefcfbd21c3de89d4fef4856747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Tue, 26 May 2026 05:23:33 +0000 Subject: [PATCH 1/2] refactor(protocol,cli,agent): replace apiKeyEnv with apiKey (#528) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Breaking change: Store API keys directly in config.yaml instead of environment variable names. ## Changes ### @uncaged/workflow-protocol - Change ProviderConfig.apiKeyEnv: string → apiKey: string - Update README to reflect new type ### @uncaged/workflow-util-agent - extract.ts: Remove dotenv loading, use providerEntry.apiKey directly - storage.ts: Update normalizeProviders to validate apiKey field - Update error messages to reference apiKey instead of apiKeyEnv ### @uncaged/cli-workflow - setup.ts: Write actual API key to config.yaml, not .env - Remove apiKeyEnvName(), loadEnvFile(), saveEnvFile() functions - Remove getEnvPath() function - Update cmdSetup to not return envPath in result - Update README to reflect config.yaml stores API keys - Fix setup-validate.test.ts to not expect envPath in result ## Verification - ✅ bun run build passes - ✅ All tests pass (260/260 in cli-workflow, 55/55 in util-agent) - ✅ bun run check passes (only pre-existing warnings) Co-Authored-By: Claude Opus 4.6 --- packages/cli-workflow/README.md | 2 +- .../src/__tests__/setup-validate.test.ts | 6 +-- packages/cli-workflow/src/commands/setup.ts | 46 +------------------ packages/workflow-protocol/README.md | 2 +- packages/workflow-protocol/src/types.ts | 2 +- packages/workflow-util-agent/src/extract.ts | 10 ++-- packages/workflow-util-agent/src/storage.ts | 8 ++-- 7 files changed, 14 insertions(+), 62 deletions(-) diff --git a/packages/cli-workflow/README.md b/packages/cli-workflow/README.md index 2b9e2fd..92f7eaf 100644 --- a/packages/cli-workflow/README.md +++ b/packages/cli-workflow/README.md @@ -119,7 +119,7 @@ uwf setup --provider openai --base-url https://api.openai.com/v1 \ --api-key sk-... --model gpt-4o --agent hermes ``` -Config: `~/.uncaged/workflow/config.yaml`. API keys: `~/.uncaged/workflow/.env`. +Config: `~/.uncaged/workflow/config.yaml` (includes API keys). ### Skill diff --git a/packages/cli-workflow/src/__tests__/setup-validate.test.ts b/packages/cli-workflow/src/__tests__/setup-validate.test.ts index f96baa3..7221775 100644 --- a/packages/cli-workflow/src/__tests__/setup-validate.test.ts +++ b/packages/cli-workflow/src/__tests__/setup-validate.test.ts @@ -129,9 +129,8 @@ describe("cmdSetup with validation", () => { const result = await cmdSetup(setupArgs()); expect(result.validation).toEqual({ ok: true, value: undefined }); - // Config files should still be written + // Config file should still be written expect(result.configPath).toBeTruthy(); - expect(result.envPath).toBeTruthy(); }); test("includes validation failure — config still saved", async () => { @@ -143,8 +142,7 @@ describe("cmdSetup with validation", () => { expect(result.validation).toBeDefined(); expect((result.validation as { ok: boolean }).ok).toBe(false); - // Config files should still be written despite validation failure + // Config file should still be written despite validation failure expect(result.configPath).toBeTruthy(); - expect(result.envPath).toBeTruthy(); }); }); diff --git a/packages/cli-workflow/src/commands/setup.ts b/packages/cli-workflow/src/commands/setup.ts index 953a67e..99873e4 100644 --- a/packages/cli-workflow/src/commands/setup.ts +++ b/packages/cli-workflow/src/commands/setup.ts @@ -85,10 +85,6 @@ function getConfigPath(root: string): string { return join(root, "config.yaml"); } -function getEnvPath(root: string): string { - return join(root, ".env"); -} - /** * Load existing config.yaml or return empty structure. */ @@ -106,37 +102,6 @@ function loadExistingConfig(configPath: string): Record { return {}; } -/** - * Load existing .env as key=value map. - */ -function loadEnvFile(envPath: string): Record { - const env: Record = {}; - try { - if (existsSync(envPath)) { - for (const line of readFileSync(envPath, "utf8").split("\n")) { - const trimmed = line.trim(); - if (trimmed === "" || trimmed.startsWith("#")) continue; - const eq = trimmed.indexOf("="); - if (eq > 0) { - env[trimmed.slice(0, eq)] = trimmed.slice(eq + 1); - } - } - } - } catch { - // ignore - } - return env; -} - -function saveEnvFile(envPath: string, env: Record): void { - const lines = Object.entries(env).map(([k, v]) => `${k}=${v}`); - writeFileSync(envPath, `${lines.join("\n")}\n`, "utf8"); -} - -function apiKeyEnvName(providerName: string): string { - return `${providerName.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`; -} - // ────────────────────────────────────────────────────────────────────────────── // Extracted helpers — _discoverAgents // ────────────────────────────────────────────────────────────────────────────── @@ -397,8 +362,7 @@ function mergeConfig(existing: Record, args: SetupArgs): Record : {} ) as Record; - const envName = apiKeyEnvName(args.provider); - providers[args.provider] = { baseUrl: args.baseUrl, apiKeyEnv: envName }; + providers[args.provider] = { baseUrl: args.baseUrl, apiKey: args.apiKey }; const models = ( typeof existing.models === "object" && existing.models !== null @@ -437,25 +401,17 @@ export async function cmdSetup(args: SetupArgs): Promise mkdirSync(storageRoot, { recursive: true }); const configPath = getConfigPath(storageRoot); - const envPath = getEnvPath(storageRoot); const existing = loadExistingConfig(configPath); const merged = mergeConfig(existing, args); writeFileSync(configPath, stringify(merged, { indent: 2 }), "utf8"); - // Write API key to .env - const envName = apiKeyEnvName(args.provider); - const envData = loadEnvFile(envPath); - envData[envName] = args.apiKey; - saveEnvFile(envPath, envData); - // Validate model connectivity const validation = await validateModel(args.baseUrl, args.apiKey, args.model); return { configPath, - envPath, provider: args.provider, model: args.model, defaultAgent: merged.defaultAgent, diff --git a/packages/workflow-protocol/README.md b/packages/workflow-protocol/README.md index 466890b..3f0644f 100644 --- a/packages/workflow-protocol/README.md +++ b/packages/workflow-protocol/README.md @@ -100,7 +100,7 @@ type ProviderAlias = string; type ModelAlias = string; type AgentAlias = string; -type ProviderConfig = { baseUrl: string; apiKeyEnv: string }; +type ProviderConfig = { baseUrl: string; apiKey: string }; type ModelConfig = { provider: ProviderAlias; name: string; diff --git a/packages/workflow-protocol/src/types.ts b/packages/workflow-protocol/src/types.ts index aa6d5c1..65de96c 100644 --- a/packages/workflow-protocol/src/types.ts +++ b/packages/workflow-protocol/src/types.ts @@ -151,7 +151,7 @@ export type Scenario = string; export type ProviderConfig = { baseUrl: string; - apiKeyEnv: string; + apiKey: string; }; export type ModelConfig = { diff --git a/packages/workflow-util-agent/src/extract.ts b/packages/workflow-util-agent/src/extract.ts index 712046f..64807d2 100644 --- a/packages/workflow-util-agent/src/extract.ts +++ b/packages/workflow-util-agent/src/extract.ts @@ -1,8 +1,7 @@ import { getSchema, validate } from "@uncaged/json-cas"; import type { CasRef, ModelAlias, WorkflowConfig } from "@uncaged/workflow-protocol"; -import { config as loadDotenv } from "dotenv"; -import { createAgentStore, getEnvPath, resolveStorageRoot } from "./storage.js"; +import { createAgentStore, resolveStorageRoot } from "./storage.js"; export type ResolvedLlmProvider = { baseUrl: string; @@ -38,9 +37,9 @@ export function resolveModel(config: WorkflowConfig, alias: ModelAlias): Resolve if (providerEntry === undefined) { throw new Error(`unknown provider "${modelEntry.provider}" for model "${alias}"`); } - const apiKey = process.env[providerEntry.apiKeyEnv]; + const apiKey = providerEntry.apiKey; if (apiKey === undefined || apiKey === "") { - throw new Error(`missing API key env var: ${providerEntry.apiKeyEnv}`); + throw new Error(`missing API key for provider: ${modelEntry.provider}`); } return { baseUrl: providerEntry.baseUrl, @@ -130,7 +129,7 @@ export type ExtractResult = { /** * Call an OpenAI-compatible LLM to extract structured output matching outputSchema. - * Loads config.yaml and .env from the workflow storage root. + * Loads config.yaml from the workflow storage root. */ export async function extract( rawOutput: string, @@ -138,7 +137,6 @@ export async function extract( config: WorkflowConfig, ): Promise { const storageRoot = resolveStorageRoot(); - loadDotenv({ path: getEnvPath(storageRoot) }); const { store } = await createAgentStore(storageRoot); const schema = getSchema(store, outputSchema); diff --git a/packages/workflow-util-agent/src/storage.ts b/packages/workflow-util-agent/src/storage.ts index 8f1c934..7d94a40 100644 --- a/packages/workflow-util-agent/src/storage.ts +++ b/packages/workflow-util-agent/src/storage.ts @@ -84,11 +84,11 @@ function normalizeProviders(raw: unknown): Record throw new Error(`config.providers.${name} must be a mapping`); } const baseUrl = entry.baseUrl; - const apiKeyEnv = entry.apiKeyEnv; - if (typeof baseUrl !== "string" || typeof apiKeyEnv !== "string") { - throw new Error(`config.providers.${name} requires baseUrl and apiKeyEnv`); + const apiKey = entry.apiKey; + if (typeof baseUrl !== "string" || typeof apiKey !== "string") { + throw new Error(`config.providers.${name} requires baseUrl and apiKey`); } - providers[name] = { baseUrl, apiKeyEnv }; + providers[name] = { baseUrl, apiKey }; } return providers; } From 47a4268b9b5cc1631c22af9dba9e0cac7c9321d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Tue, 26 May 2026 05:34:49 +0000 Subject: [PATCH 2/2] docs: update all documentation to reflect apiKey refactoring (#528) Update all documentation files that contained outdated apiKeyEnv references to use the new apiKey approach. ## Changes - docs/architecture.md: Update config example to use apiKey field - docs/wf-stateless-design.md: Update config examples and type definitions to use apiKey instead of apiKeyEnv - docs/builtin-agent-research.md: Update ProviderConfig type definition and code examples All documentation now consistent with the code implementation. Co-Authored-By: Claude Opus 4.6 --- docs/architecture.md | 2 +- docs/builtin-agent-research.md | 4 ++-- docs/wf-stateless-design.md | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index df3cc9b..b4f9a38 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -391,7 +391,7 @@ Everything else is immutable CAS content. providers: openrouter: baseUrl: "https://openrouter.ai/api/v1" - apiKeyEnv: "OPENROUTER_API_KEY" + apiKey: "sk-..." models: sonnet: diff --git a/docs/builtin-agent-research.md b/docs/builtin-agent-research.md index 63690f5..a76ae7b 100644 --- a/docs/builtin-agent-research.md +++ b/docs/builtin-agent-research.md @@ -402,7 +402,7 @@ workflow 怎么配置和使用 model? ```136:160:packages/workflow-protocol/src/types.ts export type ProviderConfig = { baseUrl: string; - apiKeyEnv: string; + apiKey: string; }; export type ModelConfig = { @@ -429,7 +429,7 @@ export type WorkflowConfig = { export function resolveModel(config: WorkflowConfig, alias: ModelAlias): ResolvedLlmProvider { const modelEntry = config.models[alias]; const providerEntry = config.providers[modelEntry.provider]; - const apiKey = process.env[providerEntry.apiKeyEnv]; + const apiKey = providerEntry.apiKey; return { baseUrl: providerEntry.baseUrl, apiKey, model: modelEntry.name }; } ``` diff --git a/docs/wf-stateless-design.md b/docs/wf-stateless-design.md index 257891d..6a0b527 100644 --- a/docs/wf-stateless-design.md +++ b/docs/wf-stateless-design.md @@ -280,13 +280,13 @@ threads.yaml: { "01J7K9M2XNPQR5VWBCDF8G3H4T": "8FWKR3TN5V1QA" } providers: openai: baseUrl: "https://api.openai.com/v1" - apiKeyEnv: "OPENAI_API_KEY" + apiKey: "sk-..." anthropic: baseUrl: "https://api.anthropic.com/v1" - apiKeyEnv: "ANTHROPIC_API_KEY" + apiKey: "sk-ant-..." openrouter: baseUrl: "https://openrouter.ai/api/v1" - apiKeyEnv: "OPENROUTER_API_KEY" + apiKey: "sk-or-..." models: sonnet: @@ -465,7 +465,7 @@ type Scenario = string; // e.g. "extract" type ProviderConfig = { baseUrl: string; - apiKeyEnv: string; // env var name to read API key from + apiKey: string; // API key stored directly }; type ModelConfig = {