From c6313e5a6c3ba4d6ab8c1816650b6968b85ac8ee Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Fri, 11 Sep 2026 15:51:07 -0700 Subject: [PATCH] Pass machine environment variables to repository setup --- .../src/command-handlers/environment.ts | 1 + apps/host-daemon/src/runtime-manager.ts | 3 + .../test/command/command-router.test.ts | 1 + .../test/command/environment-dispatch.test.ts | 68 +++++++- .../server/src/services/hosts/live-command.ts | 21 ++- .../services/threads/thread-create-helpers.ts | 1 + apps/server/test/hosts/live-command.test.ts | 147 +++++++++++++++++- .../internal-session-protocol-version.test.ts | 73 +++++---- docs/configuration.md | 7 +- packages/host-daemon-contract/src/commands.ts | 1 + packages/host-daemon-contract/src/protocol.ts | 2 +- .../test/contract.test.ts | 15 +- .../src/templates/bb-guide-machines.md | 4 + .../skills/bb-cli/references/configuration.md | 4 + .../environment-attach-recovery.test.ts | 3 + 15 files changed, 309 insertions(+), 42 deletions(-) diff --git a/apps/host-daemon/src/command-handlers/environment.ts b/apps/host-daemon/src/command-handlers/environment.ts index 23e34e4df01..6dfead558cf 100644 --- a/apps/host-daemon/src/command-handlers/environment.ts +++ b/apps/host-daemon/src/command-handlers/environment.ts @@ -44,6 +44,7 @@ export async function provisionEnvironment( environmentId: command.environmentId, provision: toProvisionWorkspaceOptions(command, onProgress), setupScriptTimeoutMs: command.setupScriptTimeoutMs, + setupContributedEnv: command.contributedEnv, }); const [branchName, resolvedDefaultBranch] = await Promise.all([ diff --git a/apps/host-daemon/src/runtime-manager.ts b/apps/host-daemon/src/runtime-manager.ts index 422f5e6943d..2f1bfb38532 100644 --- a/apps/host-daemon/src/runtime-manager.ts +++ b/apps/host-daemon/src/runtime-manager.ts @@ -17,6 +17,7 @@ import type { import { threadScope, turnScope } from "@bb/domain"; import type { HostDaemonActiveThread, + HostDaemonContributedEnvEntry, HostDaemonEnvironmentChange, HostDaemonLoadedEnvironment, HostDaemonInjectedSkillSource, @@ -142,6 +143,7 @@ export interface EnsureEnvironmentArgs { environmentId: string; injectedSkillSources?: readonly HostDaemonInjectedSkillSource[]; setupScriptTimeoutMs?: number | null; + setupContributedEnv?: readonly HostDaemonContributedEnvEntry[]; targetThreadId?: string; workspacePath?: string; provision?: ProvisionWorkspaceArgs; @@ -1216,6 +1218,7 @@ export class RuntimeManager { await runSetupScript({ workspacePath: provision.path, timeoutMs: args.setupScriptTimeoutMs, + contributedEnv: args.setupContributedEnv, shellPath: this.getShellEnv().PATH, signal: args.provisionSignal, onProgress: provision.onProgress, diff --git a/apps/host-daemon/test/command/command-router.test.ts b/apps/host-daemon/test/command/command-router.test.ts index cbedce06ba8..8036dd00597 100644 --- a/apps/host-daemon/test/command/command-router.test.ts +++ b/apps/host-daemon/test/command/command-router.test.ts @@ -182,6 +182,7 @@ function textPromptInput(text: string): TextPromptInput { function createEnvironmentProvisionCommand(): EnvironmentProvisionCommand { return { type: "environment.attach", + contributedEnv: [], environmentId: "env-router", initiator: null, path: "/tmp/env-router", diff --git a/apps/host-daemon/test/command/environment-dispatch.test.ts b/apps/host-daemon/test/command/environment-dispatch.test.ts index 0ac110cb73e..f88e3761a26 100644 --- a/apps/host-daemon/test/command/environment-dispatch.test.ts +++ b/apps/host-daemon/test/command/environment-dispatch.test.ts @@ -31,6 +31,7 @@ describe("environment command dispatch", () => { const result = await dispatchCommand( { type: "environment.attach", + contributedEnv: [], environmentId: "env-unmanaged", initiator: null, path: sourcePath, @@ -60,13 +61,21 @@ describe("environment command dispatch", () => { const markerPath = `${sourcePath}/setup-marker`; await fs.writeFile( `${sourcePath}/.bb-env-setup.sh`, - `printf '%s' ready > '${markerPath}'\n`, + `printf '%s' \"$SETUP_VALUE\" > '${markerPath}'\n`, ); const emittedEvents: EventSinkInput[] = []; await dispatchCommand( { type: "environment.attach", + contributedEnv: [ + { + name: "SETUP_VALUE", + value: "ready", + source: { plugin: "fixture" }, + reason: "test", + }, + ], environmentId: "env-provider-owned", initiator: { threadId: "thr-provider-owned", @@ -109,6 +118,7 @@ describe("environment command dispatch", () => { dispatchCommand( { type: "environment.attach", + contributedEnv: [], environmentId: "env-setup-failure", initiator: null, path: sourcePath, @@ -163,6 +173,7 @@ describe("environment command dispatch", () => { const provision = dispatchCommand( { type: "environment.attach", + contributedEnv: [], environmentId: "env-cancel", initiator: null, path: "/tmp/cancelled", @@ -216,6 +227,7 @@ describe("environment command dispatch", () => { const provision = dispatchCommand( { type: "environment.attach", + contributedEnv: [], environmentId: "env-cancel-no-settle", initiator: null, path: "/tmp/cancelled-no-settle", @@ -251,6 +263,7 @@ describe("environment command dispatch", () => { await dispatchCommand( { type: "environment.attach", + contributedEnv: [], environmentId: "env-stream", initiator: { threadId: "thr-initiator", @@ -331,6 +344,7 @@ describe("environment command dispatch", () => { await dispatchCommand( { type: "environment.attach", + contributedEnv: [], environmentId: "env-batched-progress", initiator: { threadId: "thr-batched-progress", @@ -389,6 +403,7 @@ describe("environment command dispatch", () => { dispatchCommand( { type: "environment.attach", + contributedEnv: [], environmentId: "env-failure", initiator: { threadId: "thr-failure", @@ -428,6 +443,7 @@ describe("environment command dispatch", () => { await dispatchCommand( { type: "environment.attach", + contributedEnv: [], environmentId: "env-idempotent", initiator: null, path: sourcePath, @@ -439,6 +455,7 @@ describe("environment command dispatch", () => { const result = await dispatchCommand( { type: "environment.attach", + contributedEnv: [], environmentId: "env-idempotent", initiator: { threadId: "thr-second", @@ -471,3 +488,52 @@ describe("environment command dispatch", () => { ]); }); }); + +it("cancels setup with contributions even when another attach is waiting", async () => { + const sourcePath = await makeTempDir("bb-setup-env-cancel-"); + const harness = createHarness({ workspacePath: sourcePath }); + await fs.writeFile( + `${sourcePath}/.bb-env-setup.sh`, + 'printf "%s" "$SETUP_VALUE" > started\nsleep 120\nprintf unsafe > after-cancel\n', + ); + const command = { + type: "environment.attach" as const, + contributedEnv: [ + { + name: "SETUP_VALUE", + value: "configured", + source: { plugin: "fixture" }, + reason: "test", + }, + ], + environmentId: "env-setup-cancel", + initiator: null, + path: sourcePath, + setupScriptTimeoutMs: 5000, + }; + const options = harness.dispatchOptions(); + const first = dispatchCommand(command, options); + const second = dispatchCommand(command, options); + const settled = Promise.allSettled([first, second]); + try { + await expect + .poll(async () => fs.readFile(`${sourcePath}/started`, "utf8")) + .toBe("configured"); + await dispatchCommand( + { + type: "environment.attach.cancel", + environmentId: command.environmentId, + }, + options, + ); + expect(await settled).toEqual([ + expect.objectContaining({ status: "rejected" }), + expect.objectContaining({ status: "rejected" }), + ]); + await expect(fs.stat(`${sourcePath}/after-cancel`)).rejects.toThrow(); + expect(harness.provisions).toHaveLength(0); + } finally { + await harness.manager.shutdownAll(); + await settled; + } +}); diff --git a/apps/server/src/services/hosts/live-command.ts b/apps/server/src/services/hosts/live-command.ts index 0a356d90f97..fadabf2c38b 100644 --- a/apps/server/src/services/hosts/live-command.ts +++ b/apps/server/src/services/hosts/live-command.ts @@ -1,3 +1,5 @@ +import { getEnvironment } from "@bb/db"; +import { resolveHostEnvironment } from "./host-environment.js"; import { randomUUID } from "node:crypto"; import { type HostDaemonCommand, @@ -229,8 +231,25 @@ export async function runLiveHostCommand< args.command.type === "thread.stop" ? callHostOnlineRpc : callHostOnlineRpcForWork; + const sourceCommand: HostDaemonCommand = args.command; + const command = { + ...args.command, + ...(sourceCommand.type === "environment.attach" + ? { + contributedEnv: + sourceCommand.setupScriptTimeoutMs === null + ? [] + : await resolveHostEnvironment(deps, { + hostId: args.hostId, + projectId: + getEnvironment(deps.db, sourceCommand.environmentId) + ?.projectId ?? null, + }), + } + : {}), + }; const result = await call(deps, { - command: args.command, + command, hostId: args.hostId, timeoutMs: args.timeoutMs, }); diff --git a/apps/server/src/services/threads/thread-create-helpers.ts b/apps/server/src/services/threads/thread-create-helpers.ts index e72e22c852c..05af8d6ee90 100644 --- a/apps/server/src/services/threads/thread-create-helpers.ts +++ b/apps/server/src/services/threads/thread-create-helpers.ts @@ -78,6 +78,7 @@ export function buildEnvironmentProvisionCommand( ): EnvironmentProvisionCommand { return { type: "environment.attach" as const, + contributedEnv: [], environmentId: args.environmentId, initiator: args.initiator, path: args.path, diff --git a/apps/server/test/hosts/live-command.test.ts b/apps/server/test/hosts/live-command.test.ts index 386c033b5fd..3ca6471c180 100644 --- a/apps/server/test/hosts/live-command.test.ts +++ b/apps/server/test/hosts/live-command.test.ts @@ -1,13 +1,25 @@ +import { + createEnvironment, + getEnvironment, + getAppSettings, + setAppSettings, + updateHost, +} from "@bb/db"; +import { rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { updateMachineEnvironment } from "../../src/services/machines/environment-settings.js"; +import { buildEnvironmentProvisionCommand } from "../../src/services/threads/thread-create-helpers.js"; import { describe, expect, it, vi } from "vitest"; import { LIVE_DAEMON_COMMAND_TIMEOUT_MS, startLiveHostCommand, + runLiveHostCommand, } from "../../src/services/hosts/live-command.js"; import { reportQueuedCommandError, waitForQueuedCommand, } from "../helpers/commands.js"; -import { seedHostSession } from "../helpers/seed.js"; +import { seedHostSession, seedProjectWithSource } from "../helpers/seed.js"; import { withTestHarness } from "../helpers/test-app.js"; describe("live host command logging", () => { @@ -81,3 +93,136 @@ describe("live host command logging", () => { }); }); }); + +it("resolves fresh setup values at dispatch without retaining them in the request", async () => { + await withTestHarness(async (harness) => { + setAppSettings(harness.db, { + ...getAppSettings(harness.db), + machineGitCredentialsEnabled: false, + }); + const { host } = seedHostSession(harness.deps); + updateHost(harness.db, harness.hub, host.id, { + machineProviderId: "manual", + }); + const { project } = seedProjectWithSource(harness.deps, { + hostId: host.id, + }); + const environment = createEnvironment(harness.db, harness.hub, { + projectId: project.id, + hostId: host.id, + providerOwnsPath: true, + }); + const command = buildEnvironmentProvisionCommand({ + environmentId: environment.id, + hostId: host.id, + initiator: null, + path: "/tmp/setup-values", + setupScriptTimeoutMs: 1000, + }); + const original = JSON.stringify(command); + const request = vi + .spyOn(harness.hub, "requestHostOnlineRpc") + .mockImplementation(async ({ message }) => ({ + type: "host-rpc.response", + requestId: message.requestId, + commandType: "environment.attach", + ok: true, + result: { + path: command.path, + isGitRepo: false, + isWorktree: false, + branchName: null, + defaultBranch: null, + }, + })); + for (const value of ["first-secret", "refreshed-secret"]) { + await updateMachineEnvironment( + harness.db, + harness.config.dataDir, + "SETUP_VALUE", + { name: "SETUP_VALUE", value, note: null }, + ); + await runLiveHostCommand(harness.deps, { + command, + hostId: host.id, + timeoutMs: 1000, + }); + expect(request.mock.lastCall?.[0].message.command).toMatchObject({ + contributedEnv: [ + expect.objectContaining({ name: "SETUP_VALUE", value }), + ], + }); + expect(JSON.stringify(command)).toBe(original); + expect( + JSON.stringify(getEnvironment(harness.db, environment.id)), + ).not.toContain(value); + expect( + JSON.stringify( + harness.db.$client.prepare("SELECT * FROM app_settings_values").all(), + ), + ).not.toContain(value); + } + await runLiveHostCommand(harness.deps, { + command: { ...command, setupScriptTimeoutMs: null }, + hostId: host.id, + timeoutMs: 1000, + }); + expect(request.mock.lastCall?.[0].message.command).toMatchObject({ + contributedEnv: [], + }); + await writeFile(join(harness.config.dataDir, "host-id"), host.id); + await runLiveHostCommand(harness.deps, { + command, + hostId: host.id, + timeoutMs: 1000, + }); + expect(request.mock.lastCall?.[0].message.command).toMatchObject({ + contributedEnv: [], + }); + }); +}); + +it("fails provisioning if saved setup variables cannot be decrypted", async () => { + await withTestHarness(async (harness) => { + setAppSettings(harness.db, { + ...getAppSettings(harness.db), + machineGitCredentialsEnabled: false, + }); + const { host } = seedHostSession(harness.deps); + updateHost(harness.db, harness.hub, host.id, { + machineProviderId: "manual", + }); + const { project } = seedProjectWithSource(harness.deps, { + hostId: host.id, + }); + const environment = createEnvironment(harness.db, harness.hub, { + projectId: project.id, + hostId: host.id, + providerOwnsPath: true, + }); + await updateMachineEnvironment( + harness.db, + harness.config.dataDir, + "SETUP_VALUE", + { name: "SETUP_VALUE", value: "private-value", note: null }, + ); + await rm(join(harness.config.dataDir, "machine-environment-key")); + const request = vi.spyOn(harness.hub, "requestHostOnlineRpc"); + const command = buildEnvironmentProvisionCommand({ + environmentId: environment.id, + hostId: host.id, + initiator: null, + path: "/tmp/setup-values", + setupScriptTimeoutMs: 1000, + }); + await expect( + runLiveHostCommand(harness.deps, { + command, + hostId: host.id, + timeoutMs: 1000, + }), + ).rejects.toThrow("Machine environment encryption key is unavailable"); + expect(request).not.toHaveBeenCalled(); + expect(getEnvironment(harness.db, environment.id)?.status).toBe("error"); + }); +}); diff --git a/apps/server/test/internal/internal-session-protocol-version.test.ts b/apps/server/test/internal/internal-session-protocol-version.test.ts index c6a70ae774c..02c1316e83c 100644 --- a/apps/server/test/internal/internal-session-protocol-version.test.ts +++ b/apps/server/test/internal/internal-session-protocol-version.test.ts @@ -89,40 +89,45 @@ describe("internal session protocol version", () => { } }); - it("requires a PR-1 version 191 daemon to upgrade before accepting its session", async () => { - const server = await startTestServer(); - try { - const hostId = "host-pr2-only"; - upsertHost(server.db, server.hub, { id: hostId, name: "PR 2 daemon" }); - const daemon = createHostDaemonClient( - server.baseUrl, - createTestDaemonHostKey({ hostId }), - ); - const response = await daemon.session.open.$post({ - json: { - hostId, - instanceId: "instance-pr2", - hostName: "PR 2 daemon", - hasMachineCredential: true, - platform: "linux", - dataDir: "/tmp/pr2-machine", - localApiPort: 38888, - protocolVersion: 191, - activeThreads: [], - loadedEnvironments: [], - }, - }); - expect(response.status).toBe(400); - expect(await response.json()).toMatchObject({ - code: "protocol_version_mismatch", - details: { serverProtocolVersion: HOST_DAEMON_PROTOCOL_VERSION }, - message: `Daemon protocol version 191 does not match server protocol version ${HOST_DAEMON_PROTOCOL_VERSION}`, - }); - expect(getHost(server.db, hostId)?.lastRejectedProtocolVersion).toBe(191); - } finally { - await server.close(); - } - }); + it.each([191, 203, 204])( + "requires a version %i daemon to upgrade before accepting its session", + async (protocolVersion) => { + const server = await startTestServer(); + try { + const hostId = "host-pr2-only"; + upsertHost(server.db, server.hub, { id: hostId, name: "PR 2 daemon" }); + const daemon = createHostDaemonClient( + server.baseUrl, + createTestDaemonHostKey({ hostId }), + ); + const response = await daemon.session.open.$post({ + json: { + hostId, + instanceId: "instance-pr2", + hostName: "PR 2 daemon", + hasMachineCredential: true, + platform: "linux", + dataDir: "/tmp/pr2-machine", + localApiPort: 38888, + protocolVersion, + activeThreads: [], + loadedEnvironments: [], + }, + }); + expect(response.status).toBe(400); + expect(await response.json()).toMatchObject({ + code: "protocol_version_mismatch", + details: { serverProtocolVersion: HOST_DAEMON_PROTOCOL_VERSION }, + message: `Daemon protocol version ${protocolVersion} does not match server protocol version ${HOST_DAEMON_PROTOCOL_VERSION}`, + }); + expect(getHost(server.db, hostId)?.lastRejectedProtocolVersion).toBe( + protocolVersion, + ); + } finally { + await server.close(); + } + }, + ); it("rejects a session open whose protocol version does not match the server", async () => { const server = await startTestServer(); diff --git a/docs/configuration.md b/docs/configuration.md index 679db324dc6..b653e7d04c3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1292,7 +1292,8 @@ Without an explicit directory, lifecycle commands locate the unique matching hos Core resolves machine contributions through `apps/server/src/services/hosts/host-environment.ts` before dispatching setup and -teardown hooks. The `environment.hook.run` command carries `contributedEnv`; +teardown hooks. Ordinary setup uses `environment.attach`; explicit hooks use +`environment.hook.run`. Both carry transient `contributedEnv` values; the daemon applies them to the hook child process. Hook progress and errors are forwarded as-is, so contributed values printed by the child remain visible. Machine selection and precedence stay in the server @@ -1314,8 +1315,8 @@ the environment they started with: open a new terminal after a change. Agent turns receive refreshed values on their next turn and after resume. Codex rebuilds its loaded session from the existing conversation when the environment changes. -Machine environment commands require host-daemon protocol 192, covering machine -lifecycle and contribution fields for core hooks, host plugins, and terminals. +Ordinary setup variable delivery requires host-daemon protocol 205. Older +daemons must update before the server accepts their session. The built-in GitHub row uses `gh auth token --hostname github.com` and `gh api --hostname github.com user` on the server host. It supplies `GH_TOKEN`, Git's diff --git a/packages/host-daemon-contract/src/commands.ts b/packages/host-daemon-contract/src/commands.ts index 50bd1c5cfb9..408c7944b1d 100644 --- a/packages/host-daemon-contract/src/commands.ts +++ b/packages/host-daemon-contract/src/commands.ts @@ -898,6 +898,7 @@ const unmanagedEnvironmentProvisionCommandSchema = .extend({ path: z.string().min(1), setupScriptTimeoutMs: z.number().int().positive().nullable(), + contributedEnv: z.array(hostDaemonContributedEnvEntrySchema), }) .strict(); diff --git a/packages/host-daemon-contract/src/protocol.ts b/packages/host-daemon-contract/src/protocol.ts index e3a78a54c20..d4620598a13 100644 --- a/packages/host-daemon-contract/src/protocol.ts +++ b/packages/host-daemon-contract/src/protocol.ts @@ -1,3 +1,3 @@ -export const HOST_DAEMON_PROTOCOL_VERSION = 204 as const; +export const HOST_DAEMON_PROTOCOL_VERSION = 205 as const; export const HOST_ARTIFACT_MAX_BYTES = 256 * 1024 * 1024; diff --git a/packages/host-daemon-contract/test/contract.test.ts b/packages/host-daemon-contract/test/contract.test.ts index 69f03ff0851..98df9e768f2 100644 --- a/packages/host-daemon-contract/test/contract.test.ts +++ b/packages/host-daemon-contract/test/contract.test.ts @@ -999,7 +999,7 @@ const CONTRIBUTED_ENV = [ describe("host-daemon command schemas", () => { it("uses the current host-daemon protocol version", () => { - expect(HOST_DAEMON_PROTOCOL_VERSION).toBe(204); + expect(HOST_DAEMON_PROTOCOL_VERSION).toBe(205); expect(HOST_ARTIFACT_MAX_BYTES).toBe(256 * 1024 * 1024); }); @@ -1135,6 +1135,7 @@ describe("host-daemon command schemas", () => { expect(() => hostDaemonCommandSchema.parse({ type: "environment.attach", + contributedEnv: [], environmentId: "env_123", initiator: { threadId: "thr_123", @@ -1152,6 +1153,7 @@ describe("host-daemon command schemas", () => { expect(() => hostDaemonCommandSchema.parse({ type: "environment.attach", + contributedEnv: [], environmentId: "env_personal", initiator: null, workspaceProvisionType: "personal", @@ -1162,6 +1164,7 @@ describe("host-daemon command schemas", () => { expect( hostDaemonCommandSchema.parse({ type: "environment.attach", + contributedEnv: [], environmentId: "env_123", initiator: null, path: "/tmp/project", @@ -1169,6 +1172,7 @@ describe("host-daemon command schemas", () => { }), ).toMatchObject({ type: "environment.attach", + contributedEnv: [], path: "/tmp/project", }); @@ -1670,6 +1674,7 @@ describe("host-daemon command schemas", () => { expect(() => hostDaemonCommandSchema.parse({ type: "environment.attach", + contributedEnv: [], environmentId: "env_123", initiator: null, workspaceProvisionType: "managed-worktree", @@ -1681,6 +1686,7 @@ describe("host-daemon command schemas", () => { expect(() => hostDaemonCommandSchema.parse({ type: "environment.attach", + contributedEnv: [], environmentId: "env_123", initiator: null, }), @@ -1689,6 +1695,7 @@ describe("host-daemon command schemas", () => { expect(() => hostDaemonCommandSchema.parse({ type: "environment.attach", + contributedEnv: [], environmentId: "env_123", initiator: null, path: "/tmp/project", @@ -1699,6 +1706,7 @@ describe("host-daemon command schemas", () => { expect(() => hostDaemonCommandSchema.parse({ type: "environment.attach", + contributedEnv: [], environmentId: "env_123", initiator: null, path: "/tmp/project", @@ -2592,6 +2600,7 @@ describe("host-daemon command schemas", () => { expect(() => hostDaemonCommandSchema.parse({ type: "environment.attach", + contributedEnv: [], environmentId: "env_123", initiator: { threadId: "thr_123", @@ -2621,6 +2630,7 @@ describe("host-daemon command schemas", () => { expect( hostDaemonCommandSchema.safeParse({ type: "environment.attach", + contributedEnv: [], environmentId: "env_123", initiator: null, path: "/tmp/project", @@ -2631,6 +2641,7 @@ describe("host-daemon command schemas", () => { expect( hostDaemonCommandSchema.safeParse({ type: "environment.attach", + contributedEnv: [], environmentId: "env_123", initiator: null, path: "/tmp/project", @@ -2645,6 +2656,7 @@ describe("host-daemon command schemas", () => { expect( hostDaemonCommandSchema.safeParse({ type: "environment.attach", + contributedEnv: [], environmentId: "env_123", initiator: null, workspaceProvisionType: "managed-worktree", @@ -2659,6 +2671,7 @@ describe("host-daemon command schemas", () => { expect( hostDaemonCommandSchema.safeParse({ type: "environment.attach", + contributedEnv: [], environmentId: "env_123", initiator: null, workspaceProvisionType: "managed-worktree", diff --git a/packages/templates/src/templates/bb-guide-machines.md b/packages/templates/src/templates/bb-guide-machines.md index c4cd1356d23..53661f72b51 100644 --- a/packages/templates/src/templates/bb-guide-machines.md +++ b/packages/templates/src/templates/bb-guide-machines.md @@ -255,6 +255,10 @@ original `BB_DATA_DIR` if explicitly configured, to remove its installation. ## Machine environment +Repository setup receives freshly resolved machine variables on each dispatch, +including recovery. Values are sent transiently to the setup process and are +not stored in provisioning requests. Existing attached paths skip setup. + `bb machine env list --json` lists global machine variables and built-in GitHub health. `bb machine env set NAME [--note text] --json` reads its value from stdin, removing one trailing newline; values are never accepted in argv. diff --git a/plugins/bb-guide/skills/bb-cli/references/configuration.md b/plugins/bb-guide/skills/bb-cli/references/configuration.md index 5e0a5cee527..76b6895ccac 100644 --- a/plugins/bb-guide/skills/bb-cli/references/configuration.md +++ b/plugins/bb-guide/skills/bb-cli/references/configuration.md @@ -119,6 +119,10 @@ all server requests. Do not print these headers; they can contain access tokens. ## Machine environment +Repository setup receives freshly resolved machine variables on each dispatch, +including recovery. Values are sent transiently to the setup process and are +not stored in provisioning requests. Existing attached paths skip setup. + Use `bb machine env list --json` for variables and built-in gh health. `bb machine env set NAME [--note text] --json` reads the value from stdin and removes one trailing newline; never pass secrets in argv. Runtime diff --git a/tests/integration/fake/environments/environment-attach-recovery.test.ts b/tests/integration/fake/environments/environment-attach-recovery.test.ts index 0e951474fcb..fc72dc5f3de 100644 --- a/tests/integration/fake/environments/environment-attach-recovery.test.ts +++ b/tests/integration/fake/environments/environment-attach-recovery.test.ts @@ -33,6 +33,7 @@ describe("environment attach recovery", () => { const provision = dispatchCommand( { type: "environment.attach", + contributedEnv: [], environmentId: "env-setup-cancel", initiator: null, path, @@ -69,6 +70,7 @@ describe("environment attach recovery", () => { const options = harness.dispatchOptions({ dataDir: path }); const command = { type: "environment.attach" as const, + contributedEnv: [], environmentId: "env-setup-coalesce", initiator: null, path, @@ -97,6 +99,7 @@ describe("environment attach recovery", () => { const options = harness.dispatchOptions({ dataDir: path }); const command = { type: "environment.attach" as const, + contributedEnv: [], environmentId: "env-setup-retry", initiator: null, path,