diff --git a/packages/cli-workflow/src/skill.ts b/packages/cli-workflow/src/skill.ts index 0550df7..6b82c5c 100644 --- a/packages/cli-workflow/src/skill.ts +++ b/packages/cli-workflow/src/skill.ts @@ -301,28 +301,26 @@ function createLazyAdapter(): AdapterFn { } \`\`\` -### Agent CLI paths: use optionalEnv with defaults +### Agent CLI paths: use env() with absolute path defaults -When binding agent adapters (cursor-agent, hermes, etc.), **always use \`optionalEnv\` with a sensible default** — never \`requireEnv\`. The worker process may start without the expected env vars, causing a silent crash. +Every env var in a bundle must have a sensible default — bundles must run without any env vars set. Use \`env(name, fallback)\` from \`@uncaged/workflow-util\`. Discover the correct CLI path yourself (e.g. \`which cursor-agent\`, \`which hermes\`) and hardcode it as the fallback: \`\`\`typescript -// ❌ WRONG — worker crash if env var missing, thread silently fails with 0 steps +import { env } from "@uncaged/workflow-util"; + +// ❌ WRONG — requireEnv and optionalEnv no longer exist const adapter = createCursorAgent({ command: requireEnv("WORKFLOW_CURSOR_COMMAND", "set it"), ... }); -// ❌ WRONG — bare command name fails absolute-path validation +// ✅ CORRECT — env var is an override, fallback is the discovered absolute path const adapter = createCursorAgent({ - command: optionalEnv("WORKFLOW_CURSOR_COMMAND") ?? "cursor-agent", - ... -}); - -// ✅ CORRECT — use \`which cursor-agent\` to find the path, then write it in -const adapter = createCursorAgent({ - command: optionalEnv("WORKFLOW_CURSOR_COMMAND", "/home/you/.local/bin/cursor-agent"), + command: env("WORKFLOW_CURSOR_COMMAND", "/home/you/.local/bin/cursor-agent"), + model: env("WORKFLOW_CURSOR_MODEL", "auto"), + timeout: Number(env("WORKFLOW_CURSOR_TIMEOUT", "300000")), ... }); \`\`\` diff --git a/packages/workflow-template-develop/bundle-entry.ts b/packages/workflow-template-develop/bundle-entry.ts index c5a1a6a..f268df6 100644 --- a/packages/workflow-template-develop/bundle-entry.ts +++ b/packages/workflow-template-develop/bundle-entry.ts @@ -7,24 +7,20 @@ import { createCursorAgent } from "@uncaged/workflow-agent-cursor"; import { createHermesAgent } from "@uncaged/workflow-agent-hermes"; import { createWorkflow } from "@uncaged/workflow-runtime"; -import { optionalEnv } from "@uncaged/workflow-util"; +import { env } from "@uncaged/workflow-util"; import { buildDevelopDescriptor, developWorkflowDefinition } from "./src/index.js"; const cursorAdapter = createCursorAgent({ - command: optionalEnv("WORKFLOW_CURSOR_COMMAND", "/home/azureuser/.local/bin/cursor-agent"), - model: optionalEnv("WORKFLOW_CURSOR_MODEL"), - timeout: optionalEnv("WORKFLOW_CURSOR_TIMEOUT") - ? Number(optionalEnv("WORKFLOW_CURSOR_TIMEOUT")) - : 0, + command: env("WORKFLOW_CURSOR_COMMAND", "/home/azureuser/.local/bin/cursor-agent"), + model: env("WORKFLOW_CURSOR_MODEL", "auto"), + timeout: Number(env("WORKFLOW_CURSOR_TIMEOUT", "0")), workspace: null, }); const hermesAdapter = createHermesAgent({ - command: optionalEnv("WORKFLOW_HERMES_COMMAND", "/home/azureuser/.local/bin/hermes"), - model: optionalEnv("WORKFLOW_HERMES_MODEL"), - timeout: optionalEnv("WORKFLOW_HERMES_TIMEOUT") - ? Number(optionalEnv("WORKFLOW_HERMES_TIMEOUT")) - : null, + command: env("WORKFLOW_HERMES_COMMAND", "/home/azureuser/.local/bin/hermes"), + model: env("WORKFLOW_HERMES_MODEL", "") || null, + timeout: Number(env("WORKFLOW_HERMES_TIMEOUT", "0")) || null, }); const wf = createWorkflow(developWorkflowDefinition, { diff --git a/packages/workflow-template-solve-issue/bundle-entry.ts b/packages/workflow-template-solve-issue/bundle-entry.ts index 29e70b5..b951672 100644 --- a/packages/workflow-template-solve-issue/bundle-entry.ts +++ b/packages/workflow-template-solve-issue/bundle-entry.ts @@ -7,14 +7,13 @@ import { createHermesAgent } from "@uncaged/workflow-agent-hermes"; import { workflowAdapter } from "@uncaged/workflow-execute"; import { createWorkflow } from "@uncaged/workflow-runtime"; -import { optionalEnv } from "@uncaged/workflow-util"; +import { env } from "@uncaged/workflow-util"; import { buildSolveIssueDescriptor, solveIssueWorkflowDefinition } from "./src/index.js"; const adapter = createHermesAgent({ - model: optionalEnv("WORKFLOW_HERMES_MODEL"), - timeout: optionalEnv("WORKFLOW_HERMES_TIMEOUT") - ? Number(optionalEnv("WORKFLOW_HERMES_TIMEOUT")) - : null, + command: env("WORKFLOW_HERMES_COMMAND", "/home/azureuser/.local/bin/hermes"), + model: env("WORKFLOW_HERMES_MODEL", "") || null, + timeout: Number(env("WORKFLOW_HERMES_TIMEOUT", "0")) || null, }); const wf = createWorkflow(solveIssueWorkflowDefinition, { diff --git a/packages/workflow-util/__tests__/env.test.ts b/packages/workflow-util/__tests__/env.test.ts index 0d72fb8..dd2f9ac 100644 --- a/packages/workflow-util/__tests__/env.test.ts +++ b/packages/workflow-util/__tests__/env.test.ts @@ -1,44 +1,20 @@ -import { describe, expect, test } from "bun:test"; -import { optionalEnv, requireEnv } from "../src/env.js"; +import { describe, expect, it } from "bun:test"; +import { env } from "../src/env.js"; -describe("requireEnv", () => { - test("returns value when set", () => { - process.env.TEST_REQ = "hello"; - expect(requireEnv("TEST_REQ", "missing")).toBe("hello"); - delete process.env.TEST_REQ; +describe("env", () => { + it("returns env value when set", () => { + process.env.TEST_ENV_SET = "hello"; + expect(env("TEST_ENV_SET", "default")).toBe("hello"); + delete process.env.TEST_ENV_SET; }); - test("throws with message when missing", () => { - expect(() => requireEnv("TEST_MISSING_XYZ", "need this")).toThrow("need this"); + it("returns fallback when missing", () => { + expect(env("TEST_ENV_MISSING_XYZ", "fallback")).toBe("fallback"); }); - test("throws when empty string", () => { - process.env.TEST_EMPTY = ""; - expect(() => requireEnv("TEST_EMPTY", "cannot be empty")).toThrow("cannot be empty"); - delete process.env.TEST_EMPTY; - }); -}); - -describe("optionalEnv", () => { - test("returns value when set", () => { - process.env.TEST_OPT = "world"; - expect(optionalEnv("TEST_OPT")).toBe("world"); - expect(optionalEnv("TEST_OPT", "default")).toBe("world"); - delete process.env.TEST_OPT; - }); - - test("returns null when missing and no fallback", () => { - expect(optionalEnv("TEST_MISSING_ABC")).toBeNull(); - }); - - test("returns fallback when missing", () => { - expect(optionalEnv("TEST_MISSING_ABC", "fallback")).toBe("fallback"); - }); - - test("returns fallback when empty string", () => { - process.env.TEST_EMPTY2 = ""; - expect(optionalEnv("TEST_EMPTY2", "fb")).toBe("fb"); - expect(optionalEnv("TEST_EMPTY2")).toBeNull(); - delete process.env.TEST_EMPTY2; + it("returns fallback when empty", () => { + process.env.TEST_ENV_EMPTY = ""; + expect(env("TEST_ENV_EMPTY", "fb")).toBe("fb"); + delete process.env.TEST_ENV_EMPTY; }); }); diff --git a/packages/workflow-util/src/env.ts b/packages/workflow-util/src/env.ts index 22ff527..c29bf1e 100644 --- a/packages/workflow-util/src/env.ts +++ b/packages/workflow-util/src/env.ts @@ -1,23 +1,14 @@ /** - * Read a required environment variable. Throws with `message` if missing or empty. + * Read an environment variable with a required fallback default. + * Returns the env value if set and non-empty, otherwise returns `fallback`. + * + * Every env var in a bundle must have a sensible default — bundles must run + * without any env vars set. Env vars are overrides, not requirements. */ -export function requireEnv(name: string, message: string): string { +export function env(name: string, fallback: string): string { const value = process.env[name]; if (value === undefined || value === "") { - throw new Error(message); - } - return value; -} - -/** - * Read an optional environment variable. Returns `fallback` if missing or empty. - */ -export function optionalEnv(name: string, fallback: string): string; -export function optionalEnv(name: string): string | null; -export function optionalEnv(name: string, fallback?: string): string | null { - const value = process.env[name]; - if (value === undefined || value === "") { - return fallback ?? null; + return fallback; } return value; } diff --git a/packages/workflow-util/src/index.ts b/packages/workflow-util/src/index.ts index 4fdb736..ea30c37 100644 --- a/packages/workflow-util/src/index.ts +++ b/packages/workflow-util/src/index.ts @@ -6,7 +6,7 @@ export { encodeCrockfordBase32Bits, encodeUint64AsCrockford, } from "./base32.js"; -export { optionalEnv, requireEnv } from "./env.js"; +export { env } from "./env.js"; export { createLogger } from "./logger.js"; export { mergeRefsWithContentHash, normalizeRefsField } from "./refs-field.js"; export { getDefaultWorkflowStorageRoot, getGlobalCasDir } from "./storage-root.js";