import type { Server } from "node:http"; import express from "express"; import request from "supertest"; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { unprocessable } from "../errors.js"; import { environmentRoutes } from "../routes/environments.js"; import { errorHandler } from "../middleware/index.js"; const mockAccessService = vi.hoisted(() => ({ canUser: vi.fn(), hasPermission: vi.fn(), decide: vi.fn(), })); const mockAgentService = vi.hoisted(() => ({ getById: vi.fn(), })); const mockIssueService = vi.hoisted(() => ({ getById: vi.fn(), clearExecutionWorkspaceEnvironmentSelection: vi.fn(), })); const mockProjectService = vi.hoisted(() => ({ getById: vi.fn(), clearExecutionWorkspaceEnvironmentSelection: vi.fn(), })); const mockEnvironmentRuntimeService = vi.hoisted(() => ({ destroyReusableSandboxLeasesForEnvironment: vi.fn(async () => ({ destroyed: 0, failed: 0, skippedLiveRun: 0 })), })); const mockCloseWarmNativeSessionsForEnvironment = vi.hoisted(() => vi.fn(async () => ({ closed: 0, busy: 0, failed: 0 })), ); const mockInstanceSettingsService = vi.hoisted(() => ({ listCompanyIds: vi.fn(), getGeneral: vi.fn(), getExperimental: vi.fn(), })); const mockEnvironmentService = vi.hoisted(() => ({ list: vi.fn(), getById: vi.fn(), create: vi.fn(), update: vi.fn(), removeIfDeletable: vi.fn(), getDeleteBlastRadius: vi.fn(), hasUnresolvedPendingCleanupLeases: vi.fn(), listLeases: vi.fn(), getLeaseById: vi.fn(), })); const mockEnvironmentCustomImageService = vi.hoisted(() => ({ getOverview: vi.fn(), getActiveTemplate: vi.fn(), getSessionById: vi.fn(), startSetupSession: vi.fn(), refreshSetupSession: vi.fn(), finishSetupSession: vi.fn(), cancelSetupSession: vi.fn(), rollbackTemplate: vi.fn(), disableTemplate: vi.fn(), cleanupExpiredSetupSessions: vi.fn(), })); const mockLogActivity = vi.hoisted(() => vi.fn()); const mockProbeEnvironment = vi.hoisted(() => vi.fn()); const mockSecretService = vi.hoisted(() => ({ create: vi.fn(), normalizeEnvBindingsForPersistence: vi.fn(), listBindingCompanyIdsForTarget: vi.fn(), resolveSecretValue: vi.fn(), resolveSecretValueForEphemeralAccess: vi.fn(), syncEnvBindingsForTarget: vi.fn(), syncSecretRefsForTarget: vi.fn(), replaceSecretRefsForInstanceTarget: vi.fn(), describeSecretRefs: vi.fn(), remove: vi.fn(), })); const mockValidatePluginEnvironmentDriverConfig = vi.hoisted(() => vi.fn()); const mockValidatePluginSandboxProviderConfig = vi.hoisted(() => vi.fn()); const mockListReadyPluginEnvironmentDrivers = vi.hoisted(() => vi.fn()); const mockResolvePluginSandboxProviderDriverByKey = vi.hoisted(() => vi.fn()); const mockStartPluginEnvironmentInteractiveSetup = vi.hoisted(() => vi.fn()); const mockGetPluginEnvironmentInteractiveSetup = vi.hoisted(() => vi.fn()); const mockCapturePluginEnvironmentTemplate = vi.hoisted(() => vi.fn()); const mockCancelPluginEnvironmentInteractiveSetup = vi.hoisted(() => vi.fn()); const mockDeletePluginEnvironmentTemplate = vi.hoisted(() => vi.fn()); const mockExecutionWorkspaceService = vi.hoisted(() => ({ clearEnvironmentSelection: vi.fn(), })); vi.mock("../services/index.js", () => ({ issueService: () => mockIssueService, instanceSettingsService: () => mockInstanceSettingsService, environmentCustomImageService: () => mockEnvironmentCustomImageService, environmentService: () => mockEnvironmentService, logActivity: mockLogActivity, projectService: () => mockProjectService, })); vi.mock("../services/environment-probe.js", () => ({ probeEnvironment: mockProbeEnvironment, })); vi.mock("../services/secrets.js", () => ({ secretService: () => mockSecretService, })); vi.mock("../services/environments.js", () => ({ environmentService: () => mockEnvironmentService, })); vi.mock("../services/environment-runtime.js", () => ({ environmentRuntimeService: () => mockEnvironmentRuntimeService, })); vi.mock("../services/native-runtime/native-session-executor.js", () => ({ closeWarmNativeSessionsForEnvironment: mockCloseWarmNativeSessionsForEnvironment, })); vi.mock("../services/execution-workspaces.js", () => ({ executionWorkspaceService: () => mockExecutionWorkspaceService, })); vi.mock("../services/plugin-environment-driver.js", () => ({ // The runtime reads this published constant at import time. Mirror the real // value so the mocked module keeps the same reusable-lease method contract. REUSABLE_LEASE_WORKER_METHODS: ["environmentResumeLease", "environmentReleaseLease", "environmentDestroyLease"], listReadyPluginEnvironmentDrivers: mockListReadyPluginEnvironmentDrivers, resolvePluginSandboxProviderDriverByKey: mockResolvePluginSandboxProviderDriverByKey, resolvePluginSandboxProviderConfigSchema: async (input: { db: unknown; driverKey: string }) => (await mockResolvePluginSandboxProviderDriverByKey(input))?.driver?.configSchema ?? null, startPluginEnvironmentInteractiveSetup: mockStartPluginEnvironmentInteractiveSetup, getPluginEnvironmentInteractiveSetup: mockGetPluginEnvironmentInteractiveSetup, capturePluginEnvironmentTemplate: mockCapturePluginEnvironmentTemplate, cancelPluginEnvironmentInteractiveSetup: mockCancelPluginEnvironmentInteractiveSetup, deletePluginEnvironmentTemplate: mockDeletePluginEnvironmentTemplate, validatePluginEnvironmentDriverConfig: mockValidatePluginEnvironmentDriverConfig, validatePluginSandboxProviderConfig: mockValidatePluginSandboxProviderConfig, })); function createEnvironment() { const now = new Date("2026-04-16T05:00:00.000Z"); return { id: "env-1", companyId: "company-1", name: "Local", description: "Current development machine", driver: "local", status: "active" as const, config: { shell: "zsh" }, envVars: {}, metadata: { source: "manual" }, createdAt: now, updatedAt: now, }; } function createDeleteBlastRadius(overrides: Partial<{ isManagedLocal: boolean; isInstanceDefault: boolean; agentDefaultCount: number; executionWorkspaceSelectionCount: number; issueSelectionCount: number; projectSelectionCount: number; secretBindingCount: number; activeLeaseCount: number; activeCustomImageSetupSessionCount: number; pendingCleanupLeaseCount: number; reusableSandboxLeaseCount: number; reusableSandboxLeaseHolders: Array<{ leaseId: string; executionWorkspaceId: string | null; executionWorkspaceName: string | null; issueId: string | null; issueIdentifier: string | null; issueTitle: string | null; }>; }> = {}) { const staticReferences = { isManagedLocal: overrides.isManagedLocal ?? false, isInstanceDefault: overrides.isInstanceDefault ?? false, agentDefaultCount: overrides.agentDefaultCount ?? 0, executionWorkspaceSelectionCount: overrides.executionWorkspaceSelectionCount ?? 0, issueSelectionCount: overrides.issueSelectionCount ?? 0, projectSelectionCount: overrides.projectSelectionCount ?? 0, secretBindingCount: overrides.secretBindingCount ?? 0, }; const activeRuntimeUse = { activeLeaseCount: overrides.activeLeaseCount ?? 0, activeCustomImageSetupSessionCount: overrides.activeCustomImageSetupSessionCount ?? 0, hasActiveRuntimeUse: (overrides.activeLeaseCount ?? 0) > 0 || (overrides.activeCustomImageSetupSessionCount ?? 0) > 0, }; const pendingCleanupLeaseCount = overrides.pendingCleanupLeaseCount ?? 0; const reusableSandboxLeaseCount = overrides.reusableSandboxLeaseCount ?? 0; const reusableSandboxLeaseHolders = overrides.reusableSandboxLeaseHolders ?? Array.from({ length: reusableSandboxLeaseCount }, (_, index) => ({ leaseId: `lease-${index + 1}`, executionWorkspaceId: null, executionWorkspaceName: null, issueId: null, issueIdentifier: null, issueTitle: null, })); const deleteBlockedReasons = [ ...(staticReferences.isManagedLocal ? ["managed_local" as const] : []), ...(staticReferences.isInstanceDefault ? ["instance_default" as const] : []), ...(pendingCleanupLeaseCount > 0 ? ["pending_sandbox_cleanup" as const] : []), ...(reusableSandboxLeaseCount > 0 ? ["reusable_sandbox_lease" as const] : []), ]; return { environmentId: "env-1", canDelete: deleteBlockedReasons.length === 0, deleteBlockedReasons, pendingCleanupLeaseCount, reusableSandboxLeaseCount, reusableSandboxLeaseHolders, staticReferences, activeRuntimeUse, }; } let server: Server | null = null; let currentActor: Record = localBoardActor(); const routeOptions: Record = {}; const originalSecretsProviderEnv = process.env.PAPERCLIP_SECRETS_PROVIDER; // The routes open a transaction around environment writes and their binding // syncs. Service calls are mocked, so the executor never runs a real query — // it only needs to be identity-checkable in assertions. const routeDbTx = { __routeDbTx: true }; const routeDb = { transaction: async (fn: (tx: unknown) => Promise): Promise => fn(routeDbTx), }; function createApp(actor: Record, options: Record = {}) { currentActor = actor; for (const key of Object.keys(routeOptions)) { delete routeOptions[key]; } Object.assign(routeOptions, options); if (server) return server; const app = express(); app.use(express.json()); app.use((req, _res, next) => { (req as any).actor = currentActor; next(); }); app.use("/api", environmentRoutes(routeDb as any, routeOptions as any)); app.use(errorHandler); server = app.listen(0); return server; } function localBoardActor(userId = "user-1") { return { type: "board", userId: userId, source: "local_implicit", }; } function createSecurePluginDriver() { return { driverKey: "secure-plugin", kind: "sandbox_provider", displayName: "Secure Sandbox", configSchema: { type: "object", properties: { template: { type: "string" }, apiKey: { type: "string", format: "secret-ref" }, timeoutMs: { type: "number" }, reuseLease: { type: "boolean" }, }, }, }; } function createSecretBackedSshEnvironment() { return { ...createEnvironment(), id: "env-ssh", name: "SSH Fixture", driver: "ssh" as const, config: { host: "ssh.example.test", port: 22, username: "ssh-user", remoteWorkspacePath: "/srv/paperclip/workspace", privateKey: null, privateKeySecretRef: { type: "secret_ref", secretId: "11111111-1111-1111-1111-111111111111", version: "latest", }, knownHosts: null, strictHostKeyChecking: true, }, }; } function createSshConfig() { return { host: "ssh.example.test", port: 22, username: "ssh-user", remoteWorkspacePath: "/srv/paperclip/workspace", privateKey: null, privateKeySecretRef: null, knownHosts: null, strictHostKeyChecking: true, }; } describe("environment routes", () => { afterAll(async () => { if (originalSecretsProviderEnv === undefined) { delete process.env.PAPERCLIP_SECRETS_PROVIDER; } else { process.env.PAPERCLIP_SECRETS_PROVIDER = originalSecretsProviderEnv; } if (!server) return; await new Promise((resolve, reject) => { server?.close((err) => { if (err) reject(err); else resolve(); }); }); server = null; }); beforeEach(() => { mockAccessService.canUser.mockReset(); mockAccessService.hasPermission.mockReset(); mockAccessService.decide.mockReset(); mockAgentService.getById.mockReset(); mockIssueService.getById.mockReset(); mockIssueService.clearExecutionWorkspaceEnvironmentSelection.mockReset(); mockProjectService.getById.mockReset(); mockProjectService.clearExecutionWorkspaceEnvironmentSelection.mockReset(); mockEnvironmentRuntimeService.destroyReusableSandboxLeasesForEnvironment.mockReset(); mockEnvironmentRuntimeService.destroyReusableSandboxLeasesForEnvironment.mockResolvedValue( { destroyed: 0, failed: 0, skippedLiveRun: 0 }, ); mockCloseWarmNativeSessionsForEnvironment.mockReset(); mockCloseWarmNativeSessionsForEnvironment.mockResolvedValue({ closed: 0, busy: 0, failed: 0, }); mockInstanceSettingsService.listCompanyIds.mockReset(); mockInstanceSettingsService.getGeneral.mockReset(); mockInstanceSettingsService.getGeneral.mockResolvedValue({ executionMode: "any" }); mockInstanceSettingsService.getExperimental.mockReset(); mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableManagedSandboxOnly: false }); mockEnvironmentService.list.mockReset(); mockEnvironmentService.list.mockResolvedValue([]); mockEnvironmentService.getById.mockReset(); mockEnvironmentService.create.mockReset(); mockEnvironmentService.update.mockReset(); mockEnvironmentService.removeIfDeletable.mockReset(); mockEnvironmentService.getDeleteBlastRadius.mockReset(); mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockReset(); mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockResolvedValue(false); mockEnvironmentService.listLeases.mockReset(); mockEnvironmentService.getLeaseById.mockReset(); mockExecutionWorkspaceService.clearEnvironmentSelection.mockReset(); Object.values(mockEnvironmentCustomImageService).forEach((mock) => mock.mockReset()); mockEnvironmentCustomImageService.getOverview.mockResolvedValue({ activeTemplate: null, activeSession: null, latestSession: null, }); mockEnvironmentCustomImageService.getActiveTemplate.mockResolvedValue(null); mockEnvironmentCustomImageService.getSessionById.mockResolvedValue(null); mockLogActivity.mockReset(); mockProbeEnvironment.mockReset(); mockSecretService.create.mockReset(); mockSecretService.normalizeEnvBindingsForPersistence.mockReset(); mockSecretService.listBindingCompanyIdsForTarget.mockReset(); mockSecretService.resolveSecretValue.mockReset(); mockSecretService.resolveSecretValueForEphemeralAccess.mockReset(); mockSecretService.syncEnvBindingsForTarget.mockReset(); mockSecretService.syncSecretRefsForTarget.mockReset(); mockSecretService.replaceSecretRefsForInstanceTarget.mockReset(); mockSecretService.describeSecretRefs.mockReset(); mockSecretService.describeSecretRefs.mockResolvedValue([]); mockSecretService.remove.mockReset(); mockSecretService.create.mockResolvedValue({ id: "11111111-1111-1111-1111-111111111111", }); mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1"]); mockIssueService.clearExecutionWorkspaceEnvironmentSelection.mockResolvedValue(0); mockProjectService.clearExecutionWorkspaceEnvironmentSelection.mockResolvedValue(0); mockExecutionWorkspaceService.clearEnvironmentSelection.mockResolvedValue(0); mockSecretService.normalizeEnvBindingsForPersistence.mockImplementation(async (_companyId, env) => env ?? {}); mockSecretService.listBindingCompanyIdsForTarget.mockResolvedValue([]); mockSecretService.syncEnvBindingsForTarget.mockResolvedValue([]); mockSecretService.syncSecretRefsForTarget.mockResolvedValue([]); mockSecretService.replaceSecretRefsForInstanceTarget.mockResolvedValue([]); mockSecretService.remove.mockResolvedValue(null); mockSecretService.resolveSecretValueForEphemeralAccess.mockResolvedValue("resolved-provider-key"); delete process.env.PAPERCLIP_SECRETS_PROVIDER; mockValidatePluginEnvironmentDriverConfig.mockReset(); mockValidatePluginEnvironmentDriverConfig.mockImplementation(async ({ config }) => config); mockValidatePluginSandboxProviderConfig.mockReset(); mockValidatePluginSandboxProviderConfig.mockImplementation(async ({ provider, config }) => ({ normalizedConfig: config, pluginId: `plugin-${provider}`, pluginKey: `plugin.${provider}`, driver: { driverKey: provider, kind: "sandbox_provider", displayName: provider, configSchema: { type: "object" }, }, })); mockResolvePluginSandboxProviderDriverByKey.mockReset(); mockResolvePluginSandboxProviderDriverByKey.mockImplementation(async ({ driverKey }) => ( driverKey === "secure-plugin" ? { pluginId: "plugin-secure", pluginKey: "acme.secure-sandbox-provider", driver: createSecurePluginDriver(), } : null )); mockListReadyPluginEnvironmentDrivers.mockReset(); mockListReadyPluginEnvironmentDrivers.mockResolvedValue([]); mockStartPluginEnvironmentInteractiveSetup.mockReset(); mockGetPluginEnvironmentInteractiveSetup.mockReset(); mockCapturePluginEnvironmentTemplate.mockReset(); mockCancelPluginEnvironmentInteractiveSetup.mockReset(); mockDeletePluginEnvironmentTemplate.mockReset(); mockAccessService.decide.mockResolvedValue({ allowed: true, explanation: "Allowed by test harness", }); }); it("lists instance-scoped environments through the company route alias", async () => { mockEnvironmentService.list.mockResolvedValue([createEnvironment()]); const app = createApp(localBoardActor()); const res = await request(app).get("/api/companies/company-1/environments?driver=local"); expect(res.status).toBe(200); expect(res.body).toHaveLength(1); expect(mockEnvironmentService.list).toHaveBeenCalledWith({ status: undefined, driver: "local", }); }); it("redacts environment config for non-admin board readers", async () => { mockEnvironmentService.list.mockResolvedValue([createEnvironment()]); const app = createApp({ type: "board", userId: "user-2", source: "session", companyIds: ["company-1"], memberships: [{ companyId: "company-1", status: "active", membershipRole: "member" }], isInstanceAdmin: false, }); const res = await request(app).get("/api/companies/company-1/environments"); expect(res.status).toBe(200); expect(res.body[0]).toMatchObject({ id: "env-1", name: "Local", config: {}, envVars: {}, metadata: null, }); }); it("redacts environment detail config for non-admin board readers", async () => { mockEnvironmentService.getById.mockResolvedValue(createEnvironment()); const app = createApp({ type: "board", userId: "user-2", source: "session", companyIds: ["company-1"], memberships: [{ companyId: "company-1", status: "active", membershipRole: "member" }], isInstanceAdmin: false, }); const res = await request(app).get("/api/environments/env-1"); expect(res.status).toBe(200); expect(res.body).toMatchObject({ id: "env-1", config: {}, envVars: {}, metadata: null, }); }); describe("platform-provisioned environment floor on cloud-managed instances", () => { function createPlatformSandboxEnvironment() { const now = new Date("2026-04-16T05:00:00.000Z"); return { id: "env-managed-1", companyId: "company-1", name: "Daytona", description: "Managed sandbox environment", driver: "sandbox", status: "active" as const, config: { provider: "daytona", image: "custom-image:latest", target: "us", apiKey: "config-credential-must-never-echo", }, envVars: { MY_AGENT_TOOL_SETTING: "tenant-env-value" }, metadata: { managedByPaperclip: true, managedSandboxProvider: "daytona" }, createdAt: now, updatedAt: now, }; } const ownerAdminActor = { type: "board", userId: "owner-1", source: "cloud_tenant", companyIds: ["company-1"], memberships: [{ companyId: "company-1", status: "active", membershipRole: "owner" }], isInstanceAdmin: true, }; // A minimal valid managed-config document whose `environments` entry makes // the managed-sandbox provisioner own the marked sandbox slot row. const MANAGED_CONFIG_WITH_SANDBOX_ENTRY = JSON.stringify({ v: 1, mode: "cloud", catalogVersion: "2026.720.0", features: {}, plugins: { autoInstall: [] }, environments: [{ name: "Sandbox", provider: "daytona" }], }); beforeEach(() => { process.env.PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN = "test-server-token"; }); afterEach(() => { delete process.env.PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN; }); it("never echoes credential-shaped config keys, while tenant env vars round-trip", async () => { mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment()); const app = createApp(ownerAdminActor); const res = await request(app).get("/api/environments/env-managed-1"); expect(res.status).toBe(200); // Env vars are the tenant-owned field on the managed sandbox row // (the platform never writes them), so they echo for editing. expect(res.body.envVars).toEqual({ MY_AGENT_TOOL_SETTING: "tenant-env-value" }); expect(res.body.config).toEqual({ provider: "daytona", image: "custom-image:latest", target: "us", }); expect(res.body.metadata).toMatchObject({ managedByPaperclip: true }); expect(JSON.stringify(res.body)).not.toContain("config-credential-must-never-echo"); }); it("keeps blanking env vars on legacy kubernetes-marker rows", async () => { // Pre-generalization builds may have written platform values into // these rows' env vars, so the legacy floor stays absolute there. mockEnvironmentService.getById.mockResolvedValue({ ...createPlatformSandboxEnvironment(), metadata: { managedByPaperclip: true, managedSandboxProvider: "kubernetes", managedKubernetesSandbox: true, }, }); const app = createApp(ownerAdminActor); const res = await request(app).get("/api/environments/env-managed-1"); expect(res.status).toBe(200); expect(res.body.envVars).toEqual({}); }); it("exposes structural config to restricted company readers instead of blanking it", async () => { mockEnvironmentService.list.mockResolvedValue([createPlatformSandboxEnvironment()]); const app = createApp({ type: "board", userId: "user-2", source: "session", companyIds: ["company-1"], memberships: [{ companyId: "company-1", status: "active", membershipRole: "member" }], isInstanceAdmin: false, }); const res = await request(app).get("/api/companies/company-1/environments"); expect(res.status).toBe(200); expect(res.body[0].config).toEqual({ provider: "daytona", image: "custom-image:latest", target: "us", }); // Tenant env vars can carry pasted credentials, so restricted readers // get the same blank envVars posture as on every other environment. expect(res.body[0].envVars).toEqual({}); expect(res.body[0].metadata).toMatchObject({ managedByPaperclip: true }); }); it("rejects updates to platform-provisioned rows, including for instance admins", async () => { mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment()); const app = createApp(ownerAdminActor); const res = await request(app).patch("/api/environments/env-managed-1").send({ name: "Renamed" }); expect(res.status).toBe(403); expect(res.body.details).toMatchObject({ code: "environment_platform_managed" }); expect(mockEnvironmentService.update).not.toHaveBeenCalled(); }); it("allows an envVars-only patch on the managed sandbox row", async () => { const row = createPlatformSandboxEnvironment(); mockEnvironmentService.getById.mockResolvedValue(row); mockEnvironmentService.update.mockResolvedValue({ ...row, envVars: { MY_AGENT_TOOL_SETTING: "updated-value", EXTRA: "added" }, }); const app = createApp(ownerAdminActor); const res = await request(app) .patch("/api/environments/env-managed-1") .send({ envVars: { MY_AGENT_TOOL_SETTING: "updated-value", EXTRA: "added" } }); expect(res.status).toBe(200); expect(mockEnvironmentService.update).toHaveBeenCalledWith( "env-managed-1", expect.objectContaining({ envVars: { MY_AGENT_TOOL_SETTING: "updated-value", EXTRA: "added" }, }), expect.anything(), ); expect(res.body.envVars).toEqual({ MY_AGENT_TOOL_SETTING: "updated-value", EXTRA: "added" }); }); it("scopes the envVars patch to an explicit companyId query for a multi-company actor", async () => { const row = createPlatformSandboxEnvironment(); mockEnvironmentService.getById.mockResolvedValue(row); mockEnvironmentService.update.mockResolvedValue({ ...row, envVars: { A: "1" } }); // No prior bindings and two memberships: neither inference path can // pin a company, so the explicit query context must carry the save. mockSecretService.listBindingCompanyIdsForTarget.mockResolvedValue([]); const app = createApp({ ...ownerAdminActor, companyIds: ["company-1", "company-2"], memberships: [ { companyId: "company-1", status: "active", membershipRole: "owner" }, { companyId: "company-2", status: "active", membershipRole: "member" }, ], }); const res = await request(app) .patch("/api/environments/env-managed-1?companyId=company-1") .send({ envVars: { A: "1" } }); expect(res.status).toBe(200); expect(mockSecretService.normalizeEnvBindingsForPersistence).toHaveBeenCalledWith( "company-1", { A: "1" }, expect.anything(), ); }); it("falls back to the instance's only company when the actor's memberships cannot pin one", async () => { const row = createPlatformSandboxEnvironment(); mockEnvironmentService.getById.mockResolvedValue(row); mockEnvironmentService.update.mockResolvedValue({ ...row, envVars: { A: "1" } }); mockSecretService.listBindingCompanyIdsForTarget.mockResolvedValue([]); // An instance admin provisioned without membership rows — the // owner-as-admin shape on managed stacks. const app = createApp({ ...ownerAdminActor, companyIds: [], memberships: [], }); const res = await request(app) .patch("/api/environments/env-managed-1") .send({ envVars: { A: "1" } }); expect(res.status).toBe(200); expect(mockSecretService.normalizeEnvBindingsForPersistence).toHaveBeenCalledWith( "company-1", { A: "1" }, expect.anything(), ); }); it("still fails closed when no companyId context is resolvable on a multi-company instance", async () => { mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment()); mockSecretService.listBindingCompanyIdsForTarget.mockResolvedValue([]); mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1", "company-2"]); const app = createApp({ ...ownerAdminActor, companyIds: ["company-1", "company-2"], memberships: [ { companyId: "company-1", status: "active", membershipRole: "owner" }, { companyId: "company-2", status: "active", membershipRole: "member" }, ], }); const res = await request(app) .patch("/api/environments/env-managed-1") .send({ envVars: { A: "1" } }); expect(res.status).toBe(422); expect(res.body.error).toContain("requires a companyId context"); expect(mockEnvironmentService.update).not.toHaveBeenCalled(); }); it("rejects a patch that mixes envVars with any other field on the managed sandbox row", async () => { mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment()); const app = createApp(ownerAdminActor); const res = await request(app) .patch("/api/environments/env-managed-1") .send({ envVars: { A: "1" }, name: "Renamed" }); expect(res.status).toBe(403); expect(res.body.details).toMatchObject({ code: "environment_platform_managed" }); expect(mockEnvironmentService.update).not.toHaveBeenCalled(); }); it("rejects an envVars-only patch on a legacy kubernetes-marker row", async () => { mockEnvironmentService.getById.mockResolvedValue({ ...createPlatformSandboxEnvironment(), metadata: { managedByPaperclip: true, managedSandboxProvider: "kubernetes", managedKubernetesSandbox: true, }, }); const app = createApp(ownerAdminActor); const res = await request(app) .patch("/api/environments/env-managed-1") .send({ envVars: { A: "1" } }); expect(res.status).toBe(403); expect(res.body.details).toMatchObject({ code: "environment_platform_managed" }); expect(mockEnvironmentService.update).not.toHaveBeenCalled(); }); it("hides the local environment from every read surface under managed-sandbox-only", async () => { mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableManagedSandboxOnly: true }); const localRow = { ...createPlatformSandboxEnvironment(), id: "env-local-1", name: "Local", driver: "local", config: {}, envVars: {}, metadata: { managedByPaperclip: true, defaultForInstance: true }, }; mockEnvironmentService.list.mockResolvedValue([localRow, createPlatformSandboxEnvironment()]); const app = createApp(ownerAdminActor); const listRes = await request(app).get("/api/companies/company-1/environments"); expect(listRes.status).toBe(200); expect(listRes.body.map((row: { id: string }) => row.id)).toEqual(["env-managed-1"]); mockEnvironmentService.getById.mockResolvedValue(localRow); const byIdRes = await request(app).get("/api/environments/env-local-1"); expect(byIdRes.status).toBe(404); }); it("keeps the local environment visible when managed-sandbox-only is off", async () => { const localRow = { ...createPlatformSandboxEnvironment(), id: "env-local-1", name: "Local", driver: "local", config: {}, envVars: {}, metadata: { managedByPaperclip: true }, }; mockEnvironmentService.list.mockResolvedValue([localRow]); const app = createApp(ownerAdminActor); const listRes = await request(app).get("/api/companies/company-1/environments"); expect(listRes.status).toBe(200); expect(listRes.body.map((row: { id: string }) => row.id)).toEqual(["env-local-1"]); }); it("allows a marker-clear-only patch to unblock a row with a stale legacy kubernetes marker", async () => { // A sandbox row carrying only the legacy wrapper marker does not hold // the managed sandbox slot (`environments_managed_sandbox_idx` keys on // `managedByPaperclip`), and with the persisted execution mode not // forcing kubernetes nothing selects rows by that marker either — so // the marker is a stale leftover, not live platform state. const staleRow = { ...createPlatformSandboxEnvironment(), id: "env-legacy-1", metadata: { managedKubernetesSandbox: true }, }; mockEnvironmentService.getById.mockResolvedValue(staleRow); mockEnvironmentService.update.mockResolvedValue({ ...staleRow, metadata: {} }); const app = createApp(ownerAdminActor); const res = await request(app) .patch("/api/environments/env-legacy-1") .send({ metadata: { managedKubernetesSandbox: false } }); expect(res.status).toBe(200); expect(mockEnvironmentService.update).toHaveBeenCalled(); }); it("allows a marker-clear-only patch on a non-slot driver with a stale platform marker", async () => { const staleRow = { ...createPlatformSandboxEnvironment(), id: "env-stale-ssh-1", driver: "ssh", metadata: { managedByPaperclip: true }, }; mockEnvironmentService.getById.mockResolvedValue(staleRow); mockEnvironmentService.update.mockResolvedValue({ ...staleRow, metadata: {} }); const app = createApp(ownerAdminActor); const res = await request(app) .patch("/api/environments/env-stale-ssh-1") .send({ metadata: { managedByPaperclip: false, managedKubernetesSandbox: false } }); expect(res.status).toBe(200); expect(mockEnvironmentService.update).toHaveBeenCalled(); }); it("refuses the marker-clear patch on the sandbox slot row while managed provisioning is configured", async () => { // With a managed-config `environments` entry, driver=sandbox + // managedByPaperclip is THE provisioner-owned slot row, adopted and // refreshed on every boot — clearing its markers would reclassify it // tenant-managed and let the next PATCH/DELETE bypass the write floor. process.env.PAPERCLIP_MANAGED_CONFIG = MANAGED_CONFIG_WITH_SANDBOX_ENTRY; try { mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment()); const app = createApp(ownerAdminActor); const res = await request(app) .patch("/api/environments/env-managed-1") .send({ metadata: { managedByPaperclip: false, managedKubernetesSandbox: false } }); expect(res.status).toBe(403); expect(res.body.details).toMatchObject({ code: "environment_platform_managed" }); expect(mockEnvironmentService.update).not.toHaveBeenCalled(); } finally { delete process.env.PAPERCLIP_MANAGED_CONFIG; } }); it("refuses the marker-clear patch on the sandbox slot row under the forced kubernetes execution mode", async () => { // PAPERCLIP_EXECUTION_MODE=kubernetes is the other bootstrap path that // owns (adopts and refreshes) the single marked sandbox row. process.env.PAPERCLIP_EXECUTION_MODE = "kubernetes"; try { mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment()); const app = createApp(ownerAdminActor); const res = await request(app) .patch("/api/environments/env-managed-1") .send({ metadata: { managedByPaperclip: false, managedKubernetesSandbox: false } }); expect(res.status).toBe(403); expect(res.body.details).toMatchObject({ code: "environment_platform_managed" }); expect(mockEnvironmentService.update).not.toHaveBeenCalled(); } finally { delete process.env.PAPERCLIP_EXECUTION_MODE; } }); it("refuses the marker-clear patch on a kubernetes-marked row while the persisted execution mode forces kubernetes", async () => { // `findKubernetesEnvironment` selects sandbox rows by the legacy // marker alone whenever the persisted executionMode forces kubernetes // — including when the bootstrap env that seeded the setting is gone // (rollback / config drift, which the heartbeat handles explicitly). // Clearing the marker would declassify the live runtime row. mockInstanceSettingsService.getGeneral.mockResolvedValue({ executionMode: "kubernetes" }); mockEnvironmentService.getById.mockResolvedValue({ ...createPlatformSandboxEnvironment(), id: "env-legacy-1", metadata: { managedKubernetesSandbox: true }, }); const app = createApp(ownerAdminActor); const res = await request(app) .patch("/api/environments/env-legacy-1") .send({ metadata: { managedKubernetesSandbox: false } }); expect(res.status).toBe(403); expect(res.body.details).toMatchObject({ code: "environment_platform_managed" }); expect(mockEnvironmentService.update).not.toHaveBeenCalled(); }); it("refuses the marker-clear patch on the kubernetes-managed row when only the persisted execution mode remains forced", async () => { // Drift variant with the fully-stamped managed row: no managed-config // entry and no bootstrap env, but the persisted executionMode still // forces kubernetes, so the marker keeps selecting this row for runs. mockInstanceSettingsService.getGeneral.mockResolvedValue({ executionMode: "kubernetes" }); mockEnvironmentService.getById.mockResolvedValue({ ...createPlatformSandboxEnvironment(), metadata: { managedByPaperclip: true, managedSandboxProvider: "kubernetes", managedKubernetesSandbox: true, }, }); const app = createApp(ownerAdminActor); const res = await request(app) .patch("/api/environments/env-managed-1") .send({ metadata: { managedByPaperclip: false, managedKubernetesSandbox: false } }); expect(res.status).toBe(403); expect(res.body.details).toMatchObject({ code: "environment_platform_managed" }); expect(mockEnvironmentService.update).not.toHaveBeenCalled(); }); it("allows the marker-clear patch on a marked sandbox row when no provisioning path is configured", async () => { // Without a managed-config `environments` entry or a forced execution // mode, nothing on this instance provisions a sandbox environment, so a // platform marker on a sandbox row can only be a stale leftover of the // old unrestricted API — the recovery hatch must apply or the row is // locked forever. const staleRow = createPlatformSandboxEnvironment(); mockEnvironmentService.getById.mockResolvedValue(staleRow); mockEnvironmentService.update.mockResolvedValue({ ...staleRow, metadata: {} }); const app = createApp(ownerAdminActor); const res = await request(app) .patch("/api/environments/env-managed-1") .send({ metadata: { managedByPaperclip: false, managedKubernetesSandbox: false } }); expect(res.status).toBe(200); expect(mockEnvironmentService.update).toHaveBeenCalled(); }); it("refuses the marker-clear patch on the managed local row", async () => { // The local slot needs no configuration check: on a cloud-managed // instance `ensureLocalEnvironment` adopts and stamps the single local // row from every caller, so its markers are always live platform state. mockEnvironmentService.getById.mockResolvedValue({ ...createPlatformSandboxEnvironment(), id: "env-local-1", driver: "local", metadata: { managedByPaperclip: true, defaultForInstance: true }, }); const app = createApp(ownerAdminActor); const res = await request(app) .patch("/api/environments/env-local-1") .send({ metadata: { managedByPaperclip: false } }); expect(res.status).toBe(403); expect(res.body.details).toMatchObject({ code: "environment_platform_managed" }); expect(mockEnvironmentService.update).not.toHaveBeenCalled(); }); it("rejects non-marker-only patches to platform-provisioned rows even from instance admins", async () => { mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment()); const app = createApp(ownerAdminActor); const res = await request(app) .patch("/api/environments/env-managed-1") .send({ name: "Renamed", metadata: { managedByPaperclip: false } }); expect(res.status).toBe(403); expect(res.body.details).toMatchObject({ code: "environment_platform_managed" }); expect(mockEnvironmentService.update).not.toHaveBeenCalled(); }); it("rejects deletes of platform-provisioned rows, including for instance admins", async () => { mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment()); const app = createApp(ownerAdminActor); const res = await request(app).delete("/api/environments/env-managed-1"); expect(res.status).toBe(403); expect(res.body.details).toMatchObject({ code: "environment_platform_managed" }); expect(mockEnvironmentService.getDeleteBlastRadius).not.toHaveBeenCalled(); expect(mockEnvironmentService.removeIfDeletable).not.toHaveBeenCalled(); }); it("rejects creates that stamp platform markers so tenants cannot self-lock rows", async () => { const app = createApp(ownerAdminActor); const res = await request(app) .post("/api/companies/company-1/environments") .send({ name: "Fake managed", driver: "sandbox", config: { provider: "daytona" }, metadata: { managedByPaperclip: true }, }); expect(res.status).toBe(422); expect(res.body.details).toMatchObject({ code: "environment_platform_marker_reserved" }); expect(mockEnvironmentService.create).not.toHaveBeenCalled(); }); it("rejects tenant patches that stamp platform markers so the row cannot become locked", async () => { const tenantEnvironment = { ...createPlatformSandboxEnvironment(), id: "env-tenant-1", metadata: { source: "manual" }, }; mockEnvironmentService.getById.mockResolvedValue(tenantEnvironment); const app = createApp(ownerAdminActor); const res = await request(app) .patch("/api/environments/env-tenant-1") .send({ metadata: { source: "manual", managedKubernetesSandbox: true } }); expect(res.status).toBe(422); expect(res.body.details).toMatchObject({ code: "environment_platform_marker_reserved" }); expect(mockEnvironmentService.update).not.toHaveBeenCalled(); }); it("accepts platform markers in client payloads on self-hosted instances", async () => { delete process.env.PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN; const existing = { ...createPlatformSandboxEnvironment(), id: "env-tenant-1", metadata: { source: "manual" }, }; mockEnvironmentService.getById.mockResolvedValue(existing); mockEnvironmentService.update.mockResolvedValue({ ...existing, metadata: { source: "manual", managedByPaperclip: true }, }); const app = createApp({ type: "board", userId: "admin-1", source: "session", isInstanceAdmin: true, }); const res = await request(app) .patch("/api/environments/env-tenant-1") .send({ metadata: { source: "manual", managedByPaperclip: true } }); expect(res.status).toBe(200); expect(mockEnvironmentService.update).toHaveBeenCalled(); }); it("still updates tenant-created environments for instance admins on cloud-managed instances", async () => { const tenantEnvironment = { ...createPlatformSandboxEnvironment(), id: "env-tenant-1", metadata: { source: "manual" }, }; mockEnvironmentService.getById.mockResolvedValue(tenantEnvironment); mockEnvironmentService.update.mockResolvedValue({ ...tenantEnvironment, name: "Renamed" }); const app = createApp(ownerAdminActor); const res = await request(app).patch("/api/environments/env-tenant-1").send({ name: "Renamed" }); expect(res.status).toBe(200); expect(res.body.name).toBe("Renamed"); expect(mockEnvironmentService.update).toHaveBeenCalled(); }); it("does not floor writes to platform-marked rows on self-hosted instances", async () => { delete process.env.PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN; const existing = createPlatformSandboxEnvironment(); mockEnvironmentService.getById.mockResolvedValue(existing); mockEnvironmentService.update.mockResolvedValue({ ...existing, name: "Renamed" }); const app = createApp({ type: "board", userId: "admin-1", source: "session", isInstanceAdmin: true, }); const res = await request(app).patch("/api/environments/env-managed-1").send({ name: "Renamed" }); expect(res.status).toBe(200); expect(res.body.name).toBe("Renamed"); }); it("leaves tenant-created environments unfloored for instance admins", async () => { const tenantEnvironment = { ...createPlatformSandboxEnvironment(), id: "env-tenant-1", metadata: { source: "manual" }, }; mockEnvironmentService.getById.mockResolvedValue(tenantEnvironment); const app = createApp(ownerAdminActor); const res = await request(app).get("/api/environments/env-tenant-1"); expect(res.status).toBe(200); expect(res.body.envVars).toEqual({ MY_AGENT_TOOL_SETTING: "tenant-env-value" }); expect(res.body.config.apiKey).toBe("config-credential-must-never-echo"); }); it("does not floor platform-marked rows on self-hosted instances", async () => { delete process.env.PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN; mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment()); const app = createApp({ type: "board", userId: "admin-1", source: "session", isInstanceAdmin: true, }); const res = await request(app).get("/api/environments/env-managed-1"); expect(res.status).toBe(200); expect(res.body.envVars).toEqual({ MY_AGENT_TOOL_SETTING: "tenant-env-value" }); expect(res.body.config.apiKey).toBe("config-credential-must-never-echo"); }); }); it("rejects non-admin blast-radius reads for instance-scoped environments", async () => { const app = createApp({ type: "board", userId: "user-2", source: "session", companyIds: ["company-1"], memberships: [{ companyId: "company-1", status: "active", membershipRole: "member" }], isInstanceAdmin: false, }); const res = await request(app).get("/api/environments/env-1/delete-blast-radius"); expect(res.status).toBe(403); expect(mockEnvironmentService.getDeleteBlastRadius).not.toHaveBeenCalled(); }); it("returns delete blast radius counts for instance admins", async () => { mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({ agentDefaultCount: 2, secretBindingCount: 3, activeLeaseCount: 1, })); const app = createApp({ type: "board", userId: "admin-1", source: "session", companyIds: ["company-1"], isInstanceAdmin: true, }); const res = await request(app).get("/api/environments/env-1/delete-blast-radius"); expect(res.status).toBe(200); expect(res.body).toEqual({ environmentId: "env-1", canDelete: true, deleteBlockedReasons: [], pendingCleanupLeaseCount: 0, reusableSandboxLeaseCount: 0, reusableSandboxLeaseHolders: [], staticReferences: { isManagedLocal: false, isInstanceDefault: false, agentDefaultCount: 2, executionWorkspaceSelectionCount: 0, issueSelectionCount: 0, projectSelectionCount: 0, secretBindingCount: 3, }, activeRuntimeUse: { activeLeaseCount: 1, activeCustomImageSetupSessionCount: 0, hasActiveRuntimeUse: true, }, }); expect(res.body).not.toHaveProperty("config"); expect(res.body).not.toHaveProperty("envVars"); expect(res.body).not.toHaveProperty("metadata"); }); it("returns 404 for missing delete blast radius targets", async () => { mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(null); const app = createApp({ type: "board", userId: "admin-1", source: "session", companyIds: ["company-1"], isInstanceAdmin: true, }); const res = await request(app).get("/api/environments/missing/delete-blast-radius"); expect(res.status).toBe(404); expect(res.body.error).toBe("Environment not found"); }); it("returns provider capabilities for the company", async () => { const app = createApp(localBoardActor()); const res = await request(app).get("/api/companies/company-1/environments/capabilities"); expect(res.status).toBe(200); expect(res.body.drivers.ssh).toBe("supported"); expect(res.body.sandboxProviders.fake.supportsRunExecution).toBe(false); expect(res.body.sandboxProviders).not.toHaveProperty("fake-plugin"); }); it("returns installed plugin-backed sandbox capabilities for environment creation", async () => { mockListReadyPluginEnvironmentDrivers.mockResolvedValue([ { pluginId: "plugin-1", pluginKey: "acme.secure-sandbox-provider", driverKey: "secure-plugin", displayName: "Secure Sandbox", description: "Provisions schema-driven cloud sandboxes.", supportsInteractiveSetup: true, interactiveSetupConnectionTypes: ["ssh"], supportsTemplateCapture: true, templateRefKind: "snapshot", templateConfigBinding: { field: "template", unsetFields: ["image"], }, supportsTemplateDelete: true, configSchema: { type: "object", properties: { template: { type: "string" }, apiKey: { type: "string", format: "secret-ref" }, }, }, }, ]); const app = createApp(localBoardActor()); const res = await request(app).get("/api/companies/company-1/environments/capabilities"); expect(res.status).toBe(200); expect(res.body.sandboxProviders["secure-plugin"]).toMatchObject({ status: "supported", supportsRunExecution: true, // The mock manifest omits reusable leases, so the capability defaults to // false and agrees with the execution guard. supportsReusableLeases: false, supportsInteractiveSetup: true, interactiveSetupConnectionTypes: ["ssh"], supportsTemplateCapture: true, templateRefKind: "snapshot", templateConfigBinding: { field: "template", unsetFields: ["image"], }, supportsTemplateDelete: true, displayName: "Secure Sandbox", source: "plugin", pluginKey: "acme.secure-sandbox-provider", pluginId: "plugin-1", configSchema: { type: "object", properties: { template: { type: "string" }, apiKey: { type: "string", format: "secret-ref" }, }, }, }); expect(res.body.adapters.find((row: any) => row.adapterType === "codex_local").sandboxProviders["secure-plugin"]) .toBe("supported"); }); it("publishes reusable leases from the nested capability, not the legacy flag, so the API agrees with acquisition", async () => { // The manifest sets the legacy flag `true` but the nested override `false`. // Acquisition lets the nested value win and refuses reuse. The published // API value must derive from the same declaration resolver and present the // provider as not reusable, even when the worker verified both lifecycle // methods. mockListReadyPluginEnvironmentDrivers.mockResolvedValue([ { pluginId: "plugin-1", pluginKey: "acme.legacy-override-provider", driverKey: "override-plugin", displayName: "Override Sandbox", supportsReusableLeases: true, sandboxCapabilities: { reusableLeases: false }, reusableLeaseMethodsVerified: true, configSchema: { type: "object", properties: {} }, }, ]); const app = createApp(localBoardActor()); const res = await request(app).get("/api/companies/company-1/environments/capabilities"); expect(res.status).toBe(200); expect(res.body.sandboxProviders["override-plugin"].supportsReusableLeases).toBe(false); }); it("publishes reusable leases from the legacy flag when the manifest omits the nested override and the worker verified both methods", async () => { mockListReadyPluginEnvironmentDrivers.mockResolvedValue([ { pluginId: "plugin-1", pluginKey: "acme.legacy-only-provider", driverKey: "legacy-plugin", displayName: "Legacy Sandbox", supportsReusableLeases: true, reusableLeaseMethodsVerified: true, configSchema: { type: "object", properties: {} }, }, ]); const app = createApp(localBoardActor()); const res = await request(app).get("/api/companies/company-1/environments/capabilities"); expect(res.status).toBe(200); expect(res.body.sandboxProviders["legacy-plugin"].supportsReusableLeases).toBe(true); }); it("does not publish reusable leases when the declaration allows them but the worker omits a lifecycle method", async () => { // A positive declaration alone is not enough. Acquisition verifies both // reuse lifecycle methods live and falls back to an ephemeral lease when one // is missing. The published value must agree and present as not reusable. mockListReadyPluginEnvironmentDrivers.mockResolvedValue([ { pluginId: "plugin-1", pluginKey: "acme.unverified-reuse-provider", driverKey: "unverified-plugin", displayName: "Unverified Reuse Sandbox", sandboxCapabilities: { reusableLeases: true }, reusableLeaseMethodsVerified: false, configSchema: { type: "object", properties: {} }, }, ]); const app = createApp(localBoardActor()); const res = await request(app).get("/api/companies/company-1/environments/capabilities"); expect(res.status).toBe(200); expect(res.body.sandboxProviders["unverified-plugin"].supportsReusableLeases).toBe(false); }); it("rejects agent list reads for instance-scoped environments", async () => { mockEnvironmentService.list.mockResolvedValue([createEnvironment()]); mockAgentService.getById.mockResolvedValue({ id: "agent-1", companyId: "company-1", role: "engineer", permissions: { canCreateAgents: false }, }); mockAccessService.hasPermission.mockResolvedValue(false); const app = createApp({ type: "agent", agentId: "agent-1", companyId: "company-1", source: "agent_key", runId: "run-1", }); const res = await request(app).get("/api/companies/company-1/environments"); expect(res.status).toBe(403); expect(res.body.error).toContain("Board access required"); expect(mockEnvironmentService.list).not.toHaveBeenCalled(); }); it("rejects agent detail reads for instance-scoped environments", async () => { mockEnvironmentService.getById.mockResolvedValue(createEnvironment()); mockAgentService.getById.mockResolvedValue({ id: "agent-1", companyId: "company-1", role: "cto", permissions: { canCreateAgents: true }, }); mockAccessService.hasPermission.mockResolvedValue(false); const app = createApp({ type: "agent", agentId: "agent-1", companyId: "company-1", source: "agent_key", runId: "run-1", }); const res = await request(app).get("/api/environments/env-1"); expect(res.status).toBe(403); expect(res.body.error).toContain("Board access required"); }); it("creates an environment and logs activity", async () => { const environment = createEnvironment(); mockEnvironmentService.create.mockResolvedValue(environment); const app = createApp(localBoardActor()); const res = await request(app) .post("/api/companies/company-1/environments") .send({ name: "Local", driver: "local", description: "Current development machine", config: { shell: "zsh" }, }); expect(res.status).toBe(201); expect(mockEnvironmentService.create).toHaveBeenCalledWith({ name: "Local", driver: "local", description: "Current development machine", status: "active", config: { shell: "zsh" }, envVars: {}, }, undefined, { db: routeDbTx }); expect(mockLogActivity).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ companyId: "company-1", actorType: "user", actorId: "user-1", agentId: null, runId: null, action: "environment.created", entityType: "environment", entityId: environment.id, }), ); }); it("returns conflict when creating a second local environment", async () => { mockEnvironmentService.list.mockResolvedValue([createEnvironment()]); const app = createApp(localBoardActor()); const res = await request(app) .post("/api/companies/company-1/environments") .send({ name: "Another Local", driver: "local", config: {}, }); expect(res.status).toBe(409); expect(res.body.error).toBe("A local environment already exists for this instance."); expect(mockEnvironmentService.create).not.toHaveBeenCalled(); }); it("rejects non-admin board users even when they have company environment permissions", async () => { const environment = createEnvironment(); mockAccessService.canUser.mockResolvedValue(true); mockEnvironmentService.create.mockResolvedValue(environment); const app = createApp({ type: "board", userId: "user-1", source: "session", companyIds: ["company-1"], isInstanceAdmin: false, }); const res = await request(app) .post("/api/companies/company-1/environments") .send({ name: "Local", driver: "local", config: {}, }); expect(res.status).toBe(403); expect(res.body.error).toContain("Instance admin access required"); expect(mockEnvironmentService.create).not.toHaveBeenCalled(); }); it("rejects non-admin board users without instance admin access", async () => { mockAccessService.canUser.mockResolvedValue(false); const app = createApp({ type: "board", userId: "user-1", source: "session", companyIds: ["company-1"], isInstanceAdmin: false, }); const res = await request(app) .post("/api/companies/company-1/environments") .send({ name: "Local", driver: "local", config: {}, }); expect(res.status).toBe(403); expect(res.body.error).toContain("Instance admin access required"); expect(mockEnvironmentService.create).not.toHaveBeenCalled(); }); it("rejects agent environment creation even with explicit company grants", async () => { const environment = createEnvironment(); mockAgentService.getById.mockResolvedValue({ id: "agent-1", companyId: "company-1", role: "engineer", permissions: { canCreateAgents: false }, }); mockAccessService.hasPermission.mockResolvedValue(true); mockEnvironmentService.create.mockResolvedValue(environment); const app = createApp({ type: "agent", agentId: "agent-1", companyId: "company-1", source: "agent_key", runId: "run-1", }); const res = await request(app) .post("/api/companies/company-1/environments") .send({ name: "Local", driver: "local", config: {}, }); expect(res.status).toBe(403); expect(res.body.error).toContain("board operators"); expect(mockEnvironmentService.create).not.toHaveBeenCalled(); }); it("rejects deleting the managed local environment", async () => { const environment = createEnvironment(); mockEnvironmentService.getById.mockResolvedValue(environment); mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({ isManagedLocal: true, })); const app = createApp(localBoardActor("admin-1")); const res = await request(app).delete("/api/environments/env-1"); expect(res.status).toBe(409); expect(res.body.error).toBe("Cannot delete the managed local environment."); expect(res.body.details).toEqual({ deleteBlockedReasons: ["managed_local"] }); expect(mockEnvironmentService.removeIfDeletable).not.toHaveBeenCalled(); expect(mockExecutionWorkspaceService.clearEnvironmentSelection).not.toHaveBeenCalled(); }); it("rejects deleting the current instance default environment", async () => { const environment = { ...createEnvironment(), driver: "ssh" as const, name: "SSH Fixture", config: createSshConfig(), }; mockEnvironmentService.getById.mockResolvedValue(environment); mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({ isInstanceDefault: true, })); const app = createApp(localBoardActor("admin-1")); const res = await request(app).delete("/api/environments/env-1"); expect(res.status).toBe(409); expect(res.body.error).toBe( "Cannot delete the current instance default environment. Set a new default environment before deleting this one.", ); expect(res.body.details).toEqual({ deleteBlockedReasons: ["instance_default"] }); expect(mockEnvironmentService.removeIfDeletable).not.toHaveBeenCalled(); }); it("rejects deleting an environment with a pending sandbox cleanup", async () => { const environment = { ...createEnvironment(), driver: "ssh" as const, name: "SSH Fixture", config: createSshConfig(), }; mockEnvironmentService.getById.mockResolvedValue(environment); mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({ pendingCleanupLeaseCount: 1, })); const app = createApp(localBoardActor("admin-1")); const res = await request(app).delete("/api/environments/env-1"); expect(res.status).toBe(409); expect(res.body.error).toBe( "Cannot delete this environment while a sandbox cleanup is pending. Wait for the cleanup sweep to destroy the orphan sandbox, then retry.", ); expect(res.body.details).toEqual({ deleteBlockedReasons: ["pending_sandbox_cleanup"] }); expect(mockEnvironmentService.removeIfDeletable).not.toHaveBeenCalled(); }); it("rejects deleting an environment with a live reusable sandbox lease", async () => { const environment = { ...createEnvironment(), driver: "sandbox" as const, name: "Reusable Sandbox Fixture", config: { provider: "fake-plugin", image: "fixture:test", reuseLease: true, }, }; mockEnvironmentService.getById.mockResolvedValue(environment); mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({ activeLeaseCount: 1, reusableSandboxLeaseCount: 1, })); const app = createApp(localBoardActor("admin-1")); const res = await request(app).delete("/api/environments/env-1"); expect(res.status).toBe(409); expect(res.body.error).toBe( "Cannot delete this environment while it has a reusable sandbox lease. Remove the associated execution workspace or issue so Paperclip can destroy the sandbox, then retry.", ); expect(res.body.details).toEqual({ deleteBlockedReasons: ["reusable_sandbox_lease"] }); expect(mockEnvironmentService.removeIfDeletable).not.toHaveBeenCalled(); expect(mockEnvironmentRuntimeService.destroyReusableSandboxLeasesForEnvironment).not.toHaveBeenCalled(); }); it("destroys reusable sandbox leases and deletes with explicit consent", async () => { const environment = createEnvironment(); mockEnvironmentService.getById.mockResolvedValue(environment); mockEnvironmentService.getDeleteBlastRadius .mockResolvedValueOnce(createDeleteBlastRadius({ reusableSandboxLeaseCount: 2 })) .mockResolvedValueOnce(createDeleteBlastRadius()); mockEnvironmentRuntimeService.destroyReusableSandboxLeasesForEnvironment.mockResolvedValue({ destroyed: 2, failed: 0, skippedLiveRun: 0, }); mockEnvironmentService.removeIfDeletable.mockResolvedValue(environment); mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1"]); const app = createApp(localBoardActor("admin-1")); const res = await request(app).delete("/api/environments/env-1?destroyReusableSandboxLeases=true"); expect(res.status).toBe(200); expect( mockEnvironmentRuntimeService.destroyReusableSandboxLeasesForEnvironment, ).toHaveBeenCalledExactlyOnceWith({ environmentId: "env-1", failureReason: "environment_deleted", }); expect( mockCloseWarmNativeSessionsForEnvironment, ).toHaveBeenCalledExactlyOnceWith({ environmentId: "env-1", reason: "environment deleted", }); expect(mockEnvironmentService.removeIfDeletable).toHaveBeenCalledWith( "env-1", ); expect(res.body.destroyedReusableSandboxLeaseCount).toBe(2); }); it("keeps rejecting a consented delete when a non-lease blocker remains", async () => { const environment = createEnvironment(); mockEnvironmentService.getById.mockResolvedValue(environment); mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({ isInstanceDefault: true, reusableSandboxLeaseCount: 1, })); const app = createApp(localBoardActor("admin-1")); const res = await request(app).delete("/api/environments/env-1?destroyReusableSandboxLeases=true"); // Destroying provider sandboxes and then rejecting on the other gate would // be an irreversible action with nothing gained, so the destroy must not run. expect(res.status).toBe(409); expect(mockEnvironmentRuntimeService.destroyReusableSandboxLeasesForEnvironment).not.toHaveBeenCalled(); expect(mockEnvironmentService.removeIfDeletable).not.toHaveBeenCalled(); }); it("keeps rejecting when leases survive the consented destroy", async () => { const environment = createEnvironment(); mockEnvironmentService.getById.mockResolvedValue(environment); mockEnvironmentService.getDeleteBlastRadius .mockResolvedValueOnce(createDeleteBlastRadius({ reusableSandboxLeaseCount: 1 })) .mockResolvedValueOnce(createDeleteBlastRadius({ pendingCleanupLeaseCount: 1 })); mockEnvironmentRuntimeService.destroyReusableSandboxLeasesForEnvironment.mockResolvedValue({ destroyed: 1, failed: 1, skippedLiveRun: 0, }); const app = createApp(localBoardActor("admin-1")); const res = await request(app).delete("/api/environments/env-1?destroyReusableSandboxLeases=true"); expect(res.status).toBe(409); // The rejection names what the consented destroy already did: provider // destruction is not transactional with the delete guard. expect(res.body.details).toEqual({ deleteBlockedReasons: ["pending_sandbox_cleanup"], destroyedReusableSandboxLeaseCount: 1, }); expect(mockEnvironmentService.removeIfDeletable).not.toHaveBeenCalled(); }); it("rejects a driver or provider config change while a sandbox cleanup is pending", async () => { const environment = { ...createEnvironment(), id: "env-ssh", driver: "ssh" as const, name: "SSH Fixture", config: createSshConfig(), }; mockEnvironmentService.getById.mockResolvedValue(environment); mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockResolvedValue(true); const app = createApp(localBoardActor("admin-1")); const res = await request(app) .patch("/api/environments/env-ssh") .send({ config: { host: "changed.example.test", port: 22, username: "ssh-user", remoteWorkspacePath: "/srv/paperclip/workspace", }, }); expect(res.status).toBe(409); expect(res.body.error).toBe( "Cannot change the driver or provider config while a sandbox cleanup is pending. Wait for the cleanup sweep to destroy the orphan sandbox, then retry.", ); expect(res.body.details).toEqual({ code: "environment_pending_sandbox_cleanup" }); expect(mockEnvironmentService.update).not.toHaveBeenCalled(); }); it("allows a non-provider update while a sandbox cleanup is pending", async () => { const environment = { ...createEnvironment(), id: "env-ssh", driver: "ssh" as const, name: "SSH Fixture", config: createSshConfig(), }; mockEnvironmentService.getById.mockResolvedValue(environment); mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockResolvedValue(true); mockEnvironmentService.update.mockResolvedValue({ ...environment, description: "Updated" }); const app = createApp(localBoardActor("admin-1")); const res = await request(app) .patch("/api/environments/env-ssh") .send({ description: "Updated" }); expect(res.status).toBe(200); expect(mockEnvironmentService.hasUnresolvedPendingCleanupLeases).not.toHaveBeenCalled(); expect(mockEnvironmentService.update).toHaveBeenCalled(); }); it("describes an environment's secret refs with owner metadata", async () => { const secretId = "22222222-2222-2222-2222-222222222222"; mockEnvironmentService.getById.mockResolvedValue({ ...createEnvironment(), id: "env-sandbox", name: "Daytona", driver: "sandbox" as const, config: { provider: "secure-plugin", template: "base", apiKey: secretId, timeoutMs: 450000, reuseLease: true, }, }); mockSecretService.describeSecretRefs.mockResolvedValue([ { configPath: "apiKey", secretId, name: "DAYTONA_API_KEY", status: "active", companyId: "company-2", companyName: "Other Team", }, ]); const app = createApp(localBoardActor("admin-1")); const res = await request(app).get("/api/environments/env-sandbox/secret-refs"); expect(res.status).toBe(200); expect(res.body.refs).toEqual([ { configPath: "apiKey", secretId, name: "DAYTONA_API_KEY", status: "active", companyId: "company-2", companyName: "Other Team", }, ]); expect(mockSecretService.describeSecretRefs).toHaveBeenCalledWith([ { secretId, configPath: "apiKey", versionSelector: "latest" }, ]); }); it("denies secret-ref descriptors to agents without instance environment access", async () => { mockAgentService.getById.mockResolvedValue({ id: "agent-1", companyId: "company-1", role: "engineer", permissions: { canCreateAgents: false }, }); mockAccessService.hasPermission.mockResolvedValue(false); const app = createApp({ type: "agent", agentId: "agent-1", companyId: "company-1", source: "agent_key", }); const res = await request(app).get("/api/environments/env-1/secret-refs"); expect(res.status).toBe(403); expect(mockSecretService.describeSecretRefs).not.toHaveBeenCalled(); }); it("clears environment selections and secret bindings across all companies when deleting an environment", async () => { const environment = createSecretBackedSshEnvironment(); mockEnvironmentService.getById.mockResolvedValue(environment); mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius()); mockEnvironmentService.removeIfDeletable.mockResolvedValue(environment); mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1", "company-2"]); const app = createApp(localBoardActor("admin-1")); const res = await request(app).delete("/api/environments/env-ssh"); expect(res.status).toBe(200); expect(mockEnvironmentService.removeIfDeletable).toHaveBeenCalledWith("env-ssh"); for (const companyId of ["company-1", "company-2"]) { expect(mockExecutionWorkspaceService.clearEnvironmentSelection) .toHaveBeenCalledWith(companyId, "env-ssh"); expect(mockIssueService.clearExecutionWorkspaceEnvironmentSelection) .toHaveBeenCalledWith(companyId, "env-ssh"); expect(mockProjectService.clearExecutionWorkspaceEnvironmentSelection) .toHaveBeenCalledWith(companyId, "env-ssh"); expect(mockSecretService.syncEnvBindingsForTarget).toHaveBeenCalledWith( companyId, { targetType: "environment", targetId: "env-ssh" }, {}, ); expect(mockSecretService.syncSecretRefsForTarget).toHaveBeenCalledWith( companyId, { targetType: "environment", targetId: "env-ssh" }, [], { replaceAll: true }, ); } expect(mockSecretService.remove).toHaveBeenCalledWith("11111111-1111-1111-1111-111111111111"); expect(mockLogActivity).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ companyId: "company-1", action: "environment.deleted", entityType: "environment", entityId: "env-ssh", }), ); }); it("rejects invalid SSH config on create", async () => { const app = createApp(localBoardActor()); const res = await request(app) .post("/api/companies/company-1/environments") .send({ name: "SSH Fixture", driver: "ssh", config: { host: "ssh.example.test", username: "ssh-user", }, }); expect(res.status).toBe(422); expect(res.body.error).toContain("remote workspace path"); expect(mockEnvironmentService.create).not.toHaveBeenCalled(); }); it("normalizes SSH private keys into secret refs before persistence", async () => { const environment = createSecretBackedSshEnvironment(); mockEnvironmentService.create.mockResolvedValue(environment); const pluginWorkerManager = {}; const app = createApp(localBoardActor(), { pluginWorkerManager }); const res = await request(app) .post("/api/companies/company-1/environments") .send({ name: "SSH Fixture", driver: "ssh", config: { host: "ssh.example.test", username: "ssh-user", remoteWorkspacePath: "/srv/paperclip/workspace", privateKey: " super-secret-key ", }, }); expect(res.status).toBe(201); expect(mockEnvironmentService.create).toHaveBeenCalledWith(expect.objectContaining({ config: expect.objectContaining({ privateKey: null, privateKeySecretRef: { type: "secret_ref", secretId: "11111111-1111-1111-1111-111111111111", version: "latest", }, }), envVars: {}, }), undefined, { db: routeDbTx }); expect(JSON.stringify(mockEnvironmentService.create.mock.calls[0][0])).not.toContain("super-secret-key"); expect(mockSecretService.create).toHaveBeenCalledWith( "company-1", expect.objectContaining({ provider: "local_encrypted", value: "super-secret-key", }), expect.any(Object), ); }); it("uses the configured provider for SSH private key secret materialization", async () => { process.env.PAPERCLIP_SECRETS_PROVIDER = "aws_secrets_manager"; const environment = createSecretBackedSshEnvironment(); mockEnvironmentService.create.mockResolvedValue(environment); const app = createApp(localBoardActor()); const res = await request(app) .post("/api/companies/company-1/environments") .send({ name: "SSH Fixture", driver: "ssh", config: { host: "ssh.example.test", username: "ssh-user", remoteWorkspacePath: "/srv/paperclip/workspace", privateKey: "super-secret-key", }, }); expect(res.status).toBe(201); expect(mockSecretService.create).toHaveBeenCalledWith( "company-1", expect.objectContaining({ provider: "aws_secrets_manager", value: "super-secret-key", }), expect.any(Object), ); }); it("rejects persisted fake sandbox environments", async () => { const app = createApp(localBoardActor()); const res = await request(app) .post("/api/companies/company-1/environments") .send({ name: "Fake Sandbox", driver: "sandbox", config: { provider: "fake", image: " ubuntu:24.04 ", }, }); expect(res.status).toBe(422); expect(res.body.error).toContain("reserved for internal probes"); expect(mockEnvironmentService.create).not.toHaveBeenCalled(); }); it("creates a sandbox environment with normalized Fake plugin config", async () => { const environment = { ...createEnvironment(), id: "env-sandbox-fake-plugin", name: "Fake plugin Sandbox", driver: "sandbox" as const, config: { provider: "fake-plugin", image: "fake:test", timeoutMs: 450000, reuseLease: true, }, }; mockEnvironmentService.create.mockResolvedValue(environment); const pluginWorkerManager = {}; const app = createApp(localBoardActor(), { pluginWorkerManager }); const res = await request(app) .post("/api/companies/company-1/environments") .send({ name: "Fake plugin Sandbox", driver: "sandbox", config: { provider: "fake-plugin", image: "fake:test", timeoutMs: "450000", reuseLease: true, }, }); expect(res.status).toBe(201); expect(mockValidatePluginSandboxProviderConfig).toHaveBeenCalledWith({ db: expect.anything(), workerManager: pluginWorkerManager, provider: "fake-plugin", config: { image: "fake:test", timeoutMs: 450000, reuseLease: true, }, }); expect(mockEnvironmentService.create).toHaveBeenCalledWith({ name: "Fake plugin Sandbox", driver: "sandbox", status: "active", config: { provider: "fake-plugin", image: "fake:test", timeoutMs: 450000, reuseLease: true, }, envVars: {}, }, undefined, { db: routeDbTx }); expect(mockSecretService.create).not.toHaveBeenCalled(); }); it("keeps host-owned sandbox flags and drops a removed flag a saved config still carries", async () => { // The host owns run-log streaming and runner lifecycle. A provider plugin // may normalize only its own driver fields, so it drops these host flags. // The host must re-apply them or a saved warm environment silently becomes // per-turn at execution time. // // `streamAgentSessionOutput` is a removed operator flag. A saved config can // still carry it, but session-output streaming now follows the capability // snapshot alone. The removed key must load and then drop, so it never // reaches the persisted config. const environment = { ...createEnvironment(), id: "env-sandbox-fake-plugin", name: "Streamed Sandbox", driver: "sandbox" as const, config: { provider: "fake-plugin", image: "fake:test" }, }; mockEnvironmentService.create.mockResolvedValue(environment); mockValidatePluginSandboxProviderConfig.mockImplementation( async ({ provider, config }) => { // Drop the host flag to reproduce a plugin that allowlists driver fields. const { streamRunLogs, runnerLifecycleMode, runnerIdleTimeoutMs, ...driverConfig } = config as Record; void streamRunLogs; void runnerLifecycleMode; void runnerIdleTimeoutMs; return { normalizedConfig: driverConfig, pluginId: `plugin-${provider}`, pluginKey: `plugin.${provider}`, driver: { driverKey: provider, kind: "sandbox_provider", displayName: provider, configSchema: { type: "object" }, }, }; }, ); const pluginWorkerManager = {}; const app = createApp(localBoardActor(), { pluginWorkerManager }); const res = await request(app) .post("/api/companies/company-1/environments") .send({ name: "Streamed Sandbox", driver: "sandbox", config: { provider: "fake-plugin", image: "fake:test", streamRunLogs: false, runnerLifecycleMode: "warm", runnerIdleTimeoutMs: 180_000, streamAgentSessionOutput: true, }, }); expect(res.status).toBe(201); const persisted = mockEnvironmentService.create.mock.calls[0][0].config as Record; // The removed key never reaches the persisted config. expect(persisted.streamAgentSessionOutput).toBeUndefined(); expect(persisted.streamRunLogs).toBe(false); expect(persisted.runnerLifecycleMode).toBe("warm"); expect(persisted.runnerIdleTimeoutMs).toBe(180_000); }); it("creates a schema-driven sandbox environment with secret-ref fields persisted as secrets", async () => { const environment = { ...createEnvironment(), id: "env-sandbox-secure-plugin", name: "Secure Sandbox", driver: "sandbox" as const, config: { provider: "secure-plugin", template: "base", apiKey: "11111111-1111-1111-1111-111111111111", timeoutMs: 450000, reuseLease: true, }, }; mockEnvironmentService.create.mockResolvedValue(environment); mockValidatePluginSandboxProviderConfig.mockResolvedValue({ normalizedConfig: { template: "base", apiKey: "test-provider-key", timeoutMs: 450000, reuseLease: true, }, pluginId: "plugin-secure", pluginKey: "acme.secure-sandbox-provider", driver: createSecurePluginDriver(), }); const pluginWorkerManager = {}; const app = createApp(localBoardActor(), { pluginWorkerManager }); const res = await request(app) .post("/api/companies/company-1/environments") .send({ name: "Secure Sandbox", driver: "sandbox", config: { provider: "secure-plugin", template: " base ", apiKey: " test-provider-key ", timeoutMs: "450000", reuseLease: true, }, }); expect(res.status).toBe(201); expect(mockValidatePluginSandboxProviderConfig).toHaveBeenCalledWith({ db: expect.anything(), workerManager: pluginWorkerManager, provider: "secure-plugin", config: { template: " base ", apiKey: " test-provider-key ", timeoutMs: 450000, reuseLease: true, }, }); expect(mockEnvironmentService.create).toHaveBeenCalledWith({ name: "Secure Sandbox", driver: "sandbox", status: "active", config: { provider: "secure-plugin", template: "base", apiKey: "11111111-1111-1111-1111-111111111111", timeoutMs: 450000, reuseLease: true, }, envVars: {}, }, undefined, { db: routeDbTx }); expect(JSON.stringify(mockEnvironmentService.create.mock.calls[0][0])).not.toContain("test-provider-key"); expect(mockSecretService.create).toHaveBeenCalledWith( "company-1", expect.objectContaining({ provider: "local_encrypted", value: "test-provider-key", }), expect.any(Object), ); }); it("persists a picker-submitted secret_ref binding object as the bare secret id", async () => { const secretId = "11111111-1111-1111-1111-111111111111"; const environment = { ...createEnvironment(), id: "env-sandbox-secure-plugin", name: "Secure Sandbox", driver: "sandbox" as const, config: { provider: "secure-plugin", template: "base", apiKey: secretId, timeoutMs: 450000, reuseLease: true, }, }; mockEnvironmentService.create.mockResolvedValue(environment); mockValidatePluginSandboxProviderConfig.mockImplementation(async ({ config }: { config: Record }) => ({ normalizedConfig: { ...config }, pluginId: "plugin-secure", pluginKey: "acme.secure-sandbox-provider", driver: createSecurePluginDriver(), })); const pluginWorkerManager = {}; const app = createApp(localBoardActor(), { pluginWorkerManager }); const res = await request(app) .post("/api/companies/company-1/environments") .send({ name: "Secure Sandbox", driver: "sandbox", config: { provider: "secure-plugin", template: "base", apiKey: { type: "secret_ref", secretId, version: "latest" }, timeoutMs: 450000, reuseLease: true, }, }); expect(res.status).toBe(201); expect(mockEnvironmentService.create).toHaveBeenCalledWith({ name: "Secure Sandbox", driver: "sandbox", status: "active", config: { provider: "secure-plugin", template: "base", apiKey: secretId, timeoutMs: 450000, reuseLease: true, }, envVars: {}, }, undefined, { db: routeDbTx }); expect(mockSecretService.create).not.toHaveBeenCalled(); expect(mockSecretService.replaceSecretRefsForInstanceTarget).toHaveBeenCalledWith( { targetType: "environment", targetId: "env-sandbox-secure-plugin" }, [{ secretId, configPath: "apiKey", versionSelector: "latest" }], { db: routeDbTx }, ); }); it("uses the configured provider for schema-driven sandbox secret fields", async () => { process.env.PAPERCLIP_SECRETS_PROVIDER = "aws_secrets_manager"; const environment = { ...createEnvironment(), id: "env-sandbox-secure-plugin", name: "Secure Sandbox", driver: "sandbox" as const, config: { provider: "secure-plugin", template: "base", apiKey: "11111111-1111-1111-1111-111111111111", timeoutMs: 450000, reuseLease: true, }, }; mockEnvironmentService.create.mockResolvedValue(environment); mockValidatePluginSandboxProviderConfig.mockResolvedValue({ normalizedConfig: { template: "base", apiKey: "test-provider-key", timeoutMs: 450000, reuseLease: true, }, pluginId: "plugin-secure", pluginKey: "acme.secure-sandbox-provider", driver: createSecurePluginDriver(), }); const pluginWorkerManager = {}; const app = createApp(localBoardActor(), { pluginWorkerManager }); const res = await request(app) .post("/api/companies/company-1/environments") .send({ name: "Secure Sandbox", driver: "sandbox", config: { provider: "secure-plugin", template: "base", apiKey: "test-provider-key", timeoutMs: "450000", reuseLease: true, }, }); expect(res.status).toBe(201); expect(mockSecretService.create).toHaveBeenCalledWith( "company-1", expect.objectContaining({ provider: "aws_secrets_manager", value: "test-provider-key", }), expect.any(Object), ); }); it("validates plugin environment config through the plugin driver host", async () => { const environment = { ...createEnvironment(), id: "env-plugin", name: "Plugin Sandbox", driver: "plugin" as const, config: { pluginKey: "acme.environments", driverKey: "sandbox", driverConfig: { template: "normalized", }, }, }; mockValidatePluginEnvironmentDriverConfig.mockResolvedValue(environment.config); mockEnvironmentService.create.mockResolvedValue(environment); const pluginWorkerManager = {}; const app = createApp(localBoardActor(), { pluginWorkerManager }); const res = await request(app) .post("/api/companies/company-1/environments") .send({ name: "Plugin Sandbox", driver: "plugin", config: { pluginKey: "acme.environments", driverKey: "sandbox", driverConfig: { template: "base", }, }, }); expect(res.status).toBe(201); expect(mockValidatePluginEnvironmentDriverConfig).toHaveBeenCalledWith({ db: expect.anything(), workerManager: pluginWorkerManager, config: { pluginKey: "acme.environments", driverKey: "sandbox", driverConfig: { template: "base", }, }, }); expect(mockEnvironmentService.create).toHaveBeenCalledWith(expect.objectContaining({ config: environment.config, envVars: {}, }), undefined, { db: routeDbTx }); }); it("rejects agent mutations for instance-scoped environments", async () => { mockAgentService.getById.mockResolvedValue({ id: "agent-1", companyId: "company-1", role: "engineer", permissions: { canCreateAgents: false }, }); mockAccessService.hasPermission.mockResolvedValue(false); const app = createApp({ type: "agent", agentId: "agent-1", companyId: "company-1", source: "agent_key", runId: "run-1", }); const res = await request(app) .post("/api/companies/company-1/environments") .send({ name: "Sandbox host", driver: "local", }); expect(res.status).toBe(403); expect(res.body.error).toContain("board operators"); expect(mockEnvironmentService.create).not.toHaveBeenCalled(); }); it("lists leases for an environment after company access is confirmed", async () => { const environment = createEnvironment(); mockEnvironmentService.getById.mockResolvedValue(environment); mockEnvironmentService.listLeases.mockResolvedValue([ { id: "lease-1", companyId: "company-1", environmentId: environment.id, executionWorkspaceId: "workspace-1", issueId: null, heartbeatRunId: null, status: "active", providerLeaseId: "provider-lease-1", acquiredAt: new Date("2026-04-16T05:00:00.000Z"), lastUsedAt: new Date("2026-04-16T05:05:00.000Z"), expiresAt: null, releasedAt: null, metadata: { provider: "fake" }, createdAt: new Date("2026-04-16T05:00:00.000Z"), updatedAt: new Date("2026-04-16T05:05:00.000Z"), }, ]); const app = createApp(localBoardActor()); const res = await request(app).get(`/api/environments/${environment.id}/leases?status=active`); expect(res.status).toBe(200); expect(mockEnvironmentService.listLeases).toHaveBeenCalledWith(environment.id, { status: "active", }); }); it("returns a single lease after company access is confirmed", async () => { mockEnvironmentService.getLeaseById.mockResolvedValue({ id: "lease-1", companyId: "company-1", environmentId: "env-1", executionWorkspaceId: "workspace-1", issueId: null, heartbeatRunId: "run-1", status: "active", leasePolicy: "ephemeral", provider: "ssh", providerLeaseId: "ssh://ssh-user@example.test:22/workspace", acquiredAt: new Date("2026-04-16T05:00:00.000Z"), lastUsedAt: new Date("2026-04-16T05:05:00.000Z"), expiresAt: null, releasedAt: null, failureReason: null, cleanupStatus: null, metadata: { remoteCwd: "/workspace" }, createdAt: new Date("2026-04-16T05:00:00.000Z"), updatedAt: new Date("2026-04-16T05:05:00.000Z"), }); const app = createApp(localBoardActor()); const res = await request(app).get("/api/environment-leases/lease-1"); expect(res.status).toBe(200); expect(res.body.provider).toBe("ssh"); expect(mockEnvironmentService.getLeaseById).toHaveBeenCalledWith("lease-1"); }); it("rejects agent access regardless of company when environment management is instance-scoped", async () => { mockEnvironmentService.list.mockResolvedValue([]); const app = createApp({ type: "agent", agentId: "agent-2", companyId: "company-2", source: "agent_key", runId: "run-2", }); const res = await request(app).get("/api/companies/company-1/environments"); expect(res.status).toBe(403); expect(res.body.error).toContain("Board access required"); expect(mockEnvironmentService.list).not.toHaveBeenCalled(); }); it("logs a redacted update summary instead of raw config or metadata", async () => { const environment = createEnvironment(); mockEnvironmentService.getById.mockResolvedValue(environment); mockEnvironmentService.update.mockResolvedValue({ ...environment, status: "archived", }); const app = createApp(localBoardActor()); const res = await request(app) .patch(`/api/environments/${environment.id}?companyId=company-1`) .send({ status: "archived", config: { apiKey: "super-secret", token: "another-secret", }, metadata: { password: "do-not-log", }, }); expect(res.status).toBe(200); expect(mockLogActivity).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ action: "environment.updated", details: { changedFields: ["config", "metadata", "status"], status: "archived", configChanged: true, configTopLevelKeyCount: expect.any(Number), metadataChanged: true, metadataTopLevelKeyCount: 1, }, }), ); expect(JSON.stringify(mockLogActivity.mock.calls[0][1].details)).not.toContain("super-secret"); expect(JSON.stringify(mockLogActivity.mock.calls[0][1].details)).not.toContain("do-not-log"); }); it("resets config instead of inheriting SSH secrets when switching to local without an explicit config", async () => { const environment = { ...createEnvironment(), name: "SSH Fixture", driver: "ssh" as const, config: { host: "ssh.example.test", port: 22, username: "ssh-user", remoteWorkspacePath: "/srv/paperclip/workspace", privateKey: "super-secret-key", knownHosts: "known-host", strictHostKeyChecking: true, }, }; mockEnvironmentService.getById.mockResolvedValue(environment); mockEnvironmentService.update.mockResolvedValue({ ...createEnvironment(), driver: "local" as const, config: {}, }); const app = createApp(localBoardActor()); const res = await request(app) .patch(`/api/environments/${environment.id}?companyId=company-1`) .send({ driver: "local", }); expect(res.status).toBe(200); expect(mockEnvironmentService.update).toHaveBeenCalledWith(environment.id, { driver: "local", config: {}, }, { db: routeDbTx }); expect(JSON.stringify(mockEnvironmentService.update.mock.calls[0][1])).not.toContain("super-secret-key"); expect(JSON.stringify(mockEnvironmentService.update.mock.calls[0][1])).not.toContain("known-host"); }); it("re-points a sandbox secret ref to another company's secret even when existing bindings disagree", async () => { const oldSecretId = "11111111-1111-1111-1111-111111111111"; const newSecretId = "22222222-2222-2222-2222-222222222222"; const existing = { ...createEnvironment(), id: "env-sandbox", name: "Daytona", driver: "sandbox" as const, config: { provider: "secure-plugin", template: "base", apiKey: oldSecretId, timeoutMs: 450000, reuseLease: true, }, }; const updated = { ...existing, config: { ...existing.config, apiKey: newSecretId }, }; mockEnvironmentService.getById.mockResolvedValue(existing); mockEnvironmentService.update.mockResolvedValue(updated); mockValidatePluginSandboxProviderConfig.mockImplementation(async ({ config }: { config: Record }) => ({ normalizedConfig: { ...config }, pluginId: "plugin-secure", pluginKey: "acme.secure-sandbox-provider", driver: createSecurePluginDriver(), })); // The environment's only binding still lives in another company. Before // bindings moved with the referenced secret, this state made every save // from the caller's company fail, with no route-level way out. mockSecretService.listBindingCompanyIdsForTarget.mockResolvedValue(["company-old"]); const app = createApp(localBoardActor(), { pluginWorkerManager: {} }); const res = await request(app) .patch("/api/environments/env-sandbox?companyId=company-new") .send({ config: { apiKey: { type: "secret_ref", secretId: newSecretId, version: "latest" }, }, }); expect(res.status).toBe(200); expect(mockEnvironmentService.update).toHaveBeenCalledWith( "env-sandbox", expect.objectContaining({ config: expect.objectContaining({ apiKey: newSecretId }), }), { db: routeDbTx }, ); expect(mockSecretService.replaceSecretRefsForInstanceTarget).toHaveBeenCalledWith( { targetType: "environment", targetId: "env-sandbox" }, [{ secretId: newSecretId, configPath: "apiKey", versionSelector: "latest" }], { db: routeDbTx }, ); // Explicit caller context wins outright; stale bindings are never consulted. expect(mockSecretService.listBindingCompanyIdsForTarget).not.toHaveBeenCalled(); expect(mockSecretService.syncSecretRefsForTarget).not.toHaveBeenCalled(); }); it("fails the whole save when a referenced secret cannot be bound", async () => { const existing = { ...createEnvironment(), id: "env-sandbox", name: "Daytona", driver: "sandbox" as const, config: { provider: "secure-plugin", template: "base", apiKey: "11111111-1111-1111-1111-111111111111", timeoutMs: 450000, reuseLease: true, }, }; mockEnvironmentService.getById.mockResolvedValue(existing); mockEnvironmentService.update.mockResolvedValue({ ...existing, config: { ...existing.config, apiKey: "33333333-3333-3333-3333-333333333333" }, }); mockSecretService.replaceSecretRefsForInstanceTarget.mockRejectedValue( unprocessable("Secret referenced at apiKey was not found", { code: "secret_missing" }), ); const app = createApp(localBoardActor(), { pluginWorkerManager: {} }); const res = await request(app) .patch("/api/environments/env-sandbox?companyId=company-1") .send({ config: { apiKey: { type: "secret_ref", secretId: "33333333-3333-3333-3333-333333333333", version: "latest" }, }, }); expect(res.status).toBe(422); expect(res.body.error).toContain("was not found"); }); it("requires explicit SSH config when switching from local to SSH", async () => { mockEnvironmentService.getById.mockResolvedValue(createEnvironment()); const app = createApp(localBoardActor()); const res = await request(app) .patch("/api/environments/env-1?companyId=company-1") .send({ driver: "ssh", }); expect(res.status).toBe(422); expect(res.body.error).toContain("host"); expect(mockEnvironmentService.update).not.toHaveBeenCalled(); }); it("rejects switching an environment to the built-in fake sandbox provider", async () => { mockEnvironmentService.getById.mockResolvedValue(createEnvironment()); const app = createApp(localBoardActor()); const res = await request(app) .patch("/api/environments/env-1?companyId=company-1") .send({ driver: "sandbox", config: { provider: "fake", image: "ubuntu:24.04", }, }); expect(res.status).toBe(422); expect(res.body.error).toContain("reserved for internal probes"); expect(mockEnvironmentService.update).not.toHaveBeenCalled(); }); it("returns 404 when patching a missing environment", async () => { mockEnvironmentService.getById.mockResolvedValue(null); const app = createApp(localBoardActor()); const res = await request(app) .patch("/api/environments/missing-env") .send({ status: "archived" }); expect(res.status).toBe(404); expect(res.body.error).toBe("Environment not found"); expect(mockLogActivity).not.toHaveBeenCalled(); }); it("probes an SSH environment and logs the result", async () => { const environment = { ...createEnvironment(), name: "SSH Fixture", driver: "ssh" as const, config: { host: "ssh.example.test", port: 22, username: "ssh-user", remoteWorkspacePath: "/srv/paperclip/workspace", privateKey: null, knownHosts: null, strictHostKeyChecking: true, }, }; mockEnvironmentService.getById.mockResolvedValue(environment); mockProbeEnvironment.mockResolvedValue({ ok: true, driver: "ssh", summary: "Connected to ssh-user@ssh.example.test and verified the remote workspace path.", details: { host: "ssh.example.test", }, }); const app = createApp({ type: "board", userId: "user-1", source: "local_implicit", runId: "run-1", }); const res = await request(app) .post(`/api/environments/${environment.id}/probe`) .send({}); expect(res.status).toBe(200); expect(res.body.ok).toBe(true); expect(mockProbeEnvironment).toHaveBeenCalledWith(expect.anything(), environment, { // The instance has exactly one company, so the secret-context fallback // resolves it even though the actor carries no memberships. companyId: "company-1", pluginWorkerManager: undefined, applyCustomImageTemplate: false, acquireSandboxRuntimeLease: false, }); expect(mockLogActivity).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ companyId: "company-1", action: "environment.probed", entityType: "environment", entityId: environment.id, details: expect.objectContaining({ driver: "ssh", ok: true, }), }), ); }); it("requires explicit companyId when probing a secret-backed environment without inferable secret context", async () => { const environment = { ...createEnvironment(), driver: "ssh" as const, config: { host: "ssh.example.test", port: 22, username: "ssh-user", remoteWorkspacePath: "/srv/paperclip/workspace", privateKey: null, privateKeySecretRef: { type: "secret_ref", secretId: "11111111-1111-4111-8111-111111111111", version: "latest", }, knownHosts: null, strictHostKeyChecking: true, }, }; mockEnvironmentService.getById.mockResolvedValue(environment); mockSecretService.listBindingCompanyIdsForTarget.mockResolvedValue([]); // A multi-company instance keeps the context genuinely ambiguous — a // single-company instance would resolve via the instance fallback. mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1", "company-2"]); const app = createApp({ type: "board", userId: "user-1", source: "session", companyIds: ["company-1", "company-2"], memberships: [ { companyId: "company-1", status: "active", membershipRole: "member" }, { companyId: "company-2", status: "active", membershipRole: "member" }, ], isInstanceAdmin: true, runId: "run-1", }); const res = await request(app) .post(`/api/environments/${environment.id}/probe`) .send({}); expect(res.status).toBe(422); expect(res.body.error).toContain("explicit companyId"); expect(mockProbeEnvironment).not.toHaveBeenCalled(); }); it("probes a sandbox environment and logs the result", async () => { const environment = { ...createEnvironment(), id: "env-sandbox", name: "Fake Sandbox", driver: "sandbox" as const, config: { provider: "fake", image: "ubuntu:24.04", reuseLease: true, }, }; mockEnvironmentService.getById.mockResolvedValue(environment); mockProbeEnvironment.mockResolvedValue({ ok: true, driver: "sandbox", summary: "Fake sandbox provider is ready for image ubuntu:24.04.", details: { provider: "fake", image: "ubuntu:24.04", reuseLease: true, }, }); const app = createApp({ type: "board", userId: "user-1", source: "local_implicit", runId: "run-1", }); const res = await request(app) .post(`/api/environments/${environment.id}/probe`) .send({}); expect(res.status).toBe(200); expect(res.body.driver).toBe("sandbox"); expect(mockProbeEnvironment).toHaveBeenCalledWith(expect.anything(), environment, { companyId: "company-1", pluginWorkerManager: undefined, applyCustomImageTemplate: true, acquireSandboxRuntimeLease: true, }); expect(mockLogActivity).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ companyId: "company-1", action: "environment.probed", entityType: "environment", entityId: environment.id, details: expect.objectContaining({ driver: "sandbox", ok: true, }), }), ); }); it("probes saved sandbox environments with the active custom image template without company context", async () => { const environment = { ...createEnvironment(), id: "env-sandbox", name: "Daytona Sandbox", driver: "sandbox" as const, config: { provider: "daytona", image: "ubuntu:24.04", reuseLease: true, }, }; mockEnvironmentService.getById.mockResolvedValue(environment); mockProbeEnvironment.mockResolvedValue({ ok: true, driver: "sandbox", summary: "Connected to Daytona sandbox.", details: { provider: "daytona", snapshot: "captured-template", }, }); const app = createApp({ type: "board", userId: "user-1", source: "local_implicit", runId: "run-1", }); const res = await request(app) .post(`/api/environments/${environment.id}/probe`) .send({}); expect(res.status).toBe(200); expect(res.body.driver).toBe("sandbox"); expect(mockProbeEnvironment).toHaveBeenCalledWith(expect.anything(), environment, { companyId: "company-1", pluginWorkerManager: undefined, applyCustomImageTemplate: true, acquireSandboxRuntimeLease: true, }); }); it("probes unsaved provider config without persisting secrets", async () => { mockProbeEnvironment.mockResolvedValue({ ok: true, driver: "sandbox", summary: "Fake plugin sandbox provider is ready.", details: { provider: "fake-plugin" }, }); const pluginWorkerManager = {}; const app = createApp({ type: "board", userId: "user-1", source: "local_implicit", runId: "run-1", }, { pluginWorkerManager }); const res = await request(app) .post("/api/companies/company-1/environments/probe-config") .send({ name: "Draft Fake plugin", driver: "sandbox", config: { provider: "fake-plugin", template: "base", apiKey: "unsaved-test-key", timeoutMs: 300000, reuseLease: true, }, }); expect(res.status).toBe(200); expect(mockEnvironmentService.create).not.toHaveBeenCalled(); expect(mockSecretService.create).not.toHaveBeenCalled(); expect(mockProbeEnvironment).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ id: "unsaved", driver: "sandbox", config: expect.objectContaining({ apiKey: "unsaved-test-key", }), }), expect.objectContaining({ pluginWorkerManager, resolvedConfig: expect.objectContaining({ driver: "sandbox", }), }), ); expect(JSON.stringify(mockLogActivity.mock.calls[0][1].details)).not.toContain("unsaved-test-key"); }); it("resolves selected secret refs before probing unsaved provider config", async () => { mockValidatePluginSandboxProviderConfig.mockResolvedValue({ normalizedConfig: { template: "base", apiKey: "11111111-1111-1111-1111-111111111111", timeoutMs: 300000, reuseLease: true, }, pluginId: "plugin-secure", pluginKey: "acme.secure-sandbox-provider", driver: createSecurePluginDriver(), }); mockProbeEnvironment.mockResolvedValue({ ok: true, driver: "sandbox", summary: "Secure sandbox provider is ready.", details: { provider: "secure-plugin" }, }); const pluginWorkerManager = {}; const app = createApp({ type: "board", userId: "user-1", source: "local_implicit", runId: "run-1", }, { pluginWorkerManager }); const res = await request(app) .post("/api/companies/company-1/environments/probe-config") .send({ name: "Draft Secure Sandbox", driver: "sandbox", config: { provider: "secure-plugin", template: "base", apiKey: "11111111-1111-1111-1111-111111111111", timeoutMs: 300000, reuseLease: true, }, }); expect(res.status).toBe(200); expect(mockEnvironmentService.create).not.toHaveBeenCalled(); expect(mockSecretService.create).not.toHaveBeenCalled(); expect(mockSecretService.resolveSecretValueForEphemeralAccess).toHaveBeenCalledWith( "company-1", "11111111-1111-1111-1111-111111111111", "latest", { consumerType: "system", consumerId: "environment-probe-config", configPath: "apiKey", actorType: "user", actorId: "user-1", actorSource: "local_implicit", heartbeatRunId: "run-1", }, ); expect(mockProbeEnvironment).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ id: "unsaved", driver: "sandbox", config: expect.objectContaining({ apiKey: "resolved-provider-key", }), }), expect.objectContaining({ pluginWorkerManager, resolvedConfig: expect.objectContaining({ driver: "sandbox", config: expect.objectContaining({ apiKey: "resolved-provider-key", }), }), }), ); expect(JSON.stringify(mockLogActivity.mock.calls[0][1].details)).not.toContain("resolved-provider-key"); }); it("rejects sandbox draft probes for non-admin board users", async () => { mockValidatePluginSandboxProviderConfig.mockResolvedValue({ normalizedConfig: { template: "base", apiKey: "11111111-1111-1111-1111-111111111111", }, pluginId: "plugin-secure", pluginKey: "acme.secure-sandbox-provider", driver: { driverKey: "secure-plugin", kind: "sandbox_provider", displayName: "Secure Sandbox", configSchema: { type: "object", properties: { template: { type: "string" }, apiKey: { type: "string", format: "secret-ref" }, }, }, }, }); mockAccessService.canUser.mockResolvedValue(true); mockAccessService.decide.mockResolvedValue({ allowed: false, explanation: "Missing permission: secrets:read", }); const pluginWorkerManager = {}; const app = createApp({ type: "board", userId: "user-2", source: "session", companyIds: ["company-1"], memberships: [{ companyId: "company-1", status: "active", membershipRole: "member" }], }, { pluginWorkerManager }); const res = await request(app) .post("/api/companies/company-1/environments/probe-config") .send({ name: "Draft Secure Sandbox", driver: "sandbox", config: { provider: "secure-plugin", template: "base", apiKey: "11111111-1111-1111-1111-111111111111", }, }); expect(res.status).toBe(403); expect(res.body.error).toContain("Instance admin access required"); expect(mockSecretService.resolveSecretValueForEphemeralAccess).not.toHaveBeenCalled(); expect(mockProbeEnvironment).not.toHaveBeenCalled(); }); });