From 0911a87d4881ad160ea304dfe0dbe80c8f3a2845 Mon Sep 17 00:00:00 2001 From: Andrei Ivanov Date: Tue, 8 Sep 2026 18:35:32 -0700 Subject: [PATCH] Add durable per-thread environment variables to thread spawn --- .../command-output/thread-spawn.test.ts | 36 + apps/cli/src/__tests__/spawn-helpers.test.ts | 31 + apps/cli/src/commands/thread/spawn.ts | 53 + apps/host-daemon/src/command-dispatch.test.ts | 11 + .../src/command-handlers/thread.ts | 5 + .../test/command/command-router.test.ts | 2 + .../test/command/dispatch-helpers.ts | 11 + .../test/command/thread-dispatch.test.ts | 42 + .../test/command/thread-stop-races.test.ts | 2 + .../src/services/threads/thread-commands.ts | 2 + .../services/threads/thread-create-helpers.ts | 1 + .../services/threads/thread-create-request.ts | 5 +- .../src/services/threads/thread-create.ts | 5 +- .../services/threads/thread-runtime-config.ts | 9 +- apps/server/test/helpers/seed.ts | 3 + .../plugins/plugin-thread-events.test.ts | 3 + .../threads/thread-runtime-display.test.ts | 1 + .../threads/thread-create-helpers.test.ts | 1 + .../thread-create-seed-without-run.test.ts | 8 +- .../threads/thread-runtime-config.test.ts | 12 + docs/configuration.md | 25 + .../src/runtime-thread-rewind.test.ts | 11 + .../src/runtime.lifecycle.test.ts | 33 +- packages/agent-runtime/src/runtime.ts | 22 + .../src/thread-shell-environment.ts | 38 +- packages/agent-runtime/src/types.ts | 5 + packages/bb-app/README.md | 7 +- .../db/drizzle/0115_familiar_thunderbird.sql | 1 + packages/db/drizzle/meta/0115_snapshot.json | 4187 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + packages/db/src/data/index.ts | 1 + packages/db/src/data/threads.ts | 17 + packages/db/src/schema.ts | 1 + packages/db/test/data/threads.test.ts | 20 + packages/db/test/migrate.test.ts | 58 + packages/domain/src/index.ts | 1 + packages/domain/src/provider-event.ts | 38 +- packages/domain/src/thread-env-vars.ts | 65 + packages/domain/test/provider-event.test.ts | 30 + packages/domain/test/thread-env-vars.test.ts | 77 + packages/host-daemon-contract/src/commands.ts | 2 + packages/host-daemon-contract/src/protocol.ts | 2 +- .../test/contract.test.ts | 17 +- packages/sdk/test/sdk.test.ts | 8 + packages/server-contract/src/api/threads.ts | 2 + .../server-contract/test/contract.test.ts | 33 + .../src/templates/bb-guide-threads.md | 8 + .../src/parse-operation-message.ts | 3 +- .../test/parse-operation-message.test.ts | 7 +- plugins/bb-guide/skills/bb-cli/SKILL.md | 2 + .../bb-cli/references/thread-creation.md | 7 + .../references/backend-sdk.md | 8 + .../references/providers.md | 14 +- 53 files changed, 4959 insertions(+), 41 deletions(-) create mode 100644 packages/db/drizzle/0115_familiar_thunderbird.sql create mode 100644 packages/db/drizzle/meta/0115_snapshot.json create mode 100644 packages/domain/src/thread-env-vars.ts create mode 100644 packages/domain/test/thread-env-vars.test.ts diff --git a/apps/cli/src/__tests__/command-output/thread-spawn.test.ts b/apps/cli/src/__tests__/command-output/thread-spawn.test.ts index 888379ddb1..2bd33a68cb 100644 --- a/apps/cli/src/__tests__/command-output/thread-spawn.test.ts +++ b/apps/cli/src/__tests__/command-output/thread-spawn.test.ts @@ -262,6 +262,41 @@ describe("bb thread spawn command output", () => { }); }); + it("bb thread spawn forwards repeated environment variables", async () => { + const thread: domain.Thread = fixtures.makeThread({ + id: "thread-env", + projectId: "proj-1", + providerId: "codex", + }); + const post = vi.fn(async () => thread); + stubServerApi({ "v1.threads.$post": post }); + + await runCommand( + [ + "thread", + "spawn", + "--project", + "proj-1", + "--prompt", + "hello", + "--env", + "MULTICA_TASK_ID=task-123", + "--env", + "MULTICA_TOKEN=prefix=value", + ], + register, + ); + + expect(post).toHaveBeenCalledWith({ + json: expect.objectContaining({ + envVars: { + MULTICA_TASK_ID: "task-123", + MULTICA_TOKEN: "prefix=value", + }, + }), + }); + }); + it("bb thread spawn forwards hidden visibility", async () => { const thread: domain.Thread = fixtures.makeThread({ id: "thread-hidden", @@ -333,6 +368,7 @@ describe("bb thread spawn command output", () => { const helpOutput = await getHelpOutput(["thread", "spawn"], register); expect(helpOutput).toContain("--permission-mode "); expect(helpOutput).toContain("--visibility "); + expect(helpOutput).toContain("--env "); expect(helpOutput).toContain("Exact Git ref"); expect(helpOutput).toContain("origin/ for a remote ref"); expect(helpOutput).toContain("bb environment providers"); diff --git a/apps/cli/src/__tests__/spawn-helpers.test.ts b/apps/cli/src/__tests__/spawn-helpers.test.ts index 100487d4cf..b4ff5e2f31 100644 --- a/apps/cli/src/__tests__/spawn-helpers.test.ts +++ b/apps/cli/src/__tests__/spawn-helpers.test.ts @@ -3,6 +3,7 @@ import { DEFAULT_THREAD_WAIT_POLL_INTERVAL_MS } from "@bb/sdk"; import { buildSpawnEnvironment, looksLikePath, + parseThreadEnvVars, requireHostId, } from "../commands/thread/spawn.js"; import { @@ -84,6 +85,36 @@ describe("looksLikePath", () => { }); }); +describe("parseThreadEnvVars", () => { + it("parses repeated entries using the first equals sign", () => { + expect(parseThreadEnvVars(["TOKEN=prefix=value", "EMPTY="])).toEqual({ + TOKEN: "prefix=value", + EMPTY: "", + }); + expect(parseThreadEnvVars(undefined)).toBeUndefined(); + }); + + it.each([ + [["TOKEN"], "Expected KEY=VALUE"], + [["1TOKEN=value"], "Invalid --env variable name '1TOKEN'"], + [["BB_THREAD_ID=value"], "reserved BB_ prefix"], + [["TOKEN=one", "TOKEN=two"], "Duplicate --env variable 'TOKEN'"], + [[`TOKEN=ok\0bad`], "must not contain a null byte"], + ])("rejects invalid entries %#", (values, message) => { + expect(() => parseThreadEnvVars(values)).toThrow(message); + }); + + it("reports map limits before sending a request", () => { + const values = Array.from( + { length: 33 }, + (_, index) => `VALUE_${index}=value`, + ); + expect(() => parseThreadEnvVars(values)).toThrow( + "must contain at most 32 entries", + ); + }); +}); + describe("requireHostId", () => { it("throws when host ID is null", () => { expect(() => requireHostId(null)).toThrow("Cannot reach local host daemon"); diff --git a/apps/cli/src/commands/thread/spawn.ts b/apps/cli/src/commands/thread/spawn.ts index f4b12a8990..57c983969f 100644 --- a/apps/cli/src/commands/thread/spawn.ts +++ b/apps/cli/src/commands/thread/spawn.ts @@ -2,10 +2,14 @@ import { Command } from "commander"; import { jsonValueSchema, PERSONAL_PROJECT_ID, + threadEnvVarNameSchema, + threadEnvVarValueSchema, + threadEnvVarsSchema, threadVisibilitySchema, type GitBranchSelection, type Thread, type JsonValue, + type ThreadEnvVars, } from "@bb/domain"; import type { CreateThreadEnvironmentArgs } from "@bb/server-contract"; import { action } from "../../action.js"; @@ -42,6 +46,7 @@ interface ThreadSpawnCommandOptions { json?: boolean; project?: string; environment?: string; + env?: string[]; newEnvironment?: string; environmentProvider?: string; environmentInputs?: string; @@ -71,6 +76,46 @@ export function looksLikePath(value: string): boolean { return value.includes("/") || value.startsWith(".") || value.startsWith("~"); } +export function parseThreadEnvVars( + values: readonly string[] | undefined, +): ThreadEnvVars | undefined { + if (values === undefined || values.length === 0) return undefined; + const entries: [string, string][] = []; + const names = new Set(); + for (const entry of values) { + const separatorIndex = entry.indexOf("="); + if (separatorIndex === -1) { + throw new Error("Invalid --env entry. Expected KEY=VALUE."); + } + const name = entry.slice(0, separatorIndex); + const value = entry.slice(separatorIndex + 1); + const parsedName = threadEnvVarNameSchema.safeParse(name); + if (!parsedName.success) { + throw new Error( + `Invalid --env variable name '${name}': ${parsedName.error.issues[0]?.message ?? "is invalid"}.`, + ); + } + if (names.has(parsedName.data)) { + throw new Error(`Duplicate --env variable '${parsedName.data}'.`); + } + const parsedValue = threadEnvVarValueSchema.safeParse(value); + if (!parsedValue.success) { + throw new Error( + `Invalid --env value for '${parsedName.data}': ${parsedValue.error.issues[0]?.message ?? "is invalid"}.`, + ); + } + names.add(parsedName.data); + entries.push([parsedName.data, parsedValue.data]); + } + const parsed = threadEnvVarsSchema.safeParse(Object.fromEntries(entries)); + if (!parsed.success) { + throw new Error( + `Invalid --env values: ${parsed.error.issues[0]?.message ?? "the environment is invalid"}.`, + ); + } + return parsed.data; +} + export function requireHostId(hostId: string | null): string { if (!hostId) { throw new Error("Cannot reach local host daemon. Is it running?"); @@ -265,6 +310,12 @@ export function registerSpawnCommand( "--environment ", "Existing environment ID or unmanaged workspace path", ) + .option( + "--env ", + "Set a per-thread environment variable (repeatable)", + collectOption, + [], + ) .option( "--new-environment ", "Create a fresh environment of the given kind (personal or worktree)", @@ -410,6 +461,7 @@ export function registerSpawnCommand( const sendAt = opts.sendAt === undefined ? undefined : parseSendAt(opts.sendAt); const providerId = opts.provider?.trim(); + const envVars = parseThreadEnvVars(opts.env); let thread: Thread; try { @@ -418,6 +470,7 @@ export function registerSpawnCommand( origin: "cli", projectId, ...(providerId ? { providerId } : {}), + ...(envVars ? { envVars } : {}), ...(opts.model ? { model: opts.model } : {}), input: buildPromptInputs({ message: opts.prompt, diff --git a/apps/host-daemon/src/command-dispatch.test.ts b/apps/host-daemon/src/command-dispatch.test.ts index bb3a90099e..fa16878ce5 100644 --- a/apps/host-daemon/src/command-dispatch.test.ts +++ b/apps/host-daemon/src/command-dispatch.test.ts @@ -245,6 +245,7 @@ function createTurnSubmitCommand( instructions: "Be concise.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -345,6 +346,7 @@ function createInstallationGatedThreadStart( instructions: "Be concise.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }; @@ -782,6 +784,7 @@ describe("dispatchCommand", () => { instructions: "Be concise.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -859,6 +862,7 @@ describe("dispatchCommand", () => { instructions: "Be concise.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1232,6 +1236,7 @@ describe("dispatchCommand", () => { instructions: "Be concise.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1325,6 +1330,7 @@ describe("dispatchCommand", () => { instructions: "Be concise.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }; @@ -1404,6 +1410,7 @@ describe("dispatchCommand", () => { instructions: "Be concise.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }; @@ -1471,6 +1478,7 @@ describe("dispatchCommand", () => { instructions: "Be concise.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }; @@ -1731,6 +1739,7 @@ describe("dispatchCommand", () => { instructions: start.instructions, dynamicTools: start.dynamicTools, contributedEnv: [], + envVars: {}, injectedSkillSources: start.injectedSkillSources, instructionMode: start.instructionMode, }; @@ -2297,6 +2306,7 @@ describe("dispatchCommand", () => { instructions: "Be concise.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [fixture.source], instructionMode: "append", }; @@ -2360,6 +2370,7 @@ describe("dispatchCommand", () => { instructions: "Be concise.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [fixture.source], instructionMode: "append", }, diff --git a/apps/host-daemon/src/command-handlers/thread.ts b/apps/host-daemon/src/command-handlers/thread.ts index f0e74ecc66..96fe5ff18b 100644 --- a/apps/host-daemon/src/command-handlers/thread.ts +++ b/apps/host-daemon/src/command-handlers/thread.ts @@ -197,6 +197,7 @@ async function resumeThreadRuntimeIfMissing( providerThreadId: resumeContext.providerThreadId, providerId: resumeContext.providerId, contributedEnv: resumeContext.contributedEnv, + envVars: resumeContext.envVars, options: command.options, instructions: resumeContext.instructions, dynamicTools: resumeContext.dynamicTools, @@ -243,6 +244,7 @@ export async function startThread( projectId: command.projectId, providerId: command.providerId, contributedEnv: command.contributedEnv, + envVars: command.envVars, clientRequestId: command.requestId, input: staged.input, ...(staged.inputGroups !== undefined @@ -287,6 +289,7 @@ export async function prepareThreadRewind( projectId: command.projectId, providerId: command.providerId, contributedEnv: command.contributedEnv, + envVars: command.envVars, sourceProviderThreadId: command.sourceProviderThreadId, retainThroughProviderCheckpoint: command.retainThroughProviderCheckpoint, options: command.options, @@ -353,6 +356,7 @@ async function runSubmittedTurn( clientRequestId: command.requestId, options: command.options, contributedEnv: command.resumeContext.contributedEnv, + envVars: command.resumeContext.envVars, instructions: command.resumeContext.instructions, }); return { appliedAs: "new-turn" }; @@ -376,6 +380,7 @@ async function steerSubmittedTurn( clientRequestId: command.requestId, options: command.options, contributedEnv: command.resumeContext.contributedEnv, + envVars: command.resumeContext.envVars, instructions: command.resumeContext.instructions, }); diff --git a/apps/host-daemon/test/command/command-router.test.ts b/apps/host-daemon/test/command/command-router.test.ts index 315fc53d4e..05869c1e27 100644 --- a/apps/host-daemon/test/command/command-router.test.ts +++ b/apps/host-daemon/test/command/command-router.test.ts @@ -124,6 +124,7 @@ function createTurnSubmitCommand( instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -157,6 +158,7 @@ function createThreadStartCommand(): ThreadStartCommand { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }; diff --git a/apps/host-daemon/test/command/dispatch-helpers.ts b/apps/host-daemon/test/command/dispatch-helpers.ts index cba85ee245..aae035be3d 100644 --- a/apps/host-daemon/test/command/dispatch-helpers.ts +++ b/apps/host-daemon/test/command/dispatch-helpers.ts @@ -102,17 +102,21 @@ interface FakeRuntimeThreadControls { ) => void; } +type FakeRuntimeEnvironment = Readonly>; + interface FakeRuntimeState { archivedBridgeLaunch: AgentRuntimeBridgeLaunch | undefined; archivedProviderId: string | undefined; archivedProviderThreadId: string | undefined; archivedThreadId: string | undefined; ranTurnClientRequestId: ClientTurnRequestId | undefined; + ranTurnEnvVars: FakeRuntimeEnvironment | undefined; ranTurnInput: PromptInput[] | undefined; ranTurnText: string | undefined; renamedTitle: string | undefined; resumedBridgeLaunch: AgentRuntimeBridgeLaunch | undefined; resumedEnvironmentId: string | undefined; + resumedEnvVars: FakeRuntimeEnvironment | undefined; resumedProviderThreadId: string | undefined; resumedThreadId: string | undefined; runningProviders: string[]; @@ -120,6 +124,7 @@ interface FakeRuntimeState { startedDynamicTools: DynamicTool[] | undefined; startedBridgeLaunch: AgentRuntimeBridgeLaunch | undefined; startedEnvironmentId: string | undefined; + startedEnvVars: FakeRuntimeEnvironment | undefined; startedInput: PromptInput[] | undefined; startedInputGroups: PromptInput[][] | undefined; startedInstructions: string | undefined; @@ -262,11 +267,13 @@ export function createFakeRuntime() { archivedProviderThreadId: undefined, archivedThreadId: undefined, ranTurnClientRequestId: undefined, + ranTurnEnvVars: undefined, ranTurnInput: undefined, ranTurnText: undefined, renamedTitle: undefined, resumedBridgeLaunch: undefined, resumedEnvironmentId: undefined, + resumedEnvVars: undefined, resumedProviderThreadId: undefined, resumedThreadId: undefined, runningProviders: [], @@ -274,6 +281,7 @@ export function createFakeRuntime() { startedDynamicTools: undefined, startedBridgeLaunch: undefined, startedEnvironmentId: undefined, + startedEnvVars: undefined, startedInput: undefined, startedInputGroups: undefined, startedInstructions: undefined, @@ -318,6 +326,7 @@ export function createFakeRuntime() { async startThread(args) { state.startedBridgeLaunch = args.bridgeLaunch; state.startedEnvironmentId = args.environmentId; + state.startedEnvVars = args.envVars; state.startedThreadId = args.threadId; state.startedDynamicTools = args.dynamicTools; state.startedInput = args.input; @@ -341,6 +350,7 @@ export function createFakeRuntime() { async resumeThread(args) { state.resumedBridgeLaunch = args.bridgeLaunch; state.resumedEnvironmentId = args.environmentId; + state.resumedEnvVars = args.envVars; state.resumedThreadId = args.threadId; state.resumedProviderThreadId = args.providerThreadId; const providerThreadId = @@ -356,6 +366,7 @@ export function createFakeRuntime() { state.ranTurnText = firstInput?.type === "text" ? firstInput.text : undefined; state.ranTurnClientRequestId = args.clientRequestId; + state.ranTurnEnvVars = args.envVars; state.ranTurnInput = args.input; activeTurnsByThreadId.set(args.threadId, `turn-${nextTurnNumber++}`); }, diff --git a/apps/host-daemon/test/command/thread-dispatch.test.ts b/apps/host-daemon/test/command/thread-dispatch.test.ts index 16aa2c7d06..f76080e530 100644 --- a/apps/host-daemon/test/command/thread-dispatch.test.ts +++ b/apps/host-daemon/test/command/thread-dispatch.test.ts @@ -94,6 +94,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: { MULTICA_TASK_ID: "task-123" }, injectedSkillSources: [], instructionMode: "append", }; @@ -119,6 +120,9 @@ describe("thread command dispatch", () => { providerThreadId: "provider-thread-stale-start", }); expect(harness.runtimeState.startedThreadId).toBe("thread-stale-start"); + expect(harness.runtimeState.startedEnvVars).toEqual({ + MULTICA_TASK_ID: "task-123", + }); }); it("rejects turn.submit when the loaded runtime path differs from resume workspaceContext", async () => { @@ -159,6 +163,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -221,6 +226,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -332,6 +338,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -421,6 +428,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -598,6 +606,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -697,6 +706,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -749,6 +759,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -813,6 +824,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -875,6 +887,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -953,6 +966,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1025,6 +1039,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1087,6 +1102,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1151,6 +1167,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1203,6 +1220,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1306,6 +1324,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1403,6 +1422,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1456,6 +1476,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: { MULTICA_TASK_ID: "task-123" }, injectedSkillSources: [], instructionMode: "append", }, @@ -1470,6 +1491,12 @@ describe("thread command dispatch", () => { expect(harness.runtimeState.resumedProviderThreadId).toBe( "provider-thread-resume-after-archive", ); + expect(harness.runtimeState.resumedEnvVars).toEqual({ + MULTICA_TASK_ID: "task-123", + }); + expect(harness.runtimeState.ranTurnEnvVars).toEqual({ + MULTICA_TASK_ID: "task-123", + }); expect(harness.runtimeState.ranTurnText).toBe("follow up"); }); @@ -1547,6 +1574,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1583,6 +1611,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1642,6 +1671,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1681,6 +1711,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1736,6 +1767,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1803,6 +1835,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1879,6 +1912,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1938,6 +1972,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1992,6 +2027,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -2042,6 +2078,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -2140,6 +2177,7 @@ describe("thread command dispatch", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -2291,6 +2329,7 @@ describe("thread command dispatch", () => { }, ], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "replace", }, @@ -2337,6 +2376,7 @@ describe("thread command dispatch", () => { instructions: "test", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", threadStoragePath: storagePath, @@ -2377,6 +2417,7 @@ describe("thread command dispatch", () => { instructions: "test", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -2417,6 +2458,7 @@ describe("thread command dispatch", () => { instructions: "test", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", threadStoragePath: "/tmp/evil-escape", diff --git a/apps/host-daemon/test/command/thread-stop-races.test.ts b/apps/host-daemon/test/command/thread-stop-races.test.ts index 08dfdd6458..2f32dfb1dd 100644 --- a/apps/host-daemon/test/command/thread-stop-races.test.ts +++ b/apps/host-daemon/test/command/thread-stop-races.test.ts @@ -238,6 +238,7 @@ function threadStartCommand( instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }; @@ -275,6 +276,7 @@ function turnSubmitCommand( instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, diff --git a/apps/server/src/services/threads/thread-commands.ts b/apps/server/src/services/threads/thread-commands.ts index 90caab5ee1..a98f848e63 100644 --- a/apps/server/src/services/threads/thread-commands.ts +++ b/apps/server/src/services/threads/thread-commands.ts @@ -294,6 +294,7 @@ export async function buildThreadStartCommand( instructions: runtimeContext.instructions, dynamicTools: runtimeContext.dynamicTools, contributedEnv: runtimeContext.contributedEnv, + envVars: runtimeContext.envVars, injectedSkillSources: runtimeContext.injectedSkillSources, instructionMode: runtimeContext.instructionMode, threadStoragePath: runtimeContext.threadStoragePath, @@ -335,6 +336,7 @@ function buildPreparedTurnSubmitCommandPayload( instructions: args.runtimeContext.instructions, dynamicTools: args.runtimeContext.dynamicTools, contributedEnv: args.runtimeContext.contributedEnv, + envVars: args.runtimeContext.envVars, injectedSkillSources: args.runtimeContext.injectedSkillSources, instructionMode: args.runtimeContext.instructionMode, }, diff --git a/apps/server/src/services/threads/thread-create-helpers.ts b/apps/server/src/services/threads/thread-create-helpers.ts index 084d7ca0fd..d1ce194b0f 100644 --- a/apps/server/src/services/threads/thread-create-helpers.ts +++ b/apps/server/src/services/threads/thread-create-helpers.ts @@ -100,6 +100,7 @@ export function createThreadRecord( projectId: args.request.projectId, environmentId: args.environmentId, providerId: args.request.providerId, + envVars: args.request.envVars, title: args.request.title ?? null, titleFallback: args.request.titleFallback, sectionId, diff --git a/apps/server/src/services/threads/thread-create-request.ts b/apps/server/src/services/threads/thread-create-request.ts index b448155f50..82a704d484 100644 --- a/apps/server/src/services/threads/thread-create-request.ts +++ b/apps/server/src/services/threads/thread-create-request.ts @@ -1,5 +1,6 @@ import type { PromptInput, + ThreadEnvVars, ThreadOriginKind, ThreadVisibility, } from "@bb/domain"; @@ -14,6 +15,7 @@ import type { export interface ThreadCreateServiceRequestInput { environment: CreateThreadEnvironmentArgs; + envVars?: CreateThreadRequest["envVars"]; executionInputSources?: CreateThreadRequest["executionInputSources"]; /** * Epoch ms the first message should dispatch at. Present ⇒ the thread is @@ -42,9 +44,10 @@ export interface ThreadCreateServiceRequestInput { export interface ThreadCreateServiceRequest extends Omit< ThreadCreateServiceRequestInput, - "environment" | "providerId" + "environment" | "envVars" | "providerId" > { environment: EnvironmentArgs | ProviderEnvironmentArgs; + envVars: ThreadEnvVars; providerId: string; titleFallback: string | null; visibility: ThreadVisibility; diff --git a/apps/server/src/services/threads/thread-create.ts b/apps/server/src/services/threads/thread-create.ts index 2ff752482d..271312b64e 100644 --- a/apps/server/src/services/threads/thread-create.ts +++ b/apps/server/src/services/threads/thread-create.ts @@ -478,7 +478,10 @@ export async function createThreadFromRequest( 'originPluginId requires origin "plugin"', ); } - const requestInput = { ...rawRequestInput }; + const requestInput = { + ...rawRequestInput, + envVars: rawRequestInput.envVars ?? {}, + }; const pluginMentionContext = await resolvePluginMentionContextInputs( requestInput.input, ); diff --git a/apps/server/src/services/threads/thread-runtime-config.ts b/apps/server/src/services/threads/thread-runtime-config.ts index 472181638a..a1c8081f6b 100644 --- a/apps/server/src/services/threads/thread-runtime-config.ts +++ b/apps/server/src/services/threads/thread-runtime-config.ts @@ -1,4 +1,4 @@ -import { getEnvironment, getHost, getProject } from "@bb/db"; +import { getEnvironment, getHost, getProject, getThreadEnvVars } from "@bb/db"; import type { DynamicTool, InstructionMode, @@ -8,6 +8,7 @@ import type { Thread, ThreadExecutionOptions, ThreadExecutionSource, + ThreadEnvVars, ThreadTurnInitiator, EnvironmentStatus, } from "@bb/domain"; @@ -78,6 +79,7 @@ interface ResolvePermissionEscalationArgs { export interface ResolvedThreadRuntimeCommandConfig { contributedEnv: HostDaemonContributedEnvEntry[]; dynamicTools: DynamicTool[]; + envVars: ThreadEnvVars; injectedSkillSources: HostDaemonInjectedSkillSource[]; instructionMode: InstructionMode; instructions: string; @@ -162,6 +164,10 @@ export async function resolveThreadRuntimeCommandConfig( if (!host) { throw new ApiError(404, "host_not_found", "Host not found"); } + const envVars = getThreadEnvVars(deps.db, args.thread.id); + if (envVars === null) { + throw new ApiError(404, "thread_not_found", "Thread not found"); + } const [projectSkillSources, sharedSkills, workspaceAgentInstructions] = await Promise.all([ @@ -311,6 +317,7 @@ export async function resolveThreadRuntimeCommandConfig( return { contributedEnv, dynamicTools, + envVars, injectedSkillSources, instructionMode: "append", instructions, diff --git a/apps/server/test/helpers/seed.ts b/apps/server/test/helpers/seed.ts index 485a4e1695..495152edd4 100644 --- a/apps/server/test/helpers/seed.ts +++ b/apps/server/test/helpers/seed.ts @@ -33,6 +33,7 @@ import type { ThreadEventScope, ThreadEventItemType, ThreadEventType, + ThreadEnvVars, ThreadOriginKind, ThreadStatus, ThreadVisibility, @@ -193,6 +194,7 @@ export function seedThread( args: { projectId: string; environmentId?: string | null; + envVars?: ThreadEnvVars; providerId?: string; status?: ThreadStatus; title?: string | null; @@ -207,6 +209,7 @@ export function seedThread( return createThread(deps.db, deps.hub, { projectId: args.projectId, environmentId: args.environmentId ?? null, + ...(args.envVars !== undefined ? { envVars: args.envVars } : {}), providerId: args.providerId ?? "codex", status: args.status ?? "idle", title: args.title ?? "Test Thread", diff --git a/apps/server/test/services/plugins/plugin-thread-events.test.ts b/apps/server/test/services/plugins/plugin-thread-events.test.ts index 69ca44daa5..7956873402 100644 --- a/apps/server/test/services/plugins/plugin-thread-events.test.ts +++ b/apps/server/test/services/plugins/plugin-thread-events.test.ts @@ -275,6 +275,7 @@ describe("plugin thread lifecycle events", () => { const { environment, project } = seedThreadFixture(harness); const request: ThreadCreateServiceRequest = { environment: { type: "reuse", environmentId: environment.id }, + envVars: {}, input: [], origin: null, projectId: project.id, @@ -320,6 +321,7 @@ describe("plugin thread lifecycle events", () => { environmentId: environment.id, request: { environment: { type: "reuse", environmentId: environment.id }, + envVars: { MULTICA_TOKEN: "thread-secret" }, input: [], origin: "plugin", originPluginId, @@ -342,6 +344,7 @@ describe("plugin thread lifecycle events", () => { id: owned.id, visibility: "hidden", }); + expect(JSON.stringify(recorded)).not.toContain("thread-secret"); } finally { delete globals.__hiddenCreatedEvents; await cleanup(); diff --git a/apps/server/test/services/threads/thread-runtime-display.test.ts b/apps/server/test/services/threads/thread-runtime-display.test.ts index de3f5000d3..249beef270 100644 --- a/apps/server/test/services/threads/thread-runtime-display.test.ts +++ b/apps/server/test/services/threads/thread-runtime-display.test.ts @@ -224,6 +224,7 @@ function createThreadListEntry( ): ThreadWithPendingInteractionState { return { ...args.thread, + envVarsJson: "{}", modelOverride: null, reasoningLevelOverride: null, environmentBranchName: null, diff --git a/apps/server/test/threads/thread-create-helpers.test.ts b/apps/server/test/threads/thread-create-helpers.test.ts index 97cd22b457..f4b167dfaa 100644 --- a/apps/server/test/threads/thread-create-helpers.test.ts +++ b/apps/server/test/threads/thread-create-helpers.test.ts @@ -149,6 +149,7 @@ describe("createThreadRecord", () => { environmentId: environment.id, type: "reuse", }, + envVars: {}, sectionId: sectionResult.section.id, input: [], origin: "app", diff --git a/apps/server/test/threads/thread-create-seed-without-run.test.ts b/apps/server/test/threads/thread-create-seed-without-run.test.ts index 138e4499ea..5f7d5e65da 100644 --- a/apps/server/test/threads/thread-create-seed-without-run.test.ts +++ b/apps/server/test/threads/thread-create-seed-without-run.test.ts @@ -281,6 +281,7 @@ describe("thread creation with startedOnBehalfOf (seed-without-run)", () => { hostId: host.id, workspace: { type: "unmanaged", path: "/tmp/normal-start-project" }, }, + envVars: { MULTICA_TASK_ID: "task-123" }, input: textInput("Just start normally"), origin: "app", projectId: project.id, @@ -297,7 +298,12 @@ describe("thread creation with startedOnBehalfOf (seed-without-run)", () => { ({ command }) => command.type === "thread.start" && command.threadId === thread.id, ); - expect(queuedStart.command.type).toBe("thread.start"); + if (queuedStart.command.type !== "thread.start") { + throw new Error("Expected a thread.start command"); + } + expect(queuedStart.command.envVars).toEqual({ + MULTICA_TASK_ID: "task-123", + }); }); }); }); diff --git a/apps/server/test/threads/thread-runtime-config.test.ts b/apps/server/test/threads/thread-runtime-config.test.ts index 5f0780de06..1e767df981 100644 --- a/apps/server/test/threads/thread-runtime-config.test.ts +++ b/apps/server/test/threads/thread-runtime-config.test.ts @@ -192,6 +192,10 @@ describe("thread runtime config", () => { const thread = seedThread(harness.deps, { projectId: project.id, environmentId: environment.id, + envVars: { + MULTICA_TASK_ID: "task-123", + MULTICA_TOKEN: "token", + }, providerId: "acp-custom", }); seedThreadRuntimeState(harness.deps, { @@ -234,6 +238,10 @@ describe("thread runtime config", () => { expect(startCommand.bridgeLaunch.providerOptions).toMatchObject({ acpLaunchSpec: expectedSpec, }); + expect(startCommand.envVars).toEqual({ + MULTICA_TASK_ID: "task-123", + MULTICA_TOKEN: "token", + }); expect(startCommand.dynamicTools).toEqual([ expect.objectContaining({ name: "update_environment_directory", @@ -260,6 +268,10 @@ describe("thread runtime config", () => { expect( submitCommand.resumeContext.bridgeLaunch.providerOptions, ).toMatchObject({ acpLaunchSpec: expectedSpec }); + expect(submitCommand.resumeContext.envVars).toEqual({ + MULTICA_TASK_ID: "task-123", + MULTICA_TOKEN: "token", + }); expect(submitCommand.resumeContext.dynamicTools).toEqual([ expect.objectContaining({ name: "update_environment_directory", diff --git a/docs/configuration.md b/docs/configuration.md index addc2320b4..a6cedc8d86 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -72,6 +72,31 @@ For the packaged app, prefer `bb-app config`, `bb-app env`, and launcher flags over shell variables. The environment remains the internal and deployment substrate, and source-development commands still load `.env` files. +### Per-thread provider environment + +Use repeatable `--env KEY=VALUE` flags when process configuration belongs to +one newly spawned thread instead of every provider on the machine: + +```bash +bb thread spawn --project --prompt "Work on the task" \ + --env MULTICA_TASK_ID=task-123 \ + --env MULTICA_TOKEN=token +``` + +SDK callers pass the equivalent `envVars` map to `threads.spawn`. These values +override matching host-shell and plugin-contributed values, stay attached to +the thread across later turns and provider-session resumes, and work when the +thread runs on another enrolled machine. They do not mutate the host daemon's +environment or become defaults for other threads. + +Names use portable shell-variable syntax and cannot begin with bb's reserved +`BB_` prefix. A thread accepts at most 32 entries, 16 KiB per value, and 64 KiB +for the serialized map. CLI values may be empty or contain `=`; duplicate names +and null bytes are rejected. Values are omitted from thread API responses and +masked in provider-environment timeline events. They are still supplied on the +command line and stored with the thread in the server data directory, so treat +shell history and access to that directory as sensitive. + For source development, `pnpm dev` automatically injects `BB_DEV_CONNECT_BASE_URL=http://bb.localhost:`. The Connect plugin accepts this loopback origin only when `NODE_ENV=development` diff --git a/packages/agent-runtime/src/runtime-thread-rewind.test.ts b/packages/agent-runtime/src/runtime-thread-rewind.test.ts index 0a79456817..75f6391b10 100644 --- a/packages/agent-runtime/src/runtime-thread-rewind.test.ts +++ b/packages/agent-runtime/src/runtime-thread-rewind.test.ts @@ -56,6 +56,7 @@ describe("prepareThreadRewind", () => { leaseId: "lease-1", projectId: "project-1", providerId: "codex", + envVars: { MULTICA_TASK_ID: "task-123" }, sourceProviderThreadId: "provider-source-1", retainThroughProviderCheckpoint: "turn-before-edit", options: fullRuntimeOptions, @@ -76,11 +77,21 @@ describe("prepareThreadRewind", () => { sourceProviderCheckpointId: "turn-before-edit", sourceProviderThreadId: "provider-source-1", threadId: "thread-1:rewind:lease-1", + options: expect.objectContaining({ + envVars: expect.objectContaining({ + MULTICA_TASK_ID: "task-123", + }), + }), }), expect.objectContaining({ sourceProviderCheckpointId: "turn-before-edit", sourceProviderThreadId: "provider-source-1", threadId: "thread-1:rewind:lease-2", + options: expect.objectContaining({ + envVars: expect.objectContaining({ + MULTICA_TASK_ID: "task-123", + }), + }), }), ]); expect(events).toEqual([]); diff --git a/packages/agent-runtime/src/runtime.lifecycle.test.ts b/packages/agent-runtime/src/runtime.lifecycle.test.ts index afeac5b6a8..ec699b59c4 100644 --- a/packages/agent-runtime/src/runtime.lifecycle.test.ts +++ b/packages/agent-runtime/src/runtime.lifecycle.test.ts @@ -276,6 +276,11 @@ describe("createAgentRuntime lifecycle", () => { projectId: "p1", providerId: "fake", contributedEnv, + envVars: { + PATH: "/thread/bin", + MULTICA_TOKEN: "thread-secret", + BB_PROJECT_ID: "forged-project", + }, options: fullRuntimeOptions, }); @@ -287,8 +292,9 @@ describe("createAgentRuntime lifecycle", () => { cwd: tmpDir, options: expect.objectContaining({ envVars: { - PATH: "/plugin/bin", + PATH: "/thread/bin", AUTH_PROXY_URL: "http://127.0.0.1:3334/plugins/env-test/auth", + MULTICA_TOKEN: "thread-secret", BB_HOST_DAEMON_PORT: "3002", BB_PROJECT_ID: "p1", BB_SERVER_URL: "http://127.0.0.1:3334", @@ -305,9 +311,13 @@ describe("createAgentRuntime lifecycle", () => { entries: expect.arrayContaining([ { name: "PATH", - source: { plugin: "env-test" }, - value: "/plugin/bin", - reason: "Use the plugin toolchain", + source: "thread", + value: { masked: true }, + }, + { + name: "MULTICA_TOKEN", + source: "thread", + value: { masked: true }, }, { name: "AUTH_PROXY_URL", @@ -318,6 +328,7 @@ describe("createAgentRuntime lifecycle", () => { ]), }); expect(JSON.stringify(events)).not.toContain("/plugins/env-test/auth"); + expect(JSON.stringify(events)).not.toContain("thread-secret"); await runtime.runTurn({ clientRequestId: "creq_222222224c", @@ -326,6 +337,15 @@ describe("createAgentRuntime lifecycle", () => { contributedEnv, options: fullRuntimeOptions, }); + expect(record.last("turn/start")?.params).toMatchObject({ + options: { + envVars: { + PATH: "/thread/bin", + MULTICA_TOKEN: "thread-secret", + BB_PROJECT_ID: "p1", + }, + }, + }); expect( events.filter((event) => event.type === "provider.env-resolved"), ).toHaveLength(1); @@ -595,6 +615,10 @@ describe("createAgentRuntime lifecycle", () => { projectId: "p1", providerThreadId: "prov-1", providerId: "fake", + envVars: { + MULTICA_TASK_ID: "task-123", + BB_THREAD_ID: "forged-thread", + }, options: fullRuntimeOptions, }); @@ -611,6 +635,7 @@ describe("createAgentRuntime lifecycle", () => { BB_HOST_DAEMON_PORT: "3002", BB_SERVER_URL: "http://127.0.0.1:3334", BB_PROJECT_ID: "p1", + MULTICA_TASK_ID: "task-123", BB_THREAD_ID: "t1", BB_ENVIRONMENT_ID: "env-1", }, diff --git a/packages/agent-runtime/src/runtime.ts b/packages/agent-runtime/src/runtime.ts index 3d64e4d7c8..2a786d6165 100644 --- a/packages/agent-runtime/src/runtime.ts +++ b/packages/agent-runtime/src/runtime.ts @@ -58,6 +58,7 @@ import type { AgentRuntimeBridgeLaunch, AgentRuntimeExecutionOptions, AgentRuntimeOptions, + AgentRuntimeShellEnvironment, ReapedIdleProviderSession, } from "./types.js"; import { @@ -197,6 +198,7 @@ interface ThreadRuntimeConfig { projectId?: string; providerId: string; sessionRestorable: boolean; + threadEnvVars: AgentRuntimeShellEnvironment; } interface RuntimeParsedMessageArgs { @@ -1040,6 +1042,7 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { providerThreadId: args.providerThreadId, providerId: currentConfig.providerId, contributedEnv: currentConfig.contributedEnv, + envVars: currentConfig.threadEnvVars, options: args.options, ...(resumeInstructions !== undefined ? { instructions: resumeInstructions } @@ -1159,6 +1162,7 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { function resolveRuntimeThreadEnvironment(args: { contributedEnv: readonly AgentRuntimeContributedEnvEntry[]; environmentId: string; + envVars: AgentRuntimeShellEnvironment; projectId?: string; threadId: string; }): { @@ -1170,6 +1174,7 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { baseShellEnv: options.shellEnv, contributedEnv: args.contributedEnv, environmentId: args.environmentId, + envVars: args.envVars, projectId: args.projectId, threadStoragePath: resolveThreadStoragePath({ options, @@ -1469,6 +1474,7 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { providerId, bridgeLaunch, contributedEnv = [], + envVars = {}, clientRequestId, input, inputGroups, @@ -1500,6 +1506,7 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { const resolvedEnvironment = resolveRuntimeThreadEnvironment({ contributedEnv, environmentId, + envVars, projectId, threadId, }); @@ -1523,6 +1530,7 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { projectId, providerId, sessionRestorable: false, + threadEnvVars: envVars, }); const providerExecutionContext = toProviderExecutionContext({ @@ -1610,6 +1618,7 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { clientRequestId, options: execOpts, contributedEnv, + envVars, instructions, }); } @@ -1627,6 +1636,7 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { projectId, providerId, contributedEnv = [], + envVars = {}, sourceProviderThreadId, retainThroughProviderCheckpoint, bridgeLaunch, @@ -1680,6 +1690,7 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { const resolvedEnvironment = resolveRuntimeThreadEnvironment({ contributedEnv, environmentId, + envVars, projectId, threadId, }); @@ -1798,6 +1809,7 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { providerId, bridgeLaunch, contributedEnv = [], + envVars = {}, options: execOpts, instructions, dynamicTools, @@ -1825,6 +1837,7 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { const resolvedEnvironment = resolveRuntimeThreadEnvironment({ contributedEnv, environmentId, + envVars, projectId, threadId, }); @@ -1848,6 +1861,7 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { projectId, providerId, sessionRestorable: false, + threadEnvVars: envVars, }); if (providerThreadId) { @@ -1923,6 +1937,7 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { clientRequestId, options: execOpts, contributedEnv, + envVars, instructions, }) { return runThreadOperation({ @@ -1948,9 +1963,11 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { } const resolvedContributedEnv = contributedEnv ?? currentConfig.contributedEnv; + const resolvedThreadEnvVars = envVars ?? currentConfig.threadEnvVars; const resolvedEnvironment = resolveRuntimeThreadEnvironment({ contributedEnv: resolvedContributedEnv, environmentId: currentConfig.environmentId, + envVars: resolvedThreadEnvVars, projectId: currentConfig.projectId, threadId, }); @@ -2004,6 +2021,7 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { contributedEnv: resolvedContributedEnv, envVars: resolvedEnvironment.envVars, options: execOpts, + threadEnvVars: resolvedThreadEnvVars, }); if (environmentChanged) { emitResolvedProviderEnvironment({ @@ -2030,6 +2048,7 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { clientRequestId, options: execOpts, contributedEnv, + envVars, instructions, }) { return runThreadOperation({ @@ -2066,9 +2085,11 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { } const resolvedContributedEnv = contributedEnv ?? currentConfig.contributedEnv; + const resolvedThreadEnvVars = envVars ?? currentConfig.threadEnvVars; const resolvedEnvironment = resolveRuntimeThreadEnvironment({ contributedEnv: resolvedContributedEnv, environmentId: currentConfig.environmentId, + envVars: resolvedThreadEnvVars, projectId: currentConfig.projectId, threadId, }); @@ -2117,6 +2138,7 @@ export function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime { contributedEnv: resolvedContributedEnv, envVars: resolvedEnvironment.envVars, options: execOpts, + threadEnvVars: resolvedThreadEnvVars, }); if (environmentChanged) { emitResolvedProviderEnvironment({ diff --git a/packages/agent-runtime/src/thread-shell-environment.ts b/packages/agent-runtime/src/thread-shell-environment.ts index c6aedd5b41..18f3f9e17a 100644 --- a/packages/agent-runtime/src/thread-shell-environment.ts +++ b/packages/agent-runtime/src/thread-shell-environment.ts @@ -28,12 +28,19 @@ export function buildThreadShellEnvironment( }; } -export interface ResolvedThreadEnvironmentEntry { - name: string; - source: "shell" | { plugin: string }; - value: string | { masked: true }; - reason?: string; -} +export type ResolvedThreadEnvironmentEntry = + | { + name: string; + source: "thread"; + value: { masked: true }; + reason?: string; + } + | { + name: string; + source: "shell" | { plugin: string }; + value: string | { masked: true }; + reason?: string; + }; export interface DroppedThreadEnvironmentContribution { name: string; @@ -43,6 +50,7 @@ export interface DroppedThreadEnvironmentContribution { interface ResolveThreadEnvironmentArgs extends ThreadShellEnvironmentArgs { baseShellEnv: AgentRuntimeShellEnvironment | undefined; contributedEnv: readonly AgentRuntimeContributedEnvEntry[]; + envVars: AgentRuntimeShellEnvironment; } export function resolveThreadEnvironment(args: ResolveThreadEnvironmentArgs): { @@ -88,5 +96,21 @@ export function resolveThreadEnvironment(args: ResolveThreadEnvironmentArgs): { reason: contribution.reason, }); } - return { droppedContributions, envVars, entries }; + const threadEnvEntries = Object.entries(args.envVars).filter( + ([name]) => !name.startsWith("BB_"), + ); + const resolvedEnvVars = { + ...envVars, + ...Object.fromEntries(threadEnvEntries), + }; + for (const [name] of threadEnvEntries) { + const existingIndex = entries.findIndex((entry) => entry.name === name); + if (existingIndex !== -1) entries.splice(existingIndex, 1); + entries.push({ + name, + source: "thread", + value: { masked: true }, + }); + } + return { droppedContributions, envVars: resolvedEnvVars, entries }; } diff --git a/packages/agent-runtime/src/types.ts b/packages/agent-runtime/src/types.ts index 8f56d26ebd..9b7ec0ec39 100644 --- a/packages/agent-runtime/src/types.ts +++ b/packages/agent-runtime/src/types.ts @@ -122,6 +122,7 @@ export interface StartThreadArgs { projectId: string; providerId: string; contributedEnv?: readonly AgentRuntimeContributedEnvEntry[]; + envVars?: AgentRuntimeShellEnvironment; clientRequestId?: ClientTurnRequestId; input?: PromptInput[]; inputGroups?: PromptInput[][]; @@ -148,6 +149,7 @@ interface PrepareThreadRewindArgs { projectId: string; providerId: string; contributedEnv?: readonly AgentRuntimeContributedEnvEntry[]; + envVars?: AgentRuntimeShellEnvironment; sourceProviderThreadId: string; retainThroughProviderCheckpoint: string; options: AgentRuntimeExecutionOptions; @@ -173,6 +175,7 @@ export interface ResumeThreadArgs { providerThreadId?: string; providerId: string; contributedEnv?: readonly AgentRuntimeContributedEnvEntry[]; + envVars?: AgentRuntimeShellEnvironment; options: AgentRuntimeExecutionOptions; instructions?: string; dynamicTools?: DynamicTool[]; @@ -191,6 +194,7 @@ export interface RunTurnArgs { clientRequestId: ClientTurnRequestId; options: AgentRuntimeExecutionOptions; contributedEnv?: readonly AgentRuntimeContributedEnvEntry[]; + envVars?: AgentRuntimeShellEnvironment; instructions?: string; } @@ -202,6 +206,7 @@ export interface SteerTurnArgs { clientRequestId: ClientTurnRequestId; options: AgentRuntimeExecutionOptions; contributedEnv?: readonly AgentRuntimeContributedEnvEntry[]; + envVars?: AgentRuntimeShellEnvironment; instructions?: string; } diff --git a/packages/bb-app/README.md b/packages/bb-app/README.md index f1420dcff9..bcb5076444 100644 --- a/packages/bb-app/README.md +++ b/packages/bb-app/README.md @@ -128,6 +128,7 @@ const bb = new BBSdk(); const thread = await bb.threads.spawn({ projectId: "proj_personal", environment: { type: "host", workspace: { type: "personal" } }, + envVars: { MULTICA_TASK_ID: "task-123" }, prompt: "Summarize my active bb work.", }); await bb.threads.wait({ threadId: String(thread.id), status: "idle" }); @@ -136,8 +137,10 @@ console.log(await bb.threads.output({ threadId: String(thread.id) })); `new BBSdk()` uses the same `BB_SERVER_URL` and bb config resolution as the CLI. Pass `new BBSdk({ baseUrl: "http://host:38886" })` for remote or test -targets (see the remote-access note below). Scripts launched by bb already receive `BB_SERVER_URL` and -`BB_THREAD_ID` in their environment. +targets (see the remote-access note below). `envVars` applies process +configuration to that thread across later turns and provider-session resumes. +Names beginning with `BB_` are reserved. Scripts launched by bb already +receive `BB_SERVER_URL` and `BB_THREAD_ID` in their environment. ## Provider Credentials diff --git a/packages/db/drizzle/0115_familiar_thunderbird.sql b/packages/db/drizzle/0115_familiar_thunderbird.sql new file mode 100644 index 0000000000..03299327b2 --- /dev/null +++ b/packages/db/drizzle/0115_familiar_thunderbird.sql @@ -0,0 +1 @@ +ALTER TABLE `threads` ADD `env_vars_json` text DEFAULT '{}' NOT NULL; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0115_snapshot.json b/packages/db/drizzle/meta/0115_snapshot.json new file mode 100644 index 0000000000..ba31cde0d9 --- /dev/null +++ b/packages/db/drizzle/meta/0115_snapshot.json @@ -0,0 +1,4187 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "37175819-26fd-44f9-a541-600159d64fa1", + "prevId": "bd7bafd1-abb9-4c3e-871c-9c7a26aeb3cf", + "tables": { + "app_settings": { + "name": "app_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "caffeinate": { + "name": "caffeinate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_keyboard_hints": { + "name": "show_keyboard_hints", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "steer_active_thread_on_enter": { + "name": "steer_active_thread_on_enter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_unhandled_provider_events": { + "name": "show_unhandled_provider_events", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "codex_memory_enabled": { + "name": "codex_memory_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "claude_code_memory_enabled": { + "name": "claude_code_memory_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "codex_subagents_disabled": { + "name": "codex_subagents_disabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "claude_code_subagents_disabled": { + "name": "claude_code_subagents_disabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "claude_code_workflows_disabled": { + "name": "claude_code_workflows_disabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "keybinding_overrides": { + "name": "keybinding_overrides", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "onboarding_completed_at": { + "name": "onboarding_completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_settings_values": { + "name": "app_settings_values", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_theme": { + "name": "app_theme", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "theme_id": { + "name": "theme_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "favicon_color": { + "name": "favicon_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apikey": { + "name": "apikey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "referenceId": { + "name": "referenceId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refillInterval": { + "name": "refillInterval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refillAmount": { + "name": "refillAmount", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastRefillAt": { + "name": "lastRefillAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rateLimitEnabled": { + "name": "rateLimitEnabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rateLimitTimeWindow": { + "name": "rateLimitTimeWindow", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rateLimitMax": { + "name": "rateLimitMax", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "requestCount": { + "name": "requestCount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastRequest": { + "name": "lastRequest", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "configId": { + "name": "configId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "apikey_key_unique": { + "name": "apikey_key_unique", + "columns": [ + "key" + ], + "isUnique": true + }, + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + "referenceId" + ], + "isUnique": false + }, + "apikey_config_id_idx": { + "name": "apikey_config_id_idx", + "columns": [ + "configId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "apikey_referenceId_user_id_fk": { + "name": "apikey_referenceId_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "referenceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_launches": { + "name": "environment_launches", + "columns": { + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_plugin_id": { + "name": "provider_plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path_rejected": { + "name": "path_rejected", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "failed_at": { + "name": "failed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "failure": { + "name": "failure", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "transient_failures": { + "name": "transient_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path_key": { + "name": "path_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_path": { + "name": "claim_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owns_path": { + "name": "owns_path", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "merge_base_branch": { + "name": "merge_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "step_text": { + "name": "step_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pending_log": { + "name": "pending_log", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "replaced_environment_id": { + "name": "replaced_environment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selection": { + "name": "selection", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request": { + "name": "request", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cancel_pending": { + "name": "cancel_pending", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environment_launches_phase_idx": { + "name": "environment_launches_phase_idx", + "columns": [ + "phase" + ], + "isUnique": false + }, + "environment_launches_active_claim_idx": { + "name": "environment_launches_active_claim_idx", + "columns": [ + "host_id", + "claim_path" + ], + "isUnique": false, + "where": "\"environment_launches\".\"environment_id\" is null and \"environment_launches\".\"claim_path\" is not null" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environments": { + "name": "environments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_git_repo": { + "name": "is_git_repo", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_worktree": { + "name": "is_worktree", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "merge_base_branch": { + "name": "merge_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_provider_id": { + "name": "environment_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_provider_plugin_id": { + "name": "environment_provider_plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_owns_path": { + "name": "provider_owns_path", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "environment_provider_selection": { + "name": "environment_provider_selection", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_provider_instance_key": { + "name": "environment_provider_instance_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retire_at": { + "name": "retire_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "teardown_attempt": { + "name": "teardown_attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "teardown_status": { + "name": "teardown_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "teardown_message": { + "name": "teardown_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'provisioning'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "environments_project_host_path_idx": { + "name": "environments_project_host_path_idx", + "columns": [ + "project_id", + "host_id", + "path" + ], + "isUnique": true + }, + "environments_host_path_lookup_idx": { + "name": "environments_host_path_lookup_idx", + "columns": [ + "host_id", + "path" + ], + "isUnique": false + }, + "environments_project_idx": { + "name": "environments_project_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "environments_status_idx": { + "name": "environments_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "environments_provider_instance_idx": { + "name": "environments_provider_instance_idx", + "columns": [ + "environment_provider_id", + "environment_provider_instance_key" + ], + "isUnique": false + } + }, + "foreignKeys": { + "environments_project_id_projects_id_fk": { + "name": "environments_project_id_projects_id_fk", + "tableFrom": "environments", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environments_host_id_hosts_id_fk": { + "name": "environments_host_id_hosts_id_fk", + "tableFrom": "environments", + "tableTo": "hosts", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "events": { + "name": "events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_thread_id": { + "name": "provider_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "item_kind": { + "name": "item_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_tool_call_id": { + "name": "parent_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "events_thread_sequence_idx": { + "name": "events_thread_sequence_idx", + "columns": [ + "thread_id", + "sequence" + ], + "isUnique": true + }, + "events_delegating_item_lookup_idx": { + "name": "events_delegating_item_lookup_idx", + "columns": [ + "thread_id", + "item_id", + "sequence", + "item_kind" + ], + "isUnique": false, + "where": "\"events\".\"item_kind\" IN ('toolCall', 'delegation')" + }, + "events_plan_steps_thread_sequence_idx": { + "name": "events_plan_steps_thread_sequence_idx", + "columns": [ + "thread_id", + "sequence" + ], + "isUnique": false, + "where": "(\"events\".\"item_kind\" = 'planSteps' AND \"events\".\"type\" = 'item/completed') OR \"events\".\"type\" = 'turn/plan/updated'" + }, + "events_parent_tool_call_thread_parent_sequence_idx": { + "name": "events_parent_tool_call_thread_parent_sequence_idx", + "columns": [ + "thread_id", + "parent_tool_call_id", + "sequence" + ], + "isUnique": false, + "where": "\"events\".\"parent_tool_call_id\" IS NOT NULL" + }, + "events_thread_type_item_kind_sequence_idx": { + "name": "events_thread_type_item_kind_sequence_idx", + "columns": [ + "thread_id", + "type", + "item_kind", + "sequence" + ], + "isUnique": false + }, + "events_background_task_thread_type_item_sequence_idx": { + "name": "events_background_task_thread_type_item_sequence_idx", + "columns": [ + "thread_id", + "type", + "item_id", + "sequence" + ], + "isUnique": false, + "where": "\"events\".\"item_kind\" = 'backgroundTask'" + }, + "events_thread_type_sequence_idx": { + "name": "events_thread_type_sequence_idx", + "columns": [ + "thread_id", + "type", + "sequence" + ], + "isUnique": false + }, + "events_thread_turn_type_item_sequence_idx": { + "name": "events_thread_turn_type_item_sequence_idx", + "columns": [ + "thread_id", + "turn_id", + "type", + "item_id", + "sequence" + ], + "isUnique": false + }, + "events_item_lifecycle_thread_item_sequence_idx": { + "name": "events_item_lifecycle_thread_item_sequence_idx", + "columns": [ + "thread_id", + "item_id", + "sequence" + ], + "isUnique": false, + "where": "\"events\".\"type\" IN ('item/started', 'item/completed', 'item/backgroundTask/completed')" + }, + "events_environment_idx": { + "name": "events_environment_idx", + "columns": [ + "environment_id" + ], + "isUnique": false + }, + "events_completed_item_truncation_idx": { + "name": "events_completed_item_truncation_idx", + "columns": [ + "item_kind", + "created_at", + "id" + ], + "isUnique": false, + "where": "\"events\".\"type\" = 'item/completed'" + }, + "events_thread_state_thread_sequence_idx": { + "name": "events_thread_state_thread_sequence_idx", + "columns": [ + "thread_id", + "sequence" + ], + "isUnique": false, + "where": "\"events\".\"type\" IN ('thread/goal/updated', 'thread/goal/cleared', 'thread/extensionState/updated')" + } + }, + "foreignKeys": { + "events_thread_id_threads_id_fk": { + "name": "events_thread_id_threads_id_fk", + "tableFrom": "events", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "events_environment_id_environments_id_fk": { + "name": "events_environment_id_environments_id_fk", + "tableFrom": "events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "events_scope_shape_check": { + "name": "events_scope_shape_check", + "value": "(\n (\"events\".\"scope_kind\" = 'turn' AND \"events\".\"turn_id\" IS NOT NULL)\n OR\n (\"events\".\"scope_kind\" = 'thread' AND \"events\".\"turn_id\" IS NULL)\n )" + } + } + }, + "host_daemon_sessions": { + "name": "host_daemon_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_type": { + "name": "host_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data_dir": { + "name": "data_dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heartbeat_interval_ms": { + "name": "heartbeat_interval_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lease_timeout_ms": { + "name": "lease_timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "closed_at": { + "name": "closed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "host_daemon_sessions_host_status_idx": { + "name": "host_daemon_sessions_host_status_idx", + "columns": [ + "host_id", + "status" + ], + "isUnique": false + }, + "host_daemon_sessions_host_latest_idx": { + "name": "host_daemon_sessions_host_latest_idx", + "columns": [ + "host_id", + "updated_at", + "created_at", + "id" + ], + "isUnique": false + }, + "host_daemon_sessions_closed_prune_idx": { + "name": "host_daemon_sessions_closed_prune_idx", + "columns": [ + "status", + "closed_at", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_daemon_sessions_host_id_hosts_id_fk": { + "name": "host_daemon_sessions_host_id_hosts_id_fk", + "tableFrom": "host_daemon_sessions", + "tableTo": "hosts", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "hosts": { + "name": "hosts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connect_machine_id": { + "name": "connect_machine_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_permission_mode": { + "name": "max_permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'full'" + }, + "destroyed_at": { + "name": "destroyed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_rejected_protocol_version": { + "name": "last_rejected_protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "hosts_last_seen_idx": { + "name": "hosts_last_seen_idx", + "columns": [ + "last_seen_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provenance": { + "name": "provenance", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'direct'" + }, + "catalog_entry_id": { + "name": "catalog_entry_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "catalog_marketplace_name": { + "name": "catalog_marketplace_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'path'" + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_builtin_name": { + "name": "source_builtin_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_npm_package": { + "name": "source_npm_package", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_npm_registry": { + "name": "source_npm_registry", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_npm_requested_spec": { + "name": "source_npm_requested_spec", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_npm_spec_kind": { + "name": "source_npm_spec_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_url": { + "name": "source_git_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_subdirectory": { + "name": "source_git_subdirectory", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_requested_ref": { + "name": "source_git_requested_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_ref_kind": { + "name": "source_git_ref_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_range": { + "name": "source_git_range", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_tag_prefix": { + "name": "source_git_tag_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_resolved_tag": { + "name": "source_git_resolved_tag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "npm_resolved_version": { + "name": "npm_resolved_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "npm_integrity": { + "name": "npm_integrity", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_resolved_commit": { + "name": "git_resolved_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_update_check_at": { + "name": "last_update_check_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "available_compatible_version": { + "name": "available_compatible_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "newest_incompatible_version": { + "name": "newest_incompatible_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "update_status_detail": { + "name": "update_status_detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_failure_version": { + "name": "last_failure_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_failure_at": { + "name": "last_failure_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_failure_detail": { + "name": "last_failure_detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_artifact_id": { + "name": "active_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "normalization_version": { + "name": "normalization_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "root_dir": { + "name": "root_dir", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "removed_at": { + "name": "removed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "plugins_active_artifact_id_plugin_artifacts_id_fk": { + "name": "plugins_active_artifact_id_plugin_artifacts_id_fk", + "tableFrom": "plugins", + "tableTo": "plugin_artifacts", + "columnsFrom": [ + "active_artifact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "maintenance_scan_cursors": { + "name": "maintenance_scan_cursors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_kind": { + "name": "item_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_path": { + "name": "output_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_created_at": { + "name": "last_created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_event_id": { + "name": "last_event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "maintenance_scan_cursors_path_idx": { + "name": "maintenance_scan_cursors_path_idx", + "columns": [ + "policy", + "version", + "item_kind", + "output_path" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "pending_interactions": { + "name": "pending_interactions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'provider'" + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_thread_id": { + "name": "provider_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_request_id": { + "name": "provider_request_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "renderer_id": { + "name": "renderer_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_reason": { + "name": "status_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "pending_interactions_provider_request_idx": { + "name": "pending_interactions_provider_request_idx", + "columns": [ + "provider_id", + "provider_thread_id", + "provider_request_id" + ], + "isUnique": true + }, + "pending_interactions_thread_created_idx": { + "name": "pending_interactions_thread_created_idx", + "columns": [ + "thread_id", + "created_at" + ], + "isUnique": false + }, + "pending_interactions_thread_status_created_idx": { + "name": "pending_interactions_thread_status_created_idx", + "columns": [ + "thread_id", + "status", + "created_at" + ], + "isUnique": false + }, + "pending_interactions_status_created_idx": { + "name": "pending_interactions_status_created_idx", + "columns": [ + "status", + "created_at" + ], + "isUnique": false + }, + "pending_interactions_plugin_status_created_idx": { + "name": "pending_interactions_plugin_status_created_idx", + "columns": [ + "plugin_id", + "status", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "pending_interactions_thread_id_threads_id_fk": { + "name": "pending_interactions_thread_id_threads_id_fk", + "tableFrom": "pending_interactions", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_artifacts": { + "name": "plugin_artifacts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "npm_resolved_version": { + "name": "npm_resolved_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_resolved_commit": { + "name": "git_resolved_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_checkout_root": { + "name": "git_checkout_root", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integrity": { + "name": "integrity", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "validation_result": { + "name": "validation_result", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validated_at": { + "name": "validated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "plugin_artifacts_plugin_idx": { + "name": "plugin_artifacts_plugin_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_kv": { + "name": "plugin_kv", + "columns": { + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_kv_plugin_id_key_pk": { + "columns": [ + "plugin_id", + "key" + ], + "name": "plugin_kv_plugin_id_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_marketplace_icons": { + "name": "plugin_marketplace_icons", + "columns": { + "marketplace_name": { + "name": "marketplace_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entry_id": { + "name": "entry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_marketplace_icons_marketplace_name_entry_id_pk": { + "columns": [ + "marketplace_name", + "entry_id" + ], + "name": "plugin_marketplace_icons_marketplace_name_entry_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_marketplaces": { + "name": "plugin_marketplaces", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'https'" + }, + "manifest_url": { + "name": "manifest_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_git_ref": { + "name": "source_git_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_git_commit": { + "name": "source_git_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stats_json": { + "name": "stats_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_modified": { + "name": "last_modified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_successful_refresh_at": { + "name": "last_successful_refresh_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_attempted_refresh_at": { + "name": "last_attempted_refresh_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_schedules": { + "name": "plugin_schedules", + "columns": { + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_status": { + "name": "last_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_schedules_plugin_id_name_pk": { + "columns": [ + "plugin_id", + "name" + ], + "name": "plugin_schedules_plugin_id_name_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_settings_plugin_id_key_pk": { + "columns": [ + "plugin_id", + "key" + ], + "name": "plugin_settings_plugin_id_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_state_snapshots": { + "name": "plugin_state_snapshots", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "from_artifact_id": { + "name": "from_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "to_artifact_id": { + "name": "to_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "snapshot_path": { + "name": "snapshot_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "database_path": { + "name": "database_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state_path": { + "name": "state_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets_path": { + "name": "secrets_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registration_path": { + "name": "registration_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rollback_candidate_version": { + "name": "rollback_candidate_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rollback_source_fingerprint": { + "name": "rollback_source_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rollback_bb_version": { + "name": "rollback_bb_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rollback_sdk_version": { + "name": "rollback_sdk_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rollback_detail": { + "name": "rollback_detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retained_until": { + "name": "retained_until", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "plugin_state_snapshots_plugin_idx": { + "name": "plugin_state_snapshots_plugin_idx", + "columns": [ + "plugin_id" + ], + "isUnique": false + }, + "plugin_state_snapshots_retention_idx": { + "name": "plugin_state_snapshots_retention_idx", + "columns": [ + "retained_until" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_execution_defaults": { + "name": "project_execution_defaults", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_tier": { + "name": "service_tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reasoning_level": { + "name": "reasoning_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_mode": { + "name": "permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_execution_defaults_project_idx": { + "name": "project_execution_defaults_project_idx", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_execution_defaults_project_id_projects_id_fk": { + "name": "project_execution_defaults_project_id_projects_id_fk", + "tableFrom": "project_execution_defaults", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_sources": { + "name": "project_sources", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_sources_project_idx": { + "name": "project_sources_project_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "project_sources_host_idx": { + "name": "project_sources_host_idx", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "project_sources_project_host_idx": { + "name": "project_sources_project_host_idx", + "columns": [ + "project_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_sources_project_id_projects_id_fk": { + "name": "project_sources_project_id_projects_id_fk", + "tableFrom": "project_sources", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_sources_host_id_hosts_id_fk": { + "name": "project_sources_host_id_hosts_id_fk", + "tableFrom": "project_sources", + "tableTo": "hosts", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "project_sources_shape_check": { + "name": "project_sources_shape_check", + "value": "(\n \"project_sources\".\"type\" = 'local_path' AND \"project_sources\".\"host_id\" IS NOT NULL AND \"project_sources\".\"path\" IS NOT NULL\n )" + } + } + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'standard'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "git_remote_url": { + "name": "git_remote_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_key": { + "name": "sort_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'V'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "projects_updated_idx": { + "name": "projects_updated_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "projects_deleted_idx": { + "name": "projects_deleted_idx", + "columns": [ + "deleted_at" + ], + "isUnique": false + }, + "projects_sort_idx": { + "name": "projects_sort_idx", + "columns": [ + "sort_key", + "id" + ], + "isUnique": false + }, + "projects_personal_singleton_idx": { + "name": "projects_personal_singleton_idx", + "columns": [ + "kind" + ], + "isUnique": true, + "where": "\"projects\".\"kind\" = 'personal'" + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "prompt_history_entries": { + "name": "prompt_history_entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_sequence": { + "name": "request_sequence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input": { + "name": "input", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "prompt_history_entries_thread_request_idx": { + "name": "prompt_history_entries_thread_request_idx", + "columns": [ + "thread_id", + "request_sequence" + ], + "isUnique": true + }, + "prompt_history_entries_project_scope_created_idx": { + "name": "prompt_history_entries_project_scope_created_idx", + "columns": [ + "project_id", + "scope", + "created_at", + "request_sequence", + "id" + ], + "isUnique": false + }, + "prompt_history_entries_thread_scope_created_idx": { + "name": "prompt_history_entries_thread_scope_created_idx", + "columns": [ + "thread_id", + "scope", + "created_at", + "request_sequence", + "id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "prompt_history_entries_project_id_projects_id_fk": { + "name": "prompt_history_entries_project_id_projects_id_fk", + "tableFrom": "prompt_history_entries", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "prompt_history_entries_thread_id_threads_id_fk": { + "name": "prompt_history_entries_thread_id_threads_id_fk", + "tableFrom": "prompt_history_entries", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "queued_thread_messages": { + "name": "queued_thread_messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "system_notice": { + "name": "system_notice", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sender_thread_id": { + "name": "sender_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reasoning_level": { + "name": "reasoning_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_mode": { + "name": "permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_tier": { + "name": "service_tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_with_next": { + "name": "group_with_next", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "send_at": { + "name": "send_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "waiting_on": { + "name": "waiting_on", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wait_holder": { + "name": "wait_holder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_kind": { + "name": "payload_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'inline'" + }, + "retry_of_turn_request_id": { + "name": "retry_of_turn_request_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retry_attempt": { + "name": "retry_attempt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retry_reason": { + "name": "retry_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_key": { + "name": "sort_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "queued_thread_messages_thread_created_idx": { + "name": "queued_thread_messages_thread_created_idx", + "columns": [ + "thread_id", + "created_at", + "id" + ], + "isUnique": false + }, + "queued_thread_messages_thread_sort_idx": { + "name": "queued_thread_messages_thread_sort_idx", + "columns": [ + "thread_id", + "sort_key", + "id" + ], + "isUnique": false + }, + "queued_thread_messages_due_idx": { + "name": "queued_thread_messages_due_idx", + "columns": [ + "send_at", + "id" + ], + "isUnique": false, + "where": "\"queued_thread_messages\".\"send_at\" IS NOT NULL AND \"queued_thread_messages\".\"claimed_at\" IS NULL AND \"queued_thread_messages\".\"claim_token\" IS NULL" + }, + "queued_thread_messages_wait_holder_idx": { + "name": "queued_thread_messages_wait_holder_idx", + "columns": [ + "wait_holder", + "id" + ], + "isUnique": false, + "where": "\"queued_thread_messages\".\"wait_holder\" IS NOT NULL" + } + }, + "foreignKeys": { + "queued_thread_messages_thread_id_threads_id_fk": { + "name": "queued_thread_messages_thread_id_threads_id_fk", + "tableFrom": "queued_thread_messages", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "retained_event_outputs": { + "name": "retained_event_outputs", + "columns": { + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "output_path": { + "name": "output_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "retained_event_outputs_expiry_idx": { + "name": "retained_event_outputs_expiry_idx", + "columns": [ + "expires_at", + "event_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "retained_event_outputs_event_id_events_id_fk": { + "name": "retained_event_outputs_event_id_events_id_fk", + "tableFrom": "retained_event_outputs", + "tableTo": "events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "system_experiments": { + "name": "system_experiments", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "terminal_sessions": { + "name": "terminal_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "daemon_session_id": { + "name": "daemon_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "initial_cwd": { + "name": "initial_cwd", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cols": { + "name": "cols", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rows": { + "name": "rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_user_input_at": { + "name": "last_user_input_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "terminal_sessions_thread_status_updated_idx": { + "name": "terminal_sessions_thread_status_updated_idx", + "columns": [ + "thread_id", + "status", + "updated_at" + ], + "isUnique": false + }, + "terminal_sessions_environment_status_idx": { + "name": "terminal_sessions_environment_status_idx", + "columns": [ + "environment_id", + "status" + ], + "isUnique": false + }, + "terminal_sessions_host_status_idx": { + "name": "terminal_sessions_host_status_idx", + "columns": [ + "host_id", + "status" + ], + "isUnique": false + }, + "terminal_sessions_daemon_session_idx": { + "name": "terminal_sessions_daemon_session_idx", + "columns": [ + "daemon_session_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "terminal_sessions_thread_id_threads_id_fk": { + "name": "terminal_sessions_thread_id_threads_id_fk", + "tableFrom": "terminal_sessions", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "terminal_sessions_environment_id_environments_id_fk": { + "name": "terminal_sessions_environment_id_environments_id_fk", + "tableFrom": "terminal_sessions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "terminal_sessions_host_id_hosts_id_fk": { + "name": "terminal_sessions_host_id_hosts_id_fk", + "tableFrom": "terminal_sessions", + "tableTo": "hosts", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "terminal_sessions_daemon_session_id_host_daemon_sessions_id_fk": { + "name": "terminal_sessions_daemon_session_id_host_daemon_sessions_id_fk", + "tableFrom": "terminal_sessions", + "tableTo": "host_daemon_sessions", + "columnsFrom": [ + "daemon_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_conversation_outlines": { + "name": "thread_conversation_outlines", + "columns": { + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "projection_key": { + "name": "projection_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "items_json": { + "name": "items_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "thread_conversation_outlines_thread_id_threads_id_fk": { + "name": "thread_conversation_outlines_thread_id_threads_id_fk", + "tableFrom": "thread_conversation_outlines", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_dynamic_context_file_states": { + "name": "thread_dynamic_context_file_states", + "columns": { + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_key": { + "name": "file_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_status": { + "name": "content_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "shown_at": { + "name": "shown_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "thread_dynamic_context_file_states_thread_file_idx": { + "name": "thread_dynamic_context_file_states_thread_file_idx", + "columns": [ + "thread_id", + "file_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "thread_dynamic_context_file_states_thread_id_threads_id_fk": { + "name": "thread_dynamic_context_file_states_thread_id_threads_id_fk", + "tableFrom": "thread_dynamic_context_file_states", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_search_segments": { + "name": "thread_search_segments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_seq": { + "name": "source_seq", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "thread_search_segments_source_idx": { + "name": "thread_search_segments_source_idx", + "columns": [ + "thread_id", + "source_kind", + "source_key" + ], + "isUnique": true + }, + "thread_search_segments_thread_source_seq_idx": { + "name": "thread_search_segments_thread_source_seq_idx", + "columns": [ + "thread_id", + "source_seq" + ], + "isUnique": false + } + }, + "foreignKeys": { + "thread_search_segments_thread_id_threads_id_fk": { + "name": "thread_search_segments_thread_id_threads_id_fk", + "tableFrom": "thread_search_segments", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_sections": { + "name": "thread_sections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "thread_sections_name_idx": { + "name": "thread_sections_name_idx", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_tabs": { + "name": "thread_tabs", + "columns": { + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tabs_json": { + "name": "tabs_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "thread_tabs_thread_id_threads_id_fk": { + "name": "thread_tabs_thread_id_threads_id_fk", + "tableFrom": "thread_tabs", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "threads": { + "name": "threads", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "env_vars_json": { + "name": "env_vars_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "model_override": { + "name": "model_override", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reasoning_level_override": { + "name": "reasoning_level_override", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title_fallback": { + "name": "title_fallback", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'starting'" + }, + "pending_start_context": { + "name": "pending_start_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_thread_id": { + "name": "parent_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_thread_id": { + "name": "source_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin_plugin_id": { + "name": "origin_plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'visible'" + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_sort_key": { + "name": "pin_sort_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_read_at": { + "name": "last_read_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "latest_attention_at": { + "name": "latest_attention_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "threads_project_updated_idx": { + "name": "threads_project_updated_idx", + "columns": [ + "project_id", + "updated_at" + ], + "isUnique": false + }, + "threads_project_archived_deleted_idx": { + "name": "threads_project_archived_deleted_idx", + "columns": [ + "project_id", + "archived_at", + "deleted_at", + "id" + ], + "isUnique": false + }, + "threads_pin_sort_idx": { + "name": "threads_pin_sort_idx", + "columns": [ + "archived_at", + "deleted_at", + "pin_sort_key", + "id" + ], + "isUnique": false, + "where": "\"threads\".\"pinned_at\" IS NOT NULL" + }, + "threads_environment_idx": { + "name": "threads_environment_idx", + "columns": [ + "environment_id" + ], + "isUnique": false + }, + "threads_parent_idx": { + "name": "threads_parent_idx", + "columns": [ + "parent_thread_id" + ], + "isUnique": false + }, + "threads_source_origin_idx": { + "name": "threads_source_origin_idx", + "columns": [ + "source_thread_id", + "origin_kind" + ], + "isUnique": false + }, + "threads_origin_plugin_archived_idx": { + "name": "threads_origin_plugin_archived_idx", + "columns": [ + "origin_plugin_id", + "archived_at" + ], + "isUnique": false + }, + "threads_section_archived_deleted_idx": { + "name": "threads_section_archived_deleted_idx", + "columns": [ + "section_id", + "archived_at", + "deleted_at", + "id" + ], + "isUnique": false + }, + "threads_archived_status_idx": { + "name": "threads_archived_status_idx", + "columns": [ + "archived_at", + "status" + ], + "isUnique": false + }, + "threads_environment_archived_deleted_idx": { + "name": "threads_environment_archived_deleted_idx", + "columns": [ + "environment_id", + "archived_at", + "deleted_at" + ], + "isUnique": false + }, + "threads_active_maintenance_idx": { + "name": "threads_active_maintenance_idx", + "columns": [ + "status" + ], + "isUnique": false, + "where": "\"threads\".\"deleted_at\" IS NULL" + } + }, + "foreignKeys": { + "threads_project_id_projects_id_fk": { + "name": "threads_project_id_projects_id_fk", + "tableFrom": "threads", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "threads_environment_id_environments_id_fk": { + "name": "threads_environment_id_environments_id_fk", + "tableFrom": "threads", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "threads_section_id_thread_sections_id_fk": { + "name": "threads_section_id_thread_sections_id_fk", + "tableFrom": "threads", + "tableTo": "thread_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "threads_parent_thread_id_threads_id_fk": { + "name": "threads_parent_thread_id_threads_id_fk", + "tableFrom": "threads", + "tableTo": "threads", + "columnsFrom": [ + "parent_thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "threads_source_thread_id_threads_id_fk": { + "name": "threads_source_thread_id_threads_id_fk", + "tableFrom": "threads", + "tableTo": "threads", + "columnsFrom": [ + "source_thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 6d6ca80ebf..25d6562a92 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -806,6 +806,13 @@ "when": 1788898395603, "tag": "0114_public_iron_lad", "breakpoints": true + }, + { + "idx": 115, + "version": "6", + "when": 1788917778275, + "tag": "0115_familiar_thunderbird", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/data/index.ts b/packages/db/src/data/index.ts index 473e8af8f1..a2a21a02b6 100644 --- a/packages/db/src/data/index.ts +++ b/packages/db/src/data/index.ts @@ -63,6 +63,7 @@ export { countThreads, countNonDeletedAssignedChildThreads, getThread, + getThreadEnvVars, getThreadExecutionOverride, hasActiveThreadAttention, setThreadExecutionOverride, diff --git a/packages/db/src/data/threads.ts b/packages/db/src/data/threads.ts index 335e19fa74..59382a0c1c 100644 --- a/packages/db/src/data/threads.ts +++ b/packages/db/src/data/threads.ts @@ -17,6 +17,7 @@ import { import type { ReasoningLevel, ThreadChangeKind, + ThreadEnvVars, ThreadLifecycleEvent, ThreadLifecycleNoopReason, ThreadOriginKind, @@ -26,6 +27,7 @@ import type { } from "@bb/domain"; import { evaluateThreadLifecycleEvent, + threadEnvVarsSchema, threadSearchSourceKindSchema, } from "@bb/domain"; import type { DbConnection, DbTransaction } from "../connection.js"; @@ -264,6 +266,7 @@ export interface CreateThreadInput { projectId: string; environmentId?: string | null; providerId: string; + envVars?: ThreadEnvVars; title?: string | null; titleFallback?: string | null; sectionId?: string | null; @@ -293,6 +296,7 @@ export function createThread( projectId: input.projectId, environmentId: input.environmentId ?? null, providerId: input.providerId, + envVarsJson: JSON.stringify(input.envVars ?? {}), title: input.title ?? null, titleFallback: input.titleFallback ?? null, sectionId: input.sectionId ?? null, @@ -333,6 +337,19 @@ export function getThread(db: ThreadWriteConnection, id: string) { return db.select().from(threads).where(eq(threads.id, id)).get() ?? null; } +export function getThreadEnvVars( + db: ThreadWriteConnection, + id: string, +): ThreadEnvVars | null { + const row = db + .select({ envVarsJson: threads.envVarsJson }) + .from(threads) + .where(eq(threads.id, id)) + .get(); + if (row === undefined) return null; + return threadEnvVarsSchema.parse(JSON.parse(row.envVarsJson)); +} + export interface ThreadMentionRow { id: string; projectId: string; diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 60781fa5b6..ad02f961ce 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -505,6 +505,7 @@ export const threads = sqliteTable( onDelete: "set null", }), providerId: text("provider_id").notNull(), + envVarsJson: text("env_vars_json").notNull().default("{}"), modelOverride: text("model_override"), reasoningLevelOverride: text( "reasoning_level_override", diff --git a/packages/db/test/data/threads.test.ts b/packages/db/test/data/threads.test.ts index 6ccdec96de..689d80371c 100644 --- a/packages/db/test/data/threads.test.ts +++ b/packages/db/test/data/threads.test.ts @@ -8,6 +8,7 @@ import { countLiveThreadsInEnvironment, countNonDeletedAssignedChildThreads, getThread, + getThreadEnvVars, getThreadExecutionOverride, hasActiveThreadAttention, setThreadExecutionOverride, @@ -135,6 +136,25 @@ describe("threads", () => { const fetched = getThread(db, thread.id); expect(fetched?.visibility).toBe("visible"); expect(fetched).toMatchObject({ id: thread.id }); + expect(getThreadEnvVars(db, thread.id)).toEqual({}); + }); + + it("persists environment variables with the thread", () => { + const { db, project } = setup(); + const thread = createThread(db, noopNotifier, { + projectId: project.id, + providerId: "codex", + envVars: { + MULTICA_TASK_ID: "task-123", + MULTICA_TOKEN: "prefix=value", + }, + }); + + expect(getThreadEnvVars(db, thread.id)).toEqual({ + MULTICA_TASK_ID: "task-123", + MULTICA_TOKEN: "prefix=value", + }); + expect(getThreadEnvVars(db, "thr_2222222222")).toBeNull(); }); it("resolves only exact non-deleted mention thread rows", () => { diff --git a/packages/db/test/migrate.test.ts b/packages/db/test/migrate.test.ts index f8f9c066bc..800d17a1eb 100644 --- a/packages/db/test/migrate.test.ts +++ b/packages/db/test/migrate.test.ts @@ -312,6 +312,15 @@ function dropThreadConversationOutlinesTable(db: DbConnection): void { db.$client.prepare("DROP TABLE IF EXISTS thread_conversation_outlines").run(); } +function dropThreadEnvVarsColumn(db: DbConnection): void { + const columns = db.$client + .prepare<[], TableInfoRow>("PRAGMA table_info(threads)") + .all(); + if (columns.some((column) => column.name === "env_vars_json")) { + db.$client.prepare("ALTER TABLE threads DROP COLUMN env_vars_json").run(); + } +} + function dropRewindAddedTables(db: DbConnection): void { rewindEnvironmentRowFactsMigration(db); rewindEnvironmentProvidersMigration(db); @@ -329,6 +338,7 @@ function dropRewindAddedTables(db: DbConnection): void { dropMarketplaceCatalogSchema(db); dropEventParentToolCallIdColumn(db); dropQueueReworkSchema(db); + dropThreadEnvVarsColumn(db); db.$client.prepare("DROP TABLE IF EXISTS plugins").run(); db.$client.prepare("DROP TABLE IF EXISTS plugin_kv").run(); db.$client.prepare("DROP TABLE IF EXISTS plugin_settings").run(); @@ -423,6 +433,7 @@ const pendingInteractionsMigrationWhen = 1783626227375; const permissionModesMigrationWhen = 1784311522462; const branchLocalThreadTabsMigrationWhen = 1783633750817; const eventParentToolCallMigrationWhen = 1787181956957; +const threadEnvVarsMigrationWhen = 1788917778275; const eventParentToolCallPreJsonValidMigrationHash = "79d39e7b68d1db8ba02614fe4cc227cc0c154d77c7183f2e37ed2d8475412993"; const eventLargeValuesPreOptimizationHash = @@ -2102,6 +2113,48 @@ describe("migrate", () => { } }); + it("backfills an empty environment map for existing threads", () => { + const db = createConnection(":memory:"); + + try { + migrate(db); + const host = upsertHost(db, noopNotifier, { + name: "thread-env-migration-host", + type: "persistent", + }); + const { project } = createProject(db, noopNotifier, { + name: "thread-env-migration-project", + source: { + type: "local_path", + hostId: host.id, + path: "/tmp/thread-env-migration", + }, + }); + const thread = createThread(db, noopNotifier, { + projectId: project.id, + providerId: "codex", + }); + dropThreadEnvVarsColumn(db); + db.$client + .prepare( + "DELETE FROM __drizzle_migrations WHERE created_at = ?", + ) + .run(threadEnvVarsMigrationWhen); + + migrate(db); + + expect( + db.$client + .prepare<[string], { envVarsJson: string }>( + "SELECT env_vars_json AS envVarsJson FROM threads WHERE id = ?", + ) + .get(thread.id), + ).toEqual({ envVarsJson: "{}" }); + } finally { + closeConnection(db); + } + }); + it("keeps a chosen steer preference through the steer default change", () => { const db = createConnection(":memory:"); @@ -2376,6 +2429,7 @@ describe("migrate", () => { dropMarketplaceCatalogSchema(db); dropEventParentToolCallIdColumn(db); dropQueueReworkSchema(db); + dropThreadEnvVarsColumn(db); restoreLegacyThreadOriginColumn(db); migrate(db); @@ -2783,6 +2837,7 @@ describe("migrate", () => { dropMarketplaceCatalogSchema(db); dropEventParentToolCallIdColumn(db); dropQueueReworkSchema(db); + dropThreadEnvVarsColumn(db); restoreLegacyThreadOriginColumn(db); expect( @@ -2887,6 +2942,7 @@ describe("migrate", () => { dropMarketplaceCatalogSchema(db); dropEventParentToolCallIdColumn(db); dropQueueReworkSchema(db); + dropThreadEnvVarsColumn(db); restoreLegacyThreadOriginColumn(db); expect(() => migrate(db)).not.toThrow(); @@ -5460,6 +5516,7 @@ describe("migrate", () => { dropEventParentToolCallIdColumn(db); dropMarketplaceStatsColumn(db); dropQueueReworkSchema(db); + dropThreadEnvVarsColumn(db); db.$client .prepare( "DELETE FROM __drizzle_migrations WHERE created_at >= ?", @@ -5551,6 +5608,7 @@ describe("environment providers migration", () => { const environmentProvidersMigrationWhen = 1788386943764; function seedPreProviderEnvironments(db: DbConnection): void { + dropThreadEnvVarsColumn(db); db.$client.prepare("DROP TABLE retained_event_outputs").run(); rewindEnvironmentRowFactsMigration(db); rewindEnvironmentProvidersMigration(db); diff --git a/packages/domain/src/index.ts b/packages/domain/src/index.ts index ea043c3ee6..e24faf6179 100644 --- a/packages/domain/src/index.ts +++ b/packages/domain/src/index.ts @@ -49,6 +49,7 @@ export * from "./shared-types.js"; export * from "./stored-thread-event.js"; export * from "./terminal.js"; export * from "./thread-dynamic-context.js"; +export * from "./thread-env-vars.js"; export * from "./thread-event-scope.js"; export * from "./thread-events.js"; export * from "./thread-git-diff.js"; diff --git a/packages/domain/src/provider-event.ts b/packages/domain/src/provider-event.ts index b98325c7fe..84c4fd882f 100644 --- a/packages/domain/src/provider-event.ts +++ b/packages/domain/src/provider-event.ts @@ -699,20 +699,30 @@ const unscopedProviderEventSchema = z.discriminatedUnion("type", [ threadId: z.string(), providerThreadId: z.string(), entries: z.array( - z - .object({ - name: z.string(), - source: z.union([ - z.literal("shell"), - z.object({ plugin: z.string() }).strict(), - ]), - value: z.union([ - z.string(), - z.object({ masked: z.literal(true) }).strict(), - ]), - reason: z.string().optional(), - }) - .strict(), + z.union([ + z + .object({ + name: z.string(), + source: z.literal("thread"), + value: z.object({ masked: z.literal(true) }).strict(), + reason: z.string().optional(), + }) + .strict(), + z + .object({ + name: z.string(), + source: z.union([ + z.literal("shell"), + z.object({ plugin: z.string() }).strict(), + ]), + value: z.union([ + z.string(), + z.object({ masked: z.literal(true) }).strict(), + ]), + reason: z.string().optional(), + }) + .strict(), + ]), ), }), z.object({ diff --git a/packages/domain/src/thread-env-vars.ts b/packages/domain/src/thread-env-vars.ts new file mode 100644 index 0000000000..6accc435d1 --- /dev/null +++ b/packages/domain/src/thread-env-vars.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; + +export const THREAD_ENV_VAR_NAME_MAX_CHARS = 128; +export const THREAD_ENV_VAR_VALUE_MAX_BYTES = 16 * 1024; +export const THREAD_ENV_VARS_MAX_BYTES = 64 * 1024; +export const THREAD_ENV_VARS_MAX_ENTRIES = 32; +export const THREAD_ENV_VAR_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u; + +const encoder = new TextEncoder(); + +export const threadEnvVarNameSchema = z.string().superRefine((name, ctx) => { + if (name.length === 0) { + ctx.addIssue({ code: "custom", message: "must not be empty" }); + } else if (name.length > THREAD_ENV_VAR_NAME_MAX_CHARS) { + ctx.addIssue({ + code: "custom", + message: `must be at most ${THREAD_ENV_VAR_NAME_MAX_CHARS} characters`, + }); + } else if (!THREAD_ENV_VAR_NAME_PATTERN.test(name)) { + ctx.addIssue({ + code: "custom", + message: + "must start with a letter or underscore and contain only letters, numbers, and underscores", + }); + } else if (name.startsWith("BB_")) { + ctx.addIssue({ + code: "custom", + message: "must not use the reserved BB_ prefix", + }); + } +}); + +export const threadEnvVarValueSchema = z.string().superRefine((value, ctx) => { + if (value.includes("\0")) { + ctx.addIssue({ code: "custom", message: "must not contain a null byte" }); + } + if (encoder.encode(value).byteLength > THREAD_ENV_VAR_VALUE_MAX_BYTES) { + ctx.addIssue({ + code: "custom", + message: `must be at most ${THREAD_ENV_VAR_VALUE_MAX_BYTES} UTF-8 bytes`, + }); + } +}); + +export const threadEnvVarsSchema = z + .record(threadEnvVarNameSchema, threadEnvVarValueSchema) + .superRefine((envVars, ctx) => { + if (Object.keys(envVars).length > THREAD_ENV_VARS_MAX_ENTRIES) { + ctx.addIssue({ + code: "custom", + message: `must contain at most ${THREAD_ENV_VARS_MAX_ENTRIES} entries`, + }); + } + if ( + encoder.encode(JSON.stringify(envVars)).byteLength > + THREAD_ENV_VARS_MAX_BYTES + ) { + ctx.addIssue({ + code: "custom", + message: `must be at most ${THREAD_ENV_VARS_MAX_BYTES} UTF-8 bytes when serialized`, + }); + } + }); + +export type ThreadEnvVars = z.infer; diff --git a/packages/domain/test/provider-event.test.ts b/packages/domain/test/provider-event.test.ts index 5cc91681b5..5e7e6e58f6 100644 --- a/packages/domain/test/provider-event.test.ts +++ b/packages/domain/test/provider-event.test.ts @@ -51,6 +51,36 @@ describe("provider event schema", () => { }); }); + it("requires thread environment values to stay masked in events", () => { + const event = { + type: "provider.env-resolved", + threadId: "thr_123", + providerThreadId: "provider-thread-123", + scope: { kind: "thread" }, + entries: [ + { + name: "MULTICA_TOKEN", + source: "thread", + value: { masked: true }, + }, + ], + }; + + expect(threadEventSchema.parse(event)).toMatchObject(event); + expect(() => + threadEventSchema.parse({ + ...event, + entries: [ + { + name: "MULTICA_TOKEN", + source: "thread", + value: "thread-secret", + }, + ], + }), + ).toThrow(); + }); + it("uses clientRequestId for accepted input and user-message items", () => { expect( threadEventSchema.parse({ diff --git a/packages/domain/test/thread-env-vars.test.ts b/packages/domain/test/thread-env-vars.test.ts new file mode 100644 index 0000000000..732bbe676a --- /dev/null +++ b/packages/domain/test/thread-env-vars.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from "vitest"; +import { + THREAD_ENV_VAR_NAME_MAX_CHARS, + THREAD_ENV_VAR_VALUE_MAX_BYTES, + THREAD_ENV_VARS_MAX_ENTRIES, + threadEnvVarsSchema, +} from "../src/thread-env-vars.js"; + +describe("thread environment variables", () => { + it("accepts portable names, empty values, and values containing equals signs", () => { + expect( + threadEnvVarsSchema.parse({ + MULTICA_TASK_ID: "task-123", + EMPTY: "", + TOKEN: "prefix=value", + }), + ).toEqual({ + MULTICA_TASK_ID: "task-123", + EMPTY: "", + TOKEN: "prefix=value", + }); + }); + + it.each([ + ["", "must not be empty"], + ["1TOKEN", "must start with a letter or underscore"], + ["TOKEN-NAME", "must start with a letter or underscore"], + ["BB_THREAD_ID", "reserved BB_ prefix"], + ["A".repeat(THREAD_ENV_VAR_NAME_MAX_CHARS + 1), "at most 128 characters"], + ])("rejects the variable name %j", (name, message) => { + expect(() => threadEnvVarsSchema.parse({ [name]: "value" })).toThrow( + message, + ); + }); + + it("bounds values by UTF-8 bytes and rejects null bytes", () => { + expect( + threadEnvVarsSchema.parse({ + VALUE: "é".repeat(THREAD_ENV_VAR_VALUE_MAX_BYTES / 2), + }), + ).toBeDefined(); + expect(() => + threadEnvVarsSchema.parse({ + VALUE: `ok\0bad`, + }), + ).toThrow("must not contain a null byte"); + expect(() => + threadEnvVarsSchema.parse({ + VALUE: "é".repeat(THREAD_ENV_VAR_VALUE_MAX_BYTES / 2 + 1), + }), + ).toThrow("must be at most 16384 UTF-8 bytes"); + }); + + it("bounds the entry count and serialized map size", () => { + const maximumEntries = Object.fromEntries( + Array.from({ length: THREAD_ENV_VARS_MAX_ENTRIES }, (_, index) => [ + `VALUE_${index}`, + "value", + ]), + ); + expect(threadEnvVarsSchema.parse(maximumEntries)).toEqual(maximumEntries); + expect(() => + threadEnvVarsSchema.parse({ + ...maximumEntries, + TOO_MANY: "value", + }), + ).toThrow("must contain at most 32 entries"); + expect(() => + threadEnvVarsSchema.parse({ + VALUE_1: "a".repeat(THREAD_ENV_VAR_VALUE_MAX_BYTES), + VALUE_2: "a".repeat(THREAD_ENV_VAR_VALUE_MAX_BYTES), + VALUE_3: "a".repeat(THREAD_ENV_VAR_VALUE_MAX_BYTES), + VALUE_4: "a".repeat(THREAD_ENV_VAR_VALUE_MAX_BYTES), + }), + ).toThrow("must be at most 65536 UTF-8 bytes when serialized"); + }); +}); diff --git a/packages/host-daemon-contract/src/commands.ts b/packages/host-daemon-contract/src/commands.ts index daa930600d..9de35ff641 100644 --- a/packages/host-daemon-contract/src/commands.ts +++ b/packages/host-daemon-contract/src/commands.ts @@ -14,6 +14,7 @@ import { promptInputSchema, providerForkSchema, threadGitDiffResponseSchema, + threadEnvVarsSchema, runtimeThreadExecutionOptionsSchema, rawDiffFileStatSchema, workspaceDiffTargetSchema, @@ -205,6 +206,7 @@ const hostDaemonThreadRuntimeContextSchema = z instructions: z.string().min(1), dynamicTools: z.array(dynamicToolSchema), contributedEnv: z.array(hostDaemonContributedEnvEntrySchema).default([]), + envVars: threadEnvVarsSchema, injectedSkillSources: z.array(hostDaemonInjectedSkillSourceSchema), disallowedTools: z.array(z.string()).optional(), instructionMode: instructionModeSchema, diff --git a/packages/host-daemon-contract/src/protocol.ts b/packages/host-daemon-contract/src/protocol.ts index 0bf425d2d0..a3e6d97807 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 = 195 as const; +export const HOST_DAEMON_PROTOCOL_VERSION = 196 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 5ac941bea1..8a4e07aff7 100644 --- a/packages/host-daemon-contract/test/contract.test.ts +++ b/packages/host-daemon-contract/test/contract.test.ts @@ -979,7 +979,7 @@ const CONTRIBUTED_ENV = [ describe("host-daemon command schemas", () => { it("uses the current host-daemon protocol version", () => { - expect(HOST_DAEMON_PROTOCOL_VERSION).toBe(195); + expect(HOST_DAEMON_PROTOCOL_VERSION).toBe(196); expect(HOST_ARTIFACT_MAX_BYTES).toBe(256 * 1024 * 1024); }); @@ -1700,6 +1700,7 @@ describe("host-daemon command schemas", () => { instructions: "Be concise.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", requestId: CLIENT_REQUEST_ID, @@ -1735,6 +1736,7 @@ describe("host-daemon command schemas", () => { instructions: "Be concise.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1792,6 +1794,7 @@ describe("host-daemon command schemas", () => { }, ], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "replace", }), @@ -1856,6 +1859,7 @@ describe("host-daemon command schemas", () => { instructions: "Be concise.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append" as const, }; @@ -1918,6 +1922,7 @@ describe("host-daemon command schemas", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -1969,6 +1974,7 @@ describe("host-daemon command schemas", () => { instructions: "Be a helpful thread.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "replace", }; @@ -2013,6 +2019,7 @@ describe("host-daemon command schemas", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -2074,6 +2081,7 @@ describe("host-daemon command schemas", () => { instructions: "Be a helpful thread.", dynamicTools: [], contributedEnv: CONTRIBUTED_ENV, + envVars: { MULTICA_TASK_ID: "task-123" }, injectedSkillSources: [], instructionMode: "append", }; @@ -2111,6 +2119,7 @@ describe("host-daemon command schemas", () => { instructions: "Be a helpful thread.", dynamicTools: [], contributedEnv: CONTRIBUTED_ENV, + envVars: { MULTICA_TASK_ID: "task-123" }, injectedSkillSources: [], instructionMode: "append", }, @@ -2188,6 +2197,7 @@ describe("host-daemon command schemas", () => { instructions: "Be a helpful thread.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }; @@ -2212,6 +2222,7 @@ describe("host-daemon command schemas", () => { instructions: "Be a helpful thread.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -2354,6 +2365,7 @@ describe("host-daemon command schemas", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -2400,6 +2412,7 @@ describe("host-daemon command schemas", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, @@ -2500,6 +2513,7 @@ describe("host-daemon command schemas", () => { instructions: "Be concise.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }), @@ -2535,6 +2549,7 @@ describe("host-daemon command schemas", () => { instructions: "Be a helpful coding agent.", dynamicTools: [], contributedEnv: [], + envVars: {}, injectedSkillSources: [], instructionMode: "append", }, diff --git a/packages/sdk/test/sdk.test.ts b/packages/sdk/test/sdk.test.ts index b0243611a9..4136f9ba0d 100644 --- a/packages/sdk/test/sdk.test.ts +++ b/packages/sdk/test/sdk.test.ts @@ -1077,6 +1077,10 @@ describe("@bb/sdk", () => { hostId: "host_123", workspace: { type: "unmanaged", path: null }, }, + envVars: { + MULTICA_TASK_ID: "task-123", + MULTICA_TOKEN: "prefix=value", + }, prompt: "Ship it", }); @@ -1088,6 +1092,10 @@ describe("@bb/sdk", () => { hostId: "host_123", workspace: { type: "unmanaged", path: null }, }, + envVars: { + MULTICA_TASK_ID: "task-123", + MULTICA_TOKEN: "prefix=value", + }, input: [{ type: "text", text: "Ship it", mentions: [] }], origin: "sdk", startedOnBehalfOf: null, diff --git a/packages/server-contract/src/api/threads.ts b/packages/server-contract/src/api/threads.ts index 8bd31c2426..aeacaa3467 100644 --- a/packages/server-contract/src/api/threads.ts +++ b/packages/server-contract/src/api/threads.ts @@ -26,6 +26,7 @@ import { threadTimelineModelFallbackSchema, threadTimelinePendingTodosSchema, threadEventTypeValues, + threadEnvVarsSchema, threadVisibilitySchema, threadWithRuntimeSchema, } from "@bb/domain"; @@ -107,6 +108,7 @@ export const createThreadRequestSchema = z reasoningLevel: reasoningLevelSchema.optional(), permissionMode: permissionModeInputSchema.optional(), executionInputSources: createExecutionInputSourcesSchema.optional(), + envVars: threadEnvVarsSchema.optional(), environment: createThreadEnvironmentArgsSchema, parentThreadId: z.string().min(1).optional(), sectionId: z.string().min(1).nullable().optional(), diff --git a/packages/server-contract/test/contract.test.ts b/packages/server-contract/test/contract.test.ts index 503bd78523..ef51c96d18 100644 --- a/packages/server-contract/test/contract.test.ts +++ b/packages/server-contract/test/contract.test.ts @@ -85,6 +85,11 @@ const OPTIONAL_SERVER_FIELD_GROUPS: readonly OptionalServerFieldGroup[] = [ "Thread creation may omit visibility for backward compatibility; the server fills visible at the creation boundary.", fields: ["createThreadRequestSchema.visibility"], }, + { + reason: + "Thread creation may omit environment variables when it needs no process overrides; the server fills an empty map at the creation boundary.", + fields: ["createThreadRequestSchema.envVars"], + }, { reason: "Fork creation requires only a source thread; all other fields either select an optional behavior or receive an explicit server-boundary default.", @@ -1342,6 +1347,34 @@ describe("server-contract canonical schemas", () => { expect(parsed.originKind).toBeNull(); }); + it("accepts bounded per-thread environment variables without defaulting omission", () => { + const base = { + projectId: "proj_123", + providerId: "codex", + origin: "sdk" as const, + input: [{ type: "text" as const, text: "Scripted start" }], + environment: { + type: "host" as const, + hostId: "host_abc", + workspace: { type: "unmanaged" as const, path: null }, + }, + }; + + expect(createThreadRequestSchema.parse(base).envVars).toBeUndefined(); + expect( + createThreadRequestSchema.parse({ + ...base, + envVars: { MULTICA_TASK_ID: "task-123", EMPTY: "" }, + }).envVars, + ).toEqual({ MULTICA_TASK_ID: "task-123", EMPTY: "" }); + expect(() => + createThreadRequestSchema.parse({ + ...base, + envVars: { BB_THREAD_ID: "override" }, + }), + ).toThrow("reserved BB_ prefix"); + }); + it("accepts sdk as a thread creation origin", () => { const parsed = createThreadRequestSchema.parse({ projectId: "proj_123", diff --git a/packages/templates/src/templates/bb-guide-threads.md b/packages/templates/src/templates/bb-guide-threads.md index 8da6875af6..b96e5f9224 100644 --- a/packages/templates/src/templates/bb-guide-threads.md +++ b/packages/templates/src/templates/bb-guide-threads.md @@ -22,6 +22,7 @@ Spawning: --model Model override --reasoning-level Reasoning level: low, medium, high, xhigh, max (provider-dependent) --environment Attach to an existing environment (ID or workspace path) + --env Set a per-thread environment variable (repeatable) --new-environment Create a fresh personal workspace or managed worktree --base-branch Exact Git ref for a new managed worktree (--new-environment worktree only) @@ -81,6 +82,13 @@ Spawning: --machine ` to see whether it is available, needs setup, or is unavailable and why. The first-party providers are Project checkout, Worktree, and Personal workspace. + --env splits on the first =, so values may contain = and may be empty. Names + use letters, numbers, and underscores, cannot begin with a number, and cannot + use bb's reserved BB_ prefix. A thread may carry up to 32 values; each value + is limited to 16 KiB and the serialized map to 64 KiB. Values override the + same names from the host shell or a plugin contribution and remain attached + to the thread across later turns and provider-session resumes. Duplicate + names and null bytes are rejected. Forking: diff --git a/packages/thread-view/src/parse-operation-message.ts b/packages/thread-view/src/parse-operation-message.ts index a0963dc218..6de1091349 100644 --- a/packages/thread-view/src/parse-operation-message.ts +++ b/packages/thread-view/src/parse-operation-message.ts @@ -483,7 +483,8 @@ export function parseOperationMessage( const detail = decoded.entries .map((entry) => { - const source = entry.source === "shell" ? "shell" : entry.source.plugin; + const source = + typeof entry.source === "string" ? entry.source : entry.source.plugin; const value = typeof entry.value === "string" ? entry.value : "••••••"; const reason = entry.reason ? ` — ${entry.reason}` : ""; return `${entry.name}=${value} (${source})${reason}`; diff --git a/packages/thread-view/test/parse-operation-message.test.ts b/packages/thread-view/test/parse-operation-message.test.ts index 39bf459b45..937d87e424 100644 --- a/packages/thread-view/test/parse-operation-message.test.ts +++ b/packages/thread-view/test/parse-operation-message.test.ts @@ -87,6 +87,11 @@ describe("parseOperationMessage operation titles", () => { value: { masked: true }, reason: "Authenticate provider traffic", }, + { + name: "MULTICA_TOKEN", + source: "thread", + value: { masked: true }, + }, ], }; const message = parseOperationMessage( @@ -106,7 +111,7 @@ describe("parseOperationMessage operation titles", () => { kind: "operation", title: "Provider environment resolved", detail: - "PLUGIN_TOKEN=•••••• (auth-proxy) — Authenticate provider traffic", + "PLUGIN_TOKEN=•••••• (auth-proxy) — Authenticate provider traffic\nMULTICA_TOKEN=•••••• (thread)", }); }); diff --git a/plugins/bb-guide/skills/bb-cli/SKILL.md b/plugins/bb-guide/skills/bb-cli/SKILL.md index c2c220fa54..af4c7dc9ed 100644 --- a/plugins/bb-guide/skills/bb-cli/SKILL.md +++ b/plugins/bb-guide/skills/bb-cli/SKILL.md @@ -61,6 +61,8 @@ BB_HOST_DAEMON_PORT only for an intentional non-default target. installed providers by display name. Read or set `managedBranchPrefix` through `bb settings show` and `bb settings general `. - Query provider models on the machine that will run the thread. +- Pass repeatable `--env KEY=VALUE` only when a spawned thread needs explicit, + durable process configuration; `BB_*` names are reserved by bb. - Prefer non-interactive commands and machine-readable output for automation. - Pass `--yes` for a confirmed destructive command in a non-interactive shell. - Treat plugin commands as normal top-level commands after installation. diff --git a/plugins/bb-guide/skills/bb-cli/references/thread-creation.md b/plugins/bb-guide/skills/bb-cli/references/thread-creation.md index 21e482a04d..08403b4856 100644 --- a/plugins/bb-guide/skills/bb-cli/references/thread-creation.md +++ b/plugins/bb-guide/skills/bb-cli/references/thread-creation.md @@ -20,6 +20,13 @@ `--environment-inputs ` only when the provider's schema does not accept an empty object; otherwise the CLI supplies `{}` when the flag is omitted. `--machine` picks the existing machine. +- Add repeatable `--env KEY=VALUE` flags for process configuration that belongs + to the new thread. Values may be empty or contain `=`. Names must be portable + shell variable names and cannot use bb's reserved `BB_` prefix. The map is + limited to 32 entries, 16 KiB per value, and 64 KiB serialized. It overrides + matching host-shell or plugin-contributed values and survives later turns, + stops, and provider-session resumes. Duplicate names and null bytes are + rejected. - Omit `--base-branch` for bb's default. Explicit values are exact; use `origin/` for a remote ref. It applies to `--new-environment worktree` only; a provider takes its branch through `--environment-inputs`. diff --git a/plugins/bb-guide/skills/bb-plugin-authoring/references/backend-sdk.md b/plugins/bb-guide/skills/bb-plugin-authoring/references/backend-sdk.md index 70efd4161f..774e4b84af 100644 --- a/plugins/bb-guide/skills/bb-plugin-authoring/references/backend-sdk.md +++ b/plugins/bb-guide/skills/bb-plugin-authoring/references/backend-sdk.md @@ -45,6 +45,7 @@ const thread = await bb.sdk.threads.spawn({ prompt: "Work on this issue…", // prompt XOR input — exactly one title: "ENG-42: fix the flaky test", visibility: "hidden", // optional background worker; visible is the default + envVars: { MULTICA_TASK_ID: "task-123" }, }); ``` @@ -54,6 +55,13 @@ inputs) — never both. Attribution is auto-filled: `origin: "plugin"` and threadId, mode: "auto", input: [...] })` starts a turn on an idle thread or queues/steers a running one. +`envVars` is optional per-thread process configuration. Names use portable +shell-variable syntax and cannot use bb's reserved `BB_` prefix. BB accepts at +most 32 entries, 16 KiB per value, and 64 KiB for the serialized map. Explicit +values override matching host-shell or plugin-contributed values, persist with +the thread, and are reused when its provider session resumes. Values are not +returned on thread APIs and are masked in provider-environment timeline events. + Read and edit existing threads with the same area — you do not need a sidebar panel or a spawned thread to reach them: diff --git a/plugins/bb-guide/skills/bb-plugin-authoring/references/providers.md b/plugins/bb-guide/skills/bb-plugin-authoring/references/providers.md index 0ab643f2d8..a8be62e427 100644 --- a/plugins/bb-guide/skills/bb-plugin-authoring/references/providers.md +++ b/plugins/bb-guide/skills/bb-plugin-authoring/references/providers.md @@ -168,12 +168,14 @@ literal `value` is forwarded as-is. `{ serverPath: "/..." }` is expanded by the selected host against its authenticated `BB_SERVER_URL`, which is the right form for a server route that must work from enrolled machines. -Contributions override the host shell environment. If multiple plugins return -the same name, the earlier registration wins and BB logs the conflict. A -resolver that throws, times out after five seconds, or returns invalid entries -contributes nothing for that command without blocking other plugins. Mark -credentials and sensitive URLs with `secret: true`; BB passes the real value -to the provider but masks it in `provider.env-resolved` timeline events. +Contributions override the host shell environment. An explicit per-thread +`envVars` value on `bb.sdk.threads.spawn` overrides a contribution with the +same name. If multiple plugins return the same name, the earlier registration +wins and BB logs the conflict. A resolver that throws, times out after five +seconds, or returns invalid entries contributes nothing for that command +without blocking other plugins. Mark credentials and sensitive URLs with +`secret: true`; BB passes the real value to the provider but masks it in +`provider.env-resolved` timeline events. When the contributed environment supplies credentials that replace a local login, pair the resolver with