Public: anyone can read this; the crew can change it.
The forge's own env names and log lines are vendor-neutral, and the old names still read #12242
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/12127-the-forge-s-own-env-names-and-log-lines → main 10 commits fork
15 files
changed +120
−32
docs/REFERENCE.md +4
| @@ -603,3 +603,7 @@ A repository admin cuts a wave from two doors, both through `cutWaveFor` (`src/a | ||
| 603 | 603 | Cmd-K finds a repository by its name first (exact, then prefix; your own ahead); a repository with an app has an "Open the app of …" option under it, and Cmd/Ctrl+Enter on the repository opens the app (`docs/features/cmd-k-repositories.md`). |
| 604 | 604 | |
| 605 | 605 | In the terminal, the palette's **Your profile** (any signed-in person, never an agent; a guest is not offered it) shows username, display name and email; `n` sets the display name and `e` the email through `updateProfile`, the action behind Settings → Account. Refusals read as the page's do (`src/actions/profile_words.ts`, including one shared fallback for a refusal with no words of its own), and a changed email says a confirmation link was sent to it and that it is unverified until followed. |
| 606 | ||
| 607 | ## Agent runner settings | |
| 608 | ||
| 609 | `FORGE_AGENT_RUNNER` names the agent runner's binary (default `claude` on `PATH`) and `FORGE_AGENT_CONFIG_DIR` the directory it keeps its sessions in. The older `FORGE_CLAUDE` and `FORGE_CLAUDE_CONFIG_DIR` are read too; when both are set the new name wins. The forge settles the old names to the chosen value at start, so the app planner and child forges follow it. The runner's own variables (`ANTHROPIC_*`, `CLAUDE_CODE_*`) are passed through untouched. The log says `crew is up runner=<binary> version=<v>`. |
src/actions/apps.ts +2 −1
| @@ -6,6 +6,7 @@ | ||
| 6 | 6 | * the repository may look at it and open its environments. |
| 7 | 7 | */ |
| 8 | 8 | |
| 9 | import { runnerSettings } from "../crew/runner_env.ts"; | |
| 9 | 10 | import { getVariant } from "../core/variants.ts"; |
| 10 | 11 | import { pullDir } from "../core/pulls.ts"; |
| 11 | 12 | import { getIssue } from "../core/issues.ts"; |
| @@ -426,7 +427,7 @@ export async function newApp( | ||
| 426 | 427 | // costs at most `PLAN_TIMEOUT_MS` and never a wrong answer (crew/plan.ts): |
| 427 | 428 | // the repository, the app and production above have already started. |
| 428 | 429 | if (!agent && input.pickAgent && description !== "") { |
| 429 | const binary = process.env["FORGE_CLAUDE"] ?? "claude"; | |
| 430 | const binary = runnerSettings(process.env).binary; | |
| 430 | 431 | if (planningAvailable(binary)) { |
| 431 | 432 | const roster = listAllAgents(app, user.value); |
| 432 | 433 | const agents = roster.ok ? roster.value.filter((a) => a.roles.includes("builder")) : []; |
src/core/subscriptions.ts +2 −2
| @@ -28,7 +28,7 @@ import type { Database } from "bun:sqlite"; | ||
| 28 | 28 | import { id } from "../kernel/id.ts"; |
| 29 | 29 | import { err, ok, type Result } from "../kernel/result.ts"; |
| 30 | 30 | import { open, seal } from "../kernel/secret.ts"; |
| 31 | import { accountEnv, envNameOf, hasEnvCredential } from "../crew/runner_env.ts"; | |
| 31 | import { accountEnv, envNameOf, runnerCredentialInEnv } from "../crew/runner_env.ts"; | |
| 32 | 32 | import { limitValue } from "../settings/limits.ts"; |
| 33 | 33 | |
| 34 | 34 | export type SubscriptionKind = "oauth" | "api_key"; |
| @@ -712,7 +712,7 @@ export function crewHasCredential( | ||
| 712 | 712 | env: Record<string, string | undefined> = process.env, |
| 713 | 713 | ): boolean { |
| 714 | 714 | if (listSubscriptions(db).length > 0) return true; |
| 715 | return hasEnvCredential(env); | |
| 715 | return runnerCredentialInEnv(env); | |
| 716 | 716 | } |
| 717 | 717 | |
| 718 | 718 | /** What MCP `subscriptions` and `GET /api/v1/admin/subscriptions` list: each account, never its token. */ |
src/crew/conversation.ts +1 −1
| @@ -146,7 +146,7 @@ export const turnDir = (dataDir: string): string => { | ||
| 146 | 146 | }; |
| 147 | 147 | |
| 148 | 148 | /** |
| 149 | * Where Claude Code keeps conversation sessions: `FORGE_CLAUDE_CONFIG_DIR` | |
| 149 | * Where Claude Code keeps conversation sessions: `FORGE_AGENT_CONFIG_DIR` (or the older `FORGE_CLAUDE_CONFIG_DIR`, read through `runnerSettings`) | |
| 150 | 150 | * when the operator named one, else a directory of its own under the data |
| 151 | 151 | * dir, which outlives a release (the container's HOME does not). Owner-only: |
| 152 | 152 | * it holds what the people said. |
src/crew/dispatch.ts +5 −2
| @@ -203,18 +203,21 @@ const handles = new WeakMap<App, CrewHandle>(); | ||
| 203 | 203 | /** The crew serving this forge, if one was started. */ |
| 204 | 204 | export const crewFor = (app: App): CrewHandle | undefined => handles.get(app); |
| 205 | 205 | |
| 206 | export { runnerSettings } from "./runner_env.ts"; | |
| 207 | ||
| 206 | 208 | export async function startCrew( |
| 207 | 209 | app: App, |
| 208 | 210 | options: CrewOptions, |
| 209 | 211 | ): Promise<CrewHandle> { |
| 210 | 212 | const detected = await detectAgent(options.binary); |
| 211 | 213 | if (!detected.ok) |
| 212 | log.warn("claude not usable; agent runs will fail", { | |
| 214 | log.warn("agent runner not usable; agent runs will fail", { | |
| 213 | 215 | error: detected.error, |
| 214 | 216 | }); |
| 215 | 217 | else |
| 216 | 218 | log.info("crew is up", { |
| 217 | claude: detected.version, | |
| 219 | runner: options.binary, | |
| 220 | version: detected.version, | |
| 218 | 221 | concurrency: options.concurrency, |
| 219 | 222 | }); |
| 220 | 223 | // The shell sandbox is policy, not decoration: where the host cannot |
src/crew/runner_env.ts +28 −5
| @@ -58,11 +58,34 @@ export function ownEnv(env: Env = process.env): Record<string, string> { | ||
| 58 | 58 | return own; |
| 59 | 59 | } |
| 60 | 60 | |
| 61 | /** Whether the environment holds a model token or a signed-in Claude config. */ | |
| 62 | export function hasEnvCredential(env: Env): boolean { | |
| 63 | return ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN", "FORGE_CLAUDE_CONFIG_DIR"].some( | |
| 64 | (name) => (env[name] ?? "").trim() !== "", | |
| 65 | ); | |
| 61 | /** | |
| 62 | * The runner's own names: the forge's neutral `FORGE_AGENT_RUNNER` and | |
| 63 | * `FORGE_AGENT_CONFIG_DIR`, with the older `FORGE_CLAUDE` and | |
| 64 | * `FORGE_CLAUDE_CONFIG_DIR` read too; the new name wins. Nothing writes the | |
| 65 | * environment: every reader calls `runnerSettings`. | |
| 66 | */ | |
| 67 | export function runnerSettings(env: Env): { binary: string; configDir?: string } { | |
| 68 | const pick = (a: string, b: string): string | undefined => { | |
| 69 | const v = env[a]; | |
| 70 | return v !== undefined && v !== "" ? v : env[b] !== "" ? env[b] : undefined; | |
| 71 | }; | |
| 72 | const configDir = pick("FORGE_AGENT_CONFIG_DIR", "FORGE_CLAUDE_CONFIG_DIR"); | |
| 73 | return { | |
| 74 | binary: pick("FORGE_AGENT_RUNNER", "FORGE_CLAUDE") ?? "claude", | |
| 75 | ...(configDir === undefined ? {} : { configDir }), | |
| 76 | }; | |
| 77 | } | |
| 78 | ||
| 79 | /** The runner binary only when the operator named one (a child inherits that choice). */ | |
| 80 | export function namedRunner(env: Env): string | undefined { | |
| 81 | const named = (env["FORGE_AGENT_RUNNER"] ?? "") !== "" || (env["FORGE_CLAUDE"] ?? "") !== ""; | |
| 82 | return named ? runnerSettings(env).binary : undefined; | |
| 83 | } | |
| 84 | ||
| 85 | /** Whether the environment holds a model token or a signed-in runner config dir. */ | |
| 86 | export function runnerCredentialInEnv(env: Env): boolean { | |
| 87 | if (["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"].some((n) => (env[n] ?? "").trim() !== "")) return true; | |
| 88 | return (runnerSettings(env).configDir ?? "").trim() !== ""; | |
| 66 | 89 | } |
| 67 | 90 | |
| 68 | 91 | /** Points a run at a parent's door, paying with the credit token. */ |
src/main.ts +6 −7
| @@ -80,7 +80,7 @@ import { settleLandings } from "./core/landing.ts"; | ||
| 80 | 80 | import { DRAIN_LINGER_MAX_S, drainLingerMs, startDraining } from "./http/health.ts"; |
| 81 | 81 | import { takePresence, warmBehind } from "./apps/presence.ts"; |
| 82 | 82 | import { STALE_AFTER_MS } from "./runs/lease.ts"; |
| 83 | import { startCrew, sweepAssigned } from "./crew/dispatch.ts"; | |
| 83 | import { runnerSettings, startCrew, sweepAssigned } from "./crew/dispatch.ts"; | |
| 84 | 84 | import { configureConversation, resumeInterrupted } from "./crew/conversation.ts"; |
| 85 | 85 | import { CREW_OFF_IN_PREVIEW, crewOffInPreview } from "./crew/preview.ts"; |
| 86 | 86 | import { typeSafeDecider } from "./crew/decide.ts"; |
| @@ -595,6 +595,7 @@ const memorySampler = startMemorySampler(app); | ||
| 595 | 595 | void offerStartingBoards(app) |
| 596 | 596 | .then((slugs) => slugs.length > 0 && app.log.info("starting boards offered", { slugs: slugs.join(", ") })) |
| 597 | 597 | .catch((error) => app.log.warn("starting boards not offered", { error: String(error) })); |
| 598 | const runner = runnerSettings(process.env); | |
| 598 | 599 | const crewHeldOff = crewOffInPreview(); |
| 599 | 600 | if (appsOn && crewHeldOff) |
| 600 | 601 | app.log.info(CREW_OFF_IN_PREVIEW, { |
| @@ -604,7 +605,7 @@ const crew = appsOn && !crewHeldOff | ||
| 604 | 605 | ? await startCrew(app, { |
| 605 | 606 | forgeUrl, |
| 606 | 607 | localUrl: servers.url, |
| 607 | binary: process.env.FORGE_CLAUDE ?? "claude", | |
| 608 | binary: runner.binary, | |
| 608 | 609 | concurrency: positive(process.env.FORGE_CREW_CONCURRENCY, hosted ? 1 : Number(capacityDefault("crew.concurrency"))), |
| 609 | 610 | budgetUsd: Number(process.env.FORGE_CREW_BUDGET_USD) || 5, |
| 610 | 611 | runTimeoutMs: crewRunMs, |
| @@ -612,9 +613,7 @@ const crew = appsOn && !crewHeldOff | ||
| 612 | 613 | .split(",") |
| 613 | 614 | .map((h) => h.trim()) |
| 614 | 615 | .filter((h) => h !== ""), |
| 615 | ...(process.env.FORGE_CLAUDE_CONFIG_DIR === undefined | |
| 616 | ? {} | |
| 617 | : { configDir: process.env.FORGE_CLAUDE_CONFIG_DIR }), | |
| 616 | ...(runner.configDir === undefined ? {} : { configDir: runner.configDir }), | |
| 618 | 617 | }) |
| 619 | 618 | : undefined; |
| 620 | 619 | // A conversation turn the last process was in the middle of is resumed once |
| @@ -622,11 +621,11 @@ const crew = appsOn && !crewHeldOff | ||
| 622 | 621 | const conversationsStop = new AbortController(); |
| 623 | 622 | if (crew !== undefined) { |
| 624 | 623 | const conversation = { |
| 625 | binary: process.env.FORGE_CLAUDE ?? "claude", | |
| 624 | binary: runner.binary, | |
| 626 | 625 | localUrl: servers.url, |
| 627 | 626 | budgetUsd: Number(process.env.FORGE_CREW_BUDGET_USD) || 5, |
| 628 | 627 | timeoutMs: crewRunMs, |
| 629 | ...(process.env.FORGE_CLAUDE_CONFIG_DIR === undefined ? {} : { configDir: process.env.FORGE_CLAUDE_CONFIG_DIR }), | |
| 628 | ...(runner.configDir === undefined ? {} : { configDir: runner.configDir }), | |
| 630 | 629 | signal: conversationsStop.signal, |
| 631 | 630 | }; |
| 632 | 631 | configureConversation(app, conversation); |
src/spawn/cell.ts +4 −3
| @@ -8,6 +8,7 @@ | ||
| 8 | 8 | * (`spawn/placement.ts` `fallbackPlacement`), and the ledger says which. |
| 9 | 9 | */ |
| 10 | 10 | |
| 11 | import { namedRunner } from "../crew/runner_env.ts"; | |
| 11 | 12 | import { tmpdir } from "node:os"; |
| 12 | 13 | import { dirname, join } from "node:path"; |
| 13 | 14 | import { clientAddress } from "../http/peer.ts"; |
| @@ -179,7 +180,7 @@ export function cellEnv(input: { port: number; dataDir: string; url: string; par | ||
| 179 | 180 | ...(input.parentPort === undefined ? {} : { FORGE_PARENT_LOCAL: parentLocal(input.parentPort) }), |
| 180 | 181 | // Which Claude Code its crew runs: the parent's own choice, a path and not |
| 181 | 182 | // a secret, so a test can put a stand-in in the cell (#12001). |
| 182 | ...(input.claude === undefined || input.claude === "" ? {} : { FORGE_CLAUDE: input.claude }), | |
| 183 | ...(input.claude === undefined || input.claude === "" ? {} : { FORGE_AGENT_RUNNER: input.claude }), | |
| 183 | 184 | }); |
| 184 | 185 | delete env["USER"]; |
| 185 | 186 | delete env["LOGNAME"]; |
| @@ -187,12 +188,12 @@ export function cellEnv(input: { port: number; dataDir: string; url: string; par | ||
| 187 | 188 | } |
| 188 | 189 | |
| 189 | 190 | /** |
| 190 | * What a child is started with: `cellEnv`, plus the parent's `FORGE_CLAUDE` | |
| 191 | * What a child is started with: `cellEnv`, plus the parent's runner (`FORGE_AGENT_RUNNER`, or the older `FORGE_CLAUDE`) | |
| 191 | 192 | * when it has one. The child's crew reads it (`main.ts`), so a parent run with |
| 192 | 193 | * Claude Code somewhere off `PATH` has children that find it too. |
| 193 | 194 | */ |
| 194 | 195 | export function childEnvFor(input: Parameters<typeof cellEnv>[0], parentEnv: Record<string, string | undefined> = process.env): Record<string, string> { |
| 195 | const claude = parentEnv["FORGE_CLAUDE"]; | |
| 196 | const claude = namedRunner(parentEnv); | |
| 196 | 197 | return cellEnv({ ...input, ...(claude === undefined ? {} : { claude }) }); |
| 197 | 198 | } |
| 198 | 199 |
src/web/pages/apps.ts +2 −1
| @@ -12,6 +12,7 @@ | ||
| 12 | 12 | * 404 for everyone. |
| 13 | 13 | */ |
| 14 | 14 | |
| 15 | import { runnerSettings } from "../../crew/runner_env.ts"; | |
| 15 | 16 | import { clearCache } from "../../actions/build_cache.ts"; |
| 16 | 17 | import { returnTo, withReturn } from "../return_to.ts"; |
| 17 | 18 | import { crewKeyCard } from "../studio/crew_key.ts"; |
| @@ -299,7 +300,7 @@ const PICK_AGENT = "__pick__"; | ||
| 299 | 300 | /** Whether the form can offer "pick for me": the planner is on the path (timed as `planner`). */ |
| 300 | 301 | function plannerOffered(ctx: Ctx): boolean { |
| 301 | 302 | return timePhase(ctx.request, "planner", () => |
| 302 | planningAvailable(process.env["FORGE_CLAUDE"] ?? "claude"), | |
| 303 | planningAvailable(runnerSettings(process.env).binary), | |
| 303 | 304 | ); |
| 304 | 305 | } |
| 305 | 306 |
src/web/pages/crew.ts +1 −1
| @@ -140,7 +140,7 @@ export function crewReadiness(ctx: Ctx): Html { | ||
| 140 | 140 | if (crew === undefined && isHostedChild(ctx.app)) |
| 141 | 141 | return line("No runner", html`This forge is hosted inside another forge's process, which has no crew runner for it: assigned issues will wait.`); |
| 142 | 142 | if (crew === undefined) |
| 143 | return line("No runner", html`No crew runner is up on this forge, so assigned issues will wait: start it with apps on and Claude Code on <span class="mono">PATH</span> or at <span class="mono">FORGE_CLAUDE</span>.`); | |
| 143 | return line("No runner", html`No crew runner is up on this forge, so assigned issues will wait: start it with apps on and Claude Code on <span class="mono">PATH</span> or at <span class="mono">FORGE_AGENT_RUNNER</span>.`); | |
| 144 | 144 | if (!crew.ready) |
| 145 | 145 | return line("Cannot run", html`The crew cannot run: ${crew.detail}. Nothing picks up agents' work until this is fixed.`); |
| 146 | 146 | const funded = fundingOf(ctx.app.config.dataDir); |
test/runner-env-confined.test.ts +3 −3
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | import { expect, test } from "bun:test"; |
| 2 | 2 | import { Glob } from "bun"; |
| 3 | import { accountEnv, hasEnvCredential, lentEnv, oneShotEnv, ownEnv, upstreamOf } from "../src/crew/runner_env.ts"; | |
| 3 | import { accountEnv, runnerCredentialInEnv, lentEnv, oneShotEnv, ownEnv, upstreamOf } from "../src/crew/runner_env.ts"; | |
| 4 | 4 | |
| 5 | 5 | test("only runner_env.ts names a vendor variable in src", async () => { |
| 6 | 6 | const re = /\b(ANTHROPIC_[A-Z_]+|CLAUDE_CODE_[A-Z_]+)\b/; |
| @@ -26,8 +26,8 @@ test("runner_env builds each environment", () => { | ||
| 26 | 26 | expect(lentEnv("http://x", "k")).toEqual({ ANTHROPIC_BASE_URL: "http://x", ANTHROPIC_API_KEY: "k" }); |
| 27 | 27 | expect(ownEnv({ ANTHROPIC_API_KEY: "a", PATH: "/bin" })).toEqual({ ANTHROPIC_API_KEY: "a" }); |
| 28 | 28 | expect(oneShotEnv({ CLAUDE_CODE_OAUTH_TOKEN: "o", ANTHROPIC_BASE_URL: "u" })).toEqual({ CLAUDE_CODE_OAUTH_TOKEN: "o" }); |
| 29 | expect(hasEnvCredential({})).toBe(false); | |
| 30 | expect(hasEnvCredential({ FORGE_CLAUDE_CONFIG_DIR: "/x" })).toBe(true); | |
| 29 | expect(runnerCredentialInEnv({})).toBe(false); | |
| 30 | expect(runnerCredentialInEnv({ FORGE_CLAUDE_CONFIG_DIR: "/x" })).toBe(true); | |
| 31 | 31 | expect(upstreamOf({})).toBeUndefined(); |
| 32 | 32 | expect(upstreamOf({ ANTHROPIC_API_KEY: "k", ANTHROPIC_BASE_URL: "https://g/" })).toEqual({ |
| 33 | 33 | base: "https://g", |
test/runner-env-names.test.ts added +53
| @@ -0,0 +1,53 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { runnerSettings } from "../src/crew/dispatch.ts"; | |
| 3 | import { namedRunner, runnerCredentialInEnv } from "../src/crew/runner_env.ts"; | |
| 4 | import { childEnvFor } from "../src/spawn/cell.ts"; | |
| 5 | import { planningAvailable } from "../src/crew/plan.ts"; | |
| 6 | ||
| 7 | test("FORGE_AGENT_RUNNER names the binary", () => { | |
| 8 | expect(runnerSettings({ FORGE_AGENT_RUNNER: "/opt/r" }).binary).toBe("/opt/r"); | |
| 9 | }); | |
| 10 | test("the old FORGE_CLAUDE still names the binary", () => { | |
| 11 | expect(runnerSettings({ FORGE_CLAUDE: "/opt/c" }).binary).toBe("/opt/c"); | |
| 12 | }); | |
| 13 | test("the new binary name wins; default is claude", () => { | |
| 14 | expect(runnerSettings({ FORGE_AGENT_RUNNER: "/n", FORGE_CLAUDE: "/o" }).binary).toBe("/n"); | |
| 15 | expect(runnerSettings({}).binary).toBe("claude"); | |
| 16 | }); | |
| 17 | test("config dir: new name wins, old still read", () => { | |
| 18 | expect(runnerSettings({ FORGE_AGENT_CONFIG_DIR: "/n" }).configDir).toBe("/n"); | |
| 19 | expect(runnerSettings({ FORGE_CLAUDE_CONFIG_DIR: "/o" }).configDir).toBe("/o"); | |
| 20 | expect(runnerSettings({ FORGE_AGENT_CONFIG_DIR: "/n", FORGE_CLAUDE_CONFIG_DIR: "/o" }).configDir).toBe("/n"); | |
| 21 | expect(runnerSettings({}).configDir).toBeUndefined(); | |
| 22 | }); | |
| 23 | test("nothing writes the environment: reading leaves it as it was", () => { | |
| 24 | const env: Record<string, string | undefined> = { FORGE_AGENT_RUNNER: "/n", FORGE_AGENT_CONFIG_DIR: "/d" }; | |
| 25 | runnerSettings(env); | |
| 26 | expect(env).toEqual({ FORGE_AGENT_RUNNER: "/n", FORGE_AGENT_CONFIG_DIR: "/d" }); | |
| 27 | }); | |
| 28 | test("a child is handed the neutral name, from either parent name", () => { | |
| 29 | const base = { port: 1, dataDir: "/d", url: "http://x" }; | |
| 30 | expect(childEnvFor(base, { FORGE_AGENT_RUNNER: "/n" })["FORGE_AGENT_RUNNER"]).toBe("/n"); | |
| 31 | expect(childEnvFor(base, { FORGE_CLAUDE: "/o" })["FORGE_AGENT_RUNNER"]).toBe("/o"); | |
| 32 | expect(childEnvFor(base, { FORGE_CLAUDE: "/o" })["FORGE_CLAUDE"]).toBeUndefined(); | |
| 33 | expect(childEnvFor(base, {})["FORGE_AGENT_RUNNER"]).toBeUndefined(); | |
| 34 | expect(namedRunner({})).toBeUndefined(); | |
| 35 | }); | |
| 36 | test("the credential check follows the neutral config dir", () => { | |
| 37 | expect(runnerCredentialInEnv({ FORGE_AGENT_CONFIG_DIR: "/d" })).toBe(true); | |
| 38 | expect(runnerCredentialInEnv({ FORGE_CLAUDE_CONFIG_DIR: "/d" })).toBe(true); | |
| 39 | expect(runnerCredentialInEnv({})).toBe(false); | |
| 40 | }); | |
| 41 | test("app planning follows FORGE_AGENT_RUNNER in process.env", () => { | |
| 42 | const before = { n: process.env["FORGE_AGENT_RUNNER"], o: process.env["FORGE_CLAUDE"] }; | |
| 43 | try { | |
| 44 | delete process.env["FORGE_CLAUDE"]; | |
| 45 | process.env["FORGE_AGENT_RUNNER"] = "/no/such/runner"; | |
| 46 | expect(planningAvailable(runnerSettings(process.env).binary)).toBe(false); | |
| 47 | process.env["FORGE_AGENT_RUNNER"] = "/bin/sh"; | |
| 48 | expect(planningAvailable(runnerSettings(process.env).binary)).toBe(true); | |
| 49 | } finally { | |
| 50 | for (const [k, v] of [["FORGE_AGENT_RUNNER", before.n], ["FORGE_CLAUDE", before.o]] as const) | |
| 51 | v === undefined ? delete process.env[k] : (process.env[k] = v); | |
| 52 | } | |
| 53 | }); |
test/spawn-cell.test.ts +6 −6
| @@ -376,14 +376,14 @@ describe("a child reaching its parent from inside its cell", () => { | ||
| 376 | 376 | } |
| 377 | 377 | }); |
| 378 | 378 | |
| 379 | test("cellEnv carries FORGE_CLAUDE only when the parent names one (#12001)", () => { | |
| 379 | test("cellEnv carries FORGE_AGENT_RUNNER only when the parent names one (#12001)", () => { | |
| 380 | 380 | const base = { port: 4100, dataDir: "/d/c", url: "https://kid--f.test" }; |
| 381 | expect(cellEnv({ ...base, claude: "/d/c/build-claude" })["FORGE_CLAUDE"]).toBe("/d/c/build-claude"); | |
| 382 | expect(cellEnv(base)["FORGE_CLAUDE"]).toBeUndefined(); | |
| 383 | expect(cellEnv({ ...base, claude: "" })["FORGE_CLAUDE"]).toBeUndefined(); | |
| 381 | expect(cellEnv({ ...base, claude: "/d/c/build-claude" })["FORGE_AGENT_RUNNER"]).toBe("/d/c/build-claude"); | |
| 382 | expect(cellEnv(base)["FORGE_AGENT_RUNNER"]).toBeUndefined(); | |
| 383 | expect(cellEnv({ ...base, claude: "" })["FORGE_AGENT_RUNNER"]).toBeUndefined(); | |
| 384 | 384 | // The call site `startOn` uses: the parent's own environment decides. |
| 385 | expect(childEnvFor(base, { FORGE_CLAUDE: "/opt/claude" })["FORGE_CLAUDE"]).toBe("/opt/claude"); | |
| 386 | expect(childEnvFor(base, {})["FORGE_CLAUDE"]).toBeUndefined(); | |
| 385 | expect(childEnvFor(base, { FORGE_CLAUDE: "/opt/claude" })["FORGE_AGENT_RUNNER"]).toBe("/opt/claude"); | |
| 386 | expect(childEnvFor(base, {})["FORGE_AGENT_RUNNER"]).toBeUndefined(); | |
| 387 | 387 | }); |
| 388 | 388 | |
| 389 | 389 | test("a heartbeat goes to the relayed port when one is named, and to the public address otherwise", async () => { |
test/spawn-child-apps.test.ts +1
| @@ -249,6 +249,7 @@ describe("a spawned child's apps", () => { | ||
| 249 | 249 | |
| 250 | 250 | test("a credential in the environment or a sealed account means the crew can run, and the note goes", async () => { |
| 251 | 251 | expect(crewHasCredential(child.db, {})).toBe(false); |
| 252 | expect(crewHasCredential(child.db, { FORGE_AGENT_CONFIG_DIR: "/d" })).toBe(true); | |
| 252 | 253 | expect(crewHasCredential(child.db, { ANTHROPIC_API_KEY: "sk-ant-x" })).toBe( |
| 253 | 254 | true, |
| 254 | 255 | ); |
test/web-apps.test.ts +2
| @@ -759,6 +759,8 @@ describe("apps, runs and the crew on the web", () => { | ||
| 759 | 759 | const page = await bodyOf(alice, "/settings/crew"); |
| 760 | 760 | expect(page).toContain("No agents yet"); |
| 761 | 761 | expect(page).toContain("No crew runner is up"); |
| 762 | expect(page).toContain("FORGE_AGENT_RUNNER"); | |
| 763 | expect(page).not.toContain("FORGE_CLAUDE"); | |
| 762 | 764 | expect(page).toContain("<h2>New agent</h2>"); |
| 763 | 765 | expect(await bodyOf(alice, "/settings")).toContain('href="/settings/crew"'); |
| 764 | 766 |
new above
offline