diff --git a/packages/control-plane/src/sandbox/lifecycle/manager.test.ts b/packages/control-plane/src/sandbox/lifecycle/manager.test.ts index 960241cf5..460b0c5aa 100644 --- a/packages/control-plane/src/sandbox/lifecycle/manager.test.ts +++ b/packages/control-plane/src/sandbox/lifecycle/manager.test.ts @@ -679,6 +679,33 @@ describe("SandboxLifecycleManager", () => { expect(storage.calls).toContain("updateSandboxStatus:failed"); }); + it("fails spawn when getUserEnvVars rejects", async () => { + const sandbox = createMockSandbox({ status: "pending", created_at: Date.now() - 60000 }); + const storage = createMockStorage(createMockSession(), sandbox); + storage.getUserEnvVars = vi.fn(async () => { + throw new Error("D1 decryption failure"); + }); + const broadcaster = createMockBroadcaster(); + const wsManager = createMockWebSocketManager(false); + const provider = createMockProvider(); + + const manager = new SandboxLifecycleManager( + provider, + storage, + broadcaster, + wsManager, + createMockAlarmScheduler(), + createMockIdGenerator(), + createTestConfig() + ); + + await manager.spawnSandbox(); + + expect(provider.createSandbox).not.toHaveBeenCalled(); + expect(storage.calls).toContain("updateSandboxStatus:failed"); + expect(manager.isSpawning()).toBe(false); + }); + it("skips spawn when already spawning", async () => { const sandbox = createMockSandbox({ status: "spawning" }); const storage = createMockStorage(createMockSession(), sandbox); diff --git a/packages/control-plane/src/session/durable-object.ts b/packages/control-plane/src/session/durable-object.ts index d535f215a..eaae33cf5 100644 --- a/packages/control-plane/src/session/durable-object.ts +++ b/packages/control-plane/src/session/durable-object.ts @@ -1686,30 +1686,13 @@ export class SessionDO extends DurableObject { return undefined; } - // Fetch global secrets - let globalSecrets: Record = {}; - try { - const globalStore = new GlobalSecretsStore(this.env.DB, this.env.REPO_SECRETS_ENCRYPTION_KEY); - globalSecrets = await globalStore.getDecryptedSecrets(); - } catch (e) { - this.log.error("Failed to load global secrets, proceeding without", { - error: e instanceof Error ? e.message : String(e), - }); - } + // Fail hard on secret loading — sandboxes must not silently lose secrets + const globalStore = new GlobalSecretsStore(this.env.DB, this.env.REPO_SECRETS_ENCRYPTION_KEY); + const globalSecrets = await globalStore.getDecryptedSecrets(); - // Fetch repo secrets - let repoSecrets: Record = {}; - try { - const repoId = await this.ensureRepoId(session); - const repoStore = new RepoSecretsStore(this.env.DB, this.env.REPO_SECRETS_ENCRYPTION_KEY); - repoSecrets = await repoStore.getDecryptedSecrets(repoId); - } catch (e) { - this.log.warn("Failed to load repo secrets, proceeding without", { - repo_owner: session.repo_owner, - repo_name: session.repo_name, - error: e instanceof Error ? e.message : String(e), - }); - } + const repoId = await this.ensureRepoId(session); + const repoStore = new RepoSecretsStore(this.env.DB, this.env.REPO_SECRETS_ENCRYPTION_KEY); + const repoSecrets = await repoStore.getDecryptedSecrets(repoId); // Merge: repo overrides global const { merged, totalBytes, exceedsLimit } = mergeSecrets(globalSecrets, repoSecrets);