import { resolveExternalTestDatabaseUrl } from "./helpers/external-test-database.js"; import { registerCustomImageCleanupWork } from "../services/custom-image-cleanup-work.js"; import { createDeliveryWorkCoordinator } from "../services/delivery-work-coordinator.js"; import { idleWorkSnapshot } from "../services/task-admission.js"; import { randomUUID } from "node:crypto"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { desc, eq, sql } from "drizzle-orm"; import { activityLog, companies, createDb, environmentCustomImageSetupSessions, environmentCustomImageTemplates, environments, plugins, } from "@paperclipai/db"; import type { PluginWorkerManager } from "../services/plugin-worker-manager.js"; import { environmentCustomImageService, } from "../services/environment-custom-images.js"; import { ENVIRONMENT_CUSTOM_IMAGE_CONFIG_FINGERPRINT_EXCLUDED_PATHS, environmentCustomImageTemplateMatchesBaseConfig, fingerprintEnvironmentSandboxProviderConfig, } from "../services/environment-custom-image-runtime.js"; import { resolveEnvironmentDriverConfigForRuntime, } from "../services/environment-config.js"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, } from "./helpers/embedded-postgres.js"; const externalTestDatabase = resolveExternalTestDatabaseUrl("PAPERCLIP_CUSTOM_IMAGE_TEST_DATABASE_URL"); const embeddedPostgresSupport = externalTestDatabase ? { supported: true, reason: undefined } : await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; if (!embeddedPostgresSupport.supported) { console.warn( `Skipping embedded Postgres environment customImage tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`, ); } function pluginManifest() { return { id: "paperclip.fake-sandbox-provider", apiVersion: 1, version: "0.1.0", displayName: "Fake Sandbox Provider", categories: ["automation"], capabilities: ["environment.drivers.register"], entrypoints: { worker: "./dist/worker.js" }, environmentDrivers: [ { driverKey: "fake-plugin", kind: "sandbox_provider", displayName: "Fake Sandbox Provider", supportsInteractiveSetup: true, interactiveSetupConnectionTypes: ["ssh"], supportsTemplateCapture: true, templateRefKind: "snapshot", templateConfigBinding: { field: "customTemplate", unsetFields: ["image"], }, templateIdentityPaths: ["apiUrl"], supportsTemplateDelete: true, configSchema: { type: "object" }, }, ], } as const; } const ALL_CUSTOM_IMAGE_WORKER_METHODS = [ "environmentStartInteractiveSetup", "environmentGetInteractiveSetup", "environmentCancelInteractiveSetup", "environmentCaptureTemplate", "environmentDeleteTemplate", ] as const; function createWorkerManager(options?: { workerMethods?: readonly string[] }) { const supportedMethods = options?.workerMethods ?? ALL_CUSTOM_IMAGE_WORKER_METHODS; const call = vi.fn(async (_pluginId: string, method: string, params: Record) => { if (method === "environmentInspectLease") return { providerLeaseId: params.providerLeaseId, state: "destroyed" }; if (method === "environmentStartInteractiveSetup") { return { providerLeaseId: `lease-${params.sessionId}`, status: "waiting_for_user", connectionSummary: { type: "ssh", username: "sandbox", hostRedacted: true, portRedacted: true, }, connectionPayload: { type: "ssh", command: "ssh sandbox@203.0.113.10", expiresAt: params.expiresAt, }, expiresAt: params.expiresAt, metadata: { connectUrl: "https://203.0.113.10/setup", safeLabel: "setup", }, }; } if (method === "environmentGetInteractiveSetup") { return { providerLeaseId: params.providerLeaseId, status: "waiting_for_user", connectionSummary: { type: "ssh", username: "sandbox", hostRedacted: true, portRedacted: true, }, connectionPayload: { type: "ssh", command: "ssh sandbox@203.0.113.10", }, metadata: { safeLabel: "setup", }, }; } if (method === "environmentCaptureTemplate") { return { templateRef: `snapshot-${String(params.providerLeaseId).slice(-8)}`, templateKind: "snapshot", metadata: { provider: "fake-plugin", sourceTemplateRefRedacted: Boolean(params.sourceTemplateRef), previousTemplateRefRedacted: Boolean(params.previousTemplateRef), }, }; } if (method === "environmentCancelInteractiveSetup") { return { status: params.reason === "timed_out" ? "timed_out" : "cancelled", metadata: { provider: "fake-plugin", found: true }, }; } if (method === "environmentDeleteTemplate") { return { deleted: true, metadata: { provider: "fake-plugin" } }; } throw new Error(`Unexpected plugin call: ${method}`); }); return { call, isRunning: vi.fn(() => true), getWorker: vi.fn(() => ({ supportedMethods: [...supportedMethods] })), } as unknown as PluginWorkerManager & { call: typeof call }; } describeEmbeddedPostgres("environmentCustomImageService", () => { let stopDb: (() => Promise) | null = null; let db!: ReturnType; beforeAll(async () => { if (externalTestDatabase) { db = createDb(externalTestDatabase, { maxConnections: 6, idleTimeoutSeconds: 0, maxLifetimeSeconds: 0 }); return; } const started = await startEmbeddedPostgresTestDatabase("environment-custom-images"); stopDb = started.cleanup; db = createDb(started.connectionString, { maxConnections: 6, idleTimeoutSeconds: 0, maxLifetimeSeconds: 0 }); }); afterEach(async () => { await db.delete(environmentCustomImageSetupSessions); await db.delete(environmentCustomImageTemplates); await db.delete(plugins); await db.delete(environments); await db.delete(companies); }); afterAll(async () => { await db?.$client.end({ timeout: 1 }); await stopDb?.(); }); async function seed() { const companyId = randomUUID(); const environmentId = randomUUID(); await db.insert(companies).values( { id: companyId, name: "Acme", issuePrefix: `A${companyId.slice(0, 4)}` }, ); await db.insert(environments).values({ id: environmentId, name: `Fake ${environmentId.slice(0, 8)}`, driver: "sandbox", status: "active", config: { provider: "fake-plugin", image: "fake:base", reuseLease: false, }, envVars: {}, }); await db.insert(plugins).values({ pluginKey: "paperclip.fake-sandbox-provider", packageName: "paperclip-plugin-fake-sandbox", version: "0.1.0", apiVersion: 1, categories: ["automation"], manifestJson: pluginManifest(), status: "ready", }); return { companyId, environmentId }; } describe("expiry scheduling", () => { let coordinator: ReturnType | undefined; let service: ReturnType; let provider: ReturnType; let draining: boolean; const errors: unknown[] = []; beforeEach(async () => { await Promise.all(Array.from({ length: 6 }, () => db.execute(sql`select pg_sleep(0.01)`))); draining = false; errors.length = 0; provider = createWorkerManager(); service = environmentCustomImageService(db, { pluginWorkerManager: provider }); vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout", "setInterval", "clearInterval"], shouldAdvanceTime: true }); }); afterEach(async () => { await coordinator?.stop(); coordinator = undefined; vi.useRealTimers(); vi.restoreAllMocks(); expect(errors).toEqual([]); }); async function start() { coordinator = createDeliveryWorkCoordinator({ owner: db, canRun: () => !draining, canReconcile: () => true, onError: error => errors.push(error) }); await registerCustomImageCleanupWork(coordinator, service, () => !draining).ready; } async function settle(ms = 60_000) { await vi.advanceTimersByTimeAsync(ms); if (!draining) await vi.waitFor(() => expect(idleWorkSnapshot().active === 0 || coordinator!.nextWakeAt() !== null).toBe(true), { timeout: 5000 }); } async function quiet() { await settle(); expect(coordinator!.nextWakeAt()).toBeNull(); const reads = vi.spyOn(db, "select"), calls = provider.call.mock.calls.length; await settle(30 * 60_000); expect(reads).not.toHaveBeenCalled(); expect(provider.call).toHaveBeenCalledTimes(calls); reads.mockRestore(); } async function create() { const fixture = await seed(); return service.startSetupSession({ environmentId: fixture.environmentId, actor: { userId: "fixture-user" }, ttlSeconds: 3600 }); } const cancellations = () => provider.call.mock.calls.filter(([, method]) => method === "environmentCancelInteractiveSetup"); it("is quiet when empty, wakes from the real producer, expires once, and keeps terminal history quiet", async () => { await start(); await quiet(); const created = await create(); await settle(); expect(coordinator!.nextWakeAt()).toBe(new Date(created.session.expiresAt!).getTime()); const reads = vi.spyOn(db, "select"); await settle(30 * 60_000); expect(reads).not.toHaveBeenCalled(); reads.mockRestore(); expect(cancellations()).toHaveLength(0); await settle(30 * 60_000); expect(cancellations()).toHaveLength(1); expect(await service.getSessionById(created.session.id)).toMatchObject({ status: "timed_out" }); await quiet(); }); it("honors a committed expiry inside the retry window", async () => { await start(); const created = await create(); await settle(); const earlier = new Date(Date.now() + 5_000); provider.call.mockResolvedValueOnce({ providerLeaseId: created.session.providerLeaseId, status: "waiting_for_user", expiresAt: earlier.toISOString() } as never); await service.refreshSetupSession({ sessionId: created.session.id }); await vi.waitFor(() => expect(coordinator!.nextWakeAt()).toBe(earlier.getTime())); expect(cancellations()).toHaveLength(0); await settle(Math.max(0, earlier.getTime() - Date.now())); await vi.waitFor(() => expect(cancellations()).toHaveLength(1)); expect(await service.getSessionById(created.session.id)).toMatchObject({ status: "timed_out" }); await quiet(); }); it("replaces an expiry when the provider moves it earlier and disarms on user cancellation", async () => { await start(); const created = await create(); await settle(); const earlier = new Date(Date.now() + 5 * 60_000); provider.call.mockResolvedValueOnce({ providerLeaseId: created.session.providerLeaseId, status: "waiting_for_user", expiresAt: earlier.toISOString() } as never); await service.refreshSetupSession({ sessionId: created.session.id }); await settle(); expect(coordinator!.nextWakeAt()).toBe(earlier.getTime()); await service.cancelSetupSession({ sessionId: created.session.id }); await quiet(); expect(cancellations()).toHaveLength(1); }); it.each([false, true])("waits for the producer's outer transaction (rollback %s)", async rollback => { const fixture = await seed(); await start(); await quiet(); let release!: () => void, written!: () => void; const gate = new Promise(resolve => { release = resolve; }); const staged = new Promise(resolve => { written = resolve; }); const writing = db.transaction(async tx => { const scoped = environmentCustomImageService(tx as unknown as typeof db, { pluginWorkerManager: provider }); await scoped.startSetupSession({ environmentId: fixture.environmentId, actor: { userId: "fixture-user" }, ttlSeconds: 1 }); written(); await gate; if (rollback) throw new Error("rollback fixture"); }).catch(error => { if (!rollback) throw error; }); await staged; try { await vi.advanceTimersByTimeAsync(60_000); expect(cancellations()).toHaveLength(0); } finally { release(); } await writing; await settle(); expect(cancellations()).toHaveLength(rollback ? 0 : 1); await quiet(); }); it("retains a producer commit that arrives during the deadline read", async () => { await start(); const created = await create(); await settle(); await coordinator!.stop(); let release!: () => void, reading!: () => void; const gate = new Promise(resolve => { release = resolve; }); const entered = new Promise(resolve => { reading = resolve; }); const readDeadline = service.nextSetupCleanupAt; vi.spyOn(service, "nextSetupCleanupAt").mockImplementationOnce(async () => { const stale = await readDeadline(); reading(); await gate; return stale; }); const starting = start(); await entered; const earlier = new Date(Date.now() + 5 * 60_000); try { provider.call.mockResolvedValueOnce({ providerLeaseId: created.session.providerLeaseId, status: "waiting_for_user", expiresAt: earlier.toISOString() } as never); await service.refreshSetupSession({ sessionId: created.session.id }); } finally { release(); } await starting; await settle(); await vi.waitFor(() => expect(coordinator!.nextWakeAt()).toBe(earlier.getTime())); await settle(5 * 60_000); await vi.waitFor(() => expect(cancellations()).toHaveLength(1)); await quiet(); }); it("reconstructs a saved expiry after restart", async () => { await start(); const created = await create(); await settle(); await coordinator!.stop(); await start(); expect(coordinator!.nextWakeAt()).toBe(new Date(created.session.expiresAt!).getTime()); await settle(60 * 60_000); expect(cancellations()).toHaveLength(1); await quiet(); }); it.each(["startup", "first_write", "failed_pass"])("retains %s work through drain without passive SQL", async mode => { if (mode === "startup") draining = true; await start(); if (mode === "first_write") { await quiet(); draining = true; } await create(); if (mode === "failed_pass") { vi.spyOn(service, "nextSetupCleanupAt").mockRejectedValueOnce(new Error("database unavailable")); await settle(30_000); await vi.waitFor(() => expect(errors).toHaveLength(1)); errors.length = 0; draining = true; } await settle(); const reads = vi.spyOn(db, "select"); await settle(2 * 60 * 60_000); expect(reads).not.toHaveBeenCalled(); reads.mockRestore(); expect(cancellations()).toHaveLength(0); draining = false; await settle(); expect(cancellations()).toHaveLength(1); await quiet(); }); }); it.each(["supported", "old_worker", "changed_config"])("inspects historical setup read-only with exact config and capability gates: %s", async mode => { const fixture = await seed(); const worker = createWorkerManager({ workerMethods: [...ALL_CUSTOM_IMAGE_WORKER_METHODS, ...(mode === "old_worker" ? [] : ["environmentInspectLease"])] }); const service = environmentCustomImageService(db, { pluginWorkerManager: worker }); const created = await service.startSetupSession({ environmentId: fixture.environmentId, actor: { userId: "fixture-user" }, now: new Date("2099-01-01T00:00:00Z") }); await service.cancelSetupSession({ sessionId: created.session.id }); const [session] = await db.select().from(environmentCustomImageSetupSessions).where(eq(environmentCustomImageSetupSessions.id, created.session.id)); expect(session!.metadata).toMatchObject({ setupRpcCompanyId: created.session.metadata!.setupRpcCompanyId, sourceEnvironmentConfigFingerprint: created.session.metadata!.sourceEnvironmentConfigFingerprint }); if (mode === "changed_config") await db.update(environments).set({ config: { provider: "fake-plugin", image: "replacement:base" } }).where(eq(environments.id, fixture.environmentId)); worker.call.mockClear(); const observed = await service.inspectSetupResource(session!); if (mode === "supported") { expect(observed).toEqual({ providerLeaseId: session!.providerLeaseId, state: "destroyed" }); expect(worker.call).toHaveBeenCalledExactlyOnceWith(expect.any(String), "environmentInspectLease", expect.objectContaining({ companyId: session!.metadata!.setupRpcCompanyId, providerLeaseId: session!.providerLeaseId, }), 15_000); } else { expect(observed).toBeUndefined(); expect(worker.call).not.toHaveBeenCalled(); } }); it("starts, refreshes, finishes, refreshes again, and rolls back setup sessions", async () => { const { environmentId } = await seed(); const workerManager = createWorkerManager(); const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" }, ttlSeconds: 600, }); expect(started.session.status).toBe("waiting_for_user"); expect(started.connectionPayload?.command).toContain("203.0.113.10"); expect(JSON.stringify(started.session.metadata)).not.toContain("203.0.113.10"); const status = await service.refreshSetupSession({ sessionId: started.session.id, includeConnectionPayload: true, }); expect(status.connectionPayload?.command).toContain("ssh sandbox@203.0.113.10"); const promoted = await service.finishSetupSession({ sessionId: started.session.id }); expect(promoted.session.status).toBe("promoted"); expect(promoted.template.status).toBe("active"); expect(promoted.template.templateKind).toBe("snapshot"); expect(promoted.template.metadata).toMatchObject({ runtimeConfigBinding: { field: "customTemplate", unsetFields: ["image"], }, }); const refresh = await service.startSetupSession({ environmentId, actor: { userId: "user-1" }, }); const replacement = await service.finishSetupSession({ sessionId: refresh.session.id }); expect(replacement.template.id).not.toBe(promoted.template.id); const rollback = await service.rollbackTemplate({ environmentId }); expect(rollback.activeTemplate.id).toBe(promoted.template.id); expect(rollback.supersededTemplate.id).toBe(replacement.template.id); }); it("reuses the setup provider company context across lifecycle calls", async () => { const { companyId, environmentId } = await seed(); const workerManager = createWorkerManager(); const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" }, secretContextCompanyId: companyId, }); expect(started.session.metadata).toMatchObject({ setupRpcCompanyId: companyId, }); await service.refreshSetupSession({ sessionId: started.session.id, includeConnectionPayload: true, }); const promoted = await service.finishSetupSession({ sessionId: started.session.id }); const lifecycleCalls = workerManager.call.mock.calls .filter(([, method]) => [ "environmentStartInteractiveSetup", "environmentGetInteractiveSetup", "environmentCaptureTemplate", "environmentCancelInteractiveSetup", ].includes(method)) .map(([, method, params]) => ({ method, companyId: (params as Record).companyId, })); expect(lifecycleCalls).toEqual([ { method: "environmentStartInteractiveSetup", companyId }, { method: "environmentGetInteractiveSetup", companyId }, { method: "environmentCaptureTemplate", companyId }, { method: "environmentCancelInteractiveSetup", companyId }, ]); expect(promoted.session.metadata).toMatchObject({ setupRpcCompanyId: companyId, }); expect(promoted.template.metadata).toMatchObject({ setupRpcCompanyId: companyId, }); }); it("revokes the active template before deleting the provider template", async () => { const { environmentId } = await seed(); const workerManager = createWorkerManager(); const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" }, }); const promoted = await service.finishSetupSession({ sessionId: started.session.id }); let statusAtProviderDelete: string | null = null; workerManager.call.mockImplementation(async (_pluginId, method) => { if (method !== "environmentDeleteTemplate") { throw new Error(`Unexpected plugin call after setup: ${method}`); } const [row] = await db .select({ status: environmentCustomImageTemplates.status }) .from(environmentCustomImageTemplates) .where(eq(environmentCustomImageTemplates.id, promoted.template.id)); statusAtProviderDelete = row?.status ?? null; return { deleted: true, metadata: { provider: "fake-plugin" } }; }); const disabled = await service.disableTemplate({ environmentId, deleteProviderTemplate: true, }); expect(disabled.status).toBe("revoked"); expect(statusAtProviderDelete).toBe("revoked"); expect(workerManager.call).toHaveBeenLastCalledWith( expect.any(String), "environmentDeleteTemplate", expect.objectContaining({ templateRef: promoted.template.templateRef, reason: "disabled", }), undefined, ); }); it("does not send provider template refs to a different current provider", async () => { const { environmentId } = await seed(); const workerManager = createWorkerManager(); const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" }, }); const promoted = await service.finishSetupSession({ sessionId: started.session.id }); const deleteCallsBefore = workerManager.call.mock.calls .filter(([, method]) => method === "environmentDeleteTemplate") .length; await db.update(environments) .set({ config: { provider: "other-plugin", image: "other:base", reuseLease: false, }, }) .where(eq(environments.id, environmentId)); await expect(service.disableTemplate({ environmentId, deleteProviderTemplate: true, })).rejects.toThrow("Environment customImage provider changed"); const deleteCallsAfter = workerManager.call.mock.calls .filter(([, method]) => method === "environmentDeleteTemplate") .length; const [templateRow] = await db .select({ status: environmentCustomImageTemplates.status }) .from(environmentCustomImageTemplates) .where(eq(environmentCustomImageTemplates.id, promoted.template.id)); expect(deleteCallsAfter).toBe(deleteCallsBefore); expect(templateRow?.status).toBe("active"); }); it("cancels and times out setup sessions without changing the active template", async () => { const { environmentId } = await seed(); const workerManager = createWorkerManager(); const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" }, }); const cancelled = await service.cancelSetupSession({ sessionId: started.session.id, reason: "user_cancelled", }); expect(cancelled.status).toBe("cancelled"); expect(cancelled.metadata).toMatchObject({ setupRpcCompanyId: started.session.metadata!.setupRpcCompanyId, sourceEnvironmentConfigFingerprint: started.session.metadata!.sourceEnvironmentConfigFingerprint }); expect(await service.getActiveTemplate({ environmentId })).toBeNull(); const expired = await service.startSetupSession({ environmentId, actor: { userId: "user-1" }, ttlSeconds: 60, now: new Date("2026-06-25T00:00:00.000Z"), }); const cleanup = await service.cleanupExpiredSetupSessions({ now: new Date("2026-06-25T00:02:00.000Z"), }); expect(cleanup).toMatchObject({ scanned: 1, timedOut: 1, failed: 0 }); const timedOut = await service.getSessionById(expired.session.id); expect(timedOut?.status).toBe("timed_out"); expect(timedOut?.metadata).toMatchObject({ setupRpcCompanyId: expired.session.metadata!.setupRpcCompanyId, sourceEnvironmentConfigFingerprint: expired.session.metadata!.sourceEnvironmentConfigFingerprint }); }); it("rejects interactive setup when the provider declares it but the worker omits a setup method", async () => { const { environmentId } = await seed(); // The manifest declares supportsInteractiveSetup, but the worker reports // none of the three setup methods. The gate must name the first missing one. const workerManager = createWorkerManager({ workerMethods: ["environmentCaptureTemplate", "environmentDeleteTemplate"], }); const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager }); await expect(service.startSetupSession({ environmentId, actor: { userId: "user-1" }, })).rejects.toThrow('does not report the "environmentStartInteractiveSetup" method'); expect(workerManager.call).not.toHaveBeenCalled(); }); it("rejects template capture when the provider declares it but the worker omits the capture method", async () => { const { environmentId } = await seed(); // The worker reports every setup method, so the session starts and // finishes the interactive part. It omits environmentCaptureTemplate, so // finishing the session must fail at the capture gate. const workerManager = createWorkerManager({ workerMethods: [ "environmentStartInteractiveSetup", "environmentGetInteractiveSetup", "environmentCancelInteractiveSetup", "environmentDeleteTemplate", ], }); const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" }, }); await expect(service.finishSetupSession({ sessionId: started.session.id })) .rejects.toThrow('does not report the "environmentCaptureTemplate" method'); }); it("rejects template deletion when the provider declares it but the worker omits the delete method", async () => { const { environmentId } = await seed(); // The worker reports every setup and capture method but omits // environmentDeleteTemplate, so a delete-on-disable request must fail at // the delete gate after the template is already captured. const workerManager = createWorkerManager({ workerMethods: [ "environmentStartInteractiveSetup", "environmentGetInteractiveSetup", "environmentCancelInteractiveSetup", "environmentCaptureTemplate", ], }); const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" }, }); await service.finishSetupSession({ sessionId: started.session.id }); await expect(service.disableTemplate({ environmentId, deleteProviderTemplate: true, })).rejects.toThrow('does not report the "environmentDeleteTemplate" method'); }); it("passes every gate when the provider declares each flag and the worker reports every matching method", async () => { const { environmentId } = await seed(); // The default worker manager reports all five methods, matching every // flag the manifest declares. Every gate must pass. const workerManager = createWorkerManager(); const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" }, }); const promoted = await service.finishSetupSession({ sessionId: started.session.id }); expect(promoted.template.status).toBe("active"); const disabled = await service.disableTemplate({ environmentId, deleteProviderTemplate: true, }); expect(disabled.status).toBe("revoked"); expect(workerManager.call).toHaveBeenCalledWith( expect.any(String), "environmentDeleteTemplate", expect.any(Object), undefined, ); }); it("rejects templates from another environment", async () => { const { environmentId } = await seed(); const otherEnvironmentId = randomUUID(); await db.insert(environments).values({ id: otherEnvironmentId, name: `Other ${otherEnvironmentId.slice(0, 8)}`, driver: "sandbox", status: "active", config: { provider: "fake-plugin", image: "fake:base", reuseLease: false, }, envVars: {}, }); const workerManager = createWorkerManager(); const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager }); const [otherTemplate] = await db.insert(environmentCustomImageTemplates).values({ environmentId: otherEnvironmentId, provider: "fake-plugin", templateKind: "snapshot", templateRef: "snapshot-other-environment", status: "active", }).returning(); await expect(service.startSetupSession({ environmentId, templateId: otherTemplate!.id, actor: { userId: "user-1" }, })).rejects.toThrow("Setup template must be the active template"); }); it("applies the active template regardless of base-config changes and falls back when none exists", async () => { const { companyId, environmentId } = await seed(); const environment = await db.select().from(environments).where(eq(environments.id, environmentId)).then((rows) => rows[0]!); const fallback = await resolveEnvironmentDriverConfigForRuntime(db, companyId, { id: environment.id, driver: "sandbox", config: environment.config, }, { heartbeatRunId: randomUUID() }); expect(fallback.driver).toBe("sandbox"); expect(fallback.config).toMatchObject({ image: "fake:base" }); await db.insert(environmentCustomImageTemplates).values({ environmentId, provider: "fake-plugin", templateKind: "snapshot", templateRef: "snapshot-active", sourceEnvironmentConfigFingerprint: fingerprintEnvironmentSandboxProviderConfig(environment.config as any, { excludePaths: ENVIRONMENT_CUSTOM_IMAGE_CONFIG_FINGERPRINT_EXCLUDED_PATHS, }), status: "active", }); const resolved = await resolveEnvironmentDriverConfigForRuntime(db, companyId, { id: environment.id, driver: "sandbox", config: environment.config, }, { heartbeatRunId: randomUUID() }); expect(resolved.driver).toBe("sandbox"); expect(resolved.config).toMatchObject({ snapshot: "snapshot-active" }); expect(resolved.config).not.toHaveProperty("image"); // Runtime-only resource/lease edits must NOT silently discard the captured // template. await db.update(environments) .set({ config: { ...(environment.config as Record), cpu: 4, timeoutMs: 600000, reuseLease: true, }, }) .where(eq(environments.id, environment.id)); const afterResourceChangeEnvironment = await db.select().from(environments).where(eq(environments.id, environmentId)).then((rows) => rows[0]!); const afterResourceChange = await resolveEnvironmentDriverConfigForRuntime(db, companyId, { id: afterResourceChangeEnvironment.id, driver: "sandbox", config: afterResourceChangeEnvironment.config, }, { heartbeatRunId: randomUUID() }); expect(afterResourceChange.driver).toBe("sandbox"); expect(afterResourceChange.config).toMatchObject({ snapshot: "snapshot-active" }); expect(afterResourceChange.config).not.toHaveProperty("image"); // Changing the base image is a meaningful source-template change. In that // case, the old capture must not mask the newly saved image. await db.update(environmentCustomImageTemplates) .set({ sourceEnvironmentConfigFingerprint: fingerprintEnvironmentSandboxProviderConfig(environment.config as any, { excludePaths: ENVIRONMENT_CUSTOM_IMAGE_CONFIG_FINGERPRINT_EXCLUDED_PATHS, }), }) .where(eq(environmentCustomImageTemplates.templateRef, "snapshot-active")); await db.update(environments) .set({ config: { ...(environment.config as Record), image: "fake:new-base", }, }) .where(eq(environments.id, environment.id)); const afterImageChangeEnvironment = await db.select().from(environments).where(eq(environments.id, environmentId)).then((rows) => rows[0]!); const afterImageChange = await resolveEnvironmentDriverConfigForRuntime(db, companyId, { id: afterImageChangeEnvironment.id, driver: "sandbox", config: afterImageChangeEnvironment.config, }, { heartbeatRunId: randomUUID() }); expect(afterImageChange.driver).toBe("sandbox"); expect(afterImageChange.config).toMatchObject({ image: "fake:new-base" }); expect(afterImageChange.config).not.toHaveProperty("snapshot"); }); it("applies the active template for ad-hoc Test probes only when applyCustomImageTemplate is set", async () => { const { companyId, environmentId } = await seed(); const environment = await db.select().from(environments).where(eq(environments.id, environmentId)).then((rows) => rows[0]!); await db.insert(environmentCustomImageTemplates).values({ environmentId, provider: "fake-plugin", templateKind: "snapshot", templateRef: "snapshot-active", status: "active", }); // No issueId/heartbeatRunId and no opt-in: an operator Test probe would // otherwise silently boot the base image instead of the captured template. const withoutOptIn = await resolveEnvironmentDriverConfigForRuntime(db, companyId, { id: environment.id, driver: "sandbox", config: environment.config, }); expect(withoutOptIn.config).toMatchObject({ image: "fake:base" }); expect(withoutOptIn.config).not.toHaveProperty("snapshot"); // The Test route opts in explicitly so the probe uses the captured image. const withOptIn = await resolveEnvironmentDriverConfigForRuntime(db, companyId, { id: environment.id, driver: "sandbox", config: environment.config, }, { applyCustomImageTemplate: true }); expect(withOptIn.config).toMatchObject({ snapshot: "snapshot-active" }); expect(withOptIn.config).not.toHaveProperty("image"); }); it("applies provider-declared runtime config bindings for captured templates", async () => { const { companyId, environmentId } = await seed(); const workerManager = createWorkerManager(); const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" }, }); const promoted = await service.finishSetupSession({ sessionId: started.session.id }); const environment = await db.select().from(environments).where(eq(environments.id, environmentId)).then((rows) => rows[0]!); const resolved = await resolveEnvironmentDriverConfigForRuntime(db, companyId, { id: environment.id, driver: "sandbox", config: environment.config, }, { heartbeatRunId: randomUUID() }); expect(promoted.template.metadata).toMatchObject({ runtimeConfigBinding: { field: "customTemplate", unsetFields: ["image"], }, }); expect(resolved.driver).toBe("sandbox"); expect(resolved.config).toMatchObject({ customTemplate: promoted.template.templateRef }); expect(resolved.config).not.toHaveProperty("image"); expect(resolved.config).not.toHaveProperty("snapshot"); }); }); describe("fingerprintEnvironmentSandboxProviderConfig", () => { it("ignores excluded secret-ref paths so secretizing a credential on save keeps the fingerprint stable", () => { // Mirrors the real flow: a template is captured before "Save Environment" // rewrites the raw apiKey into a secret ref. Excluding the secret path must // keep the capture-time and run-time fingerprints equal. const rawCredential = { provider: "daytona", image: "daytonaio/sandbox:0.8.0", apiKey: "raw-api-key-value", } as any; const secretRefCredential = { provider: "daytona", image: "daytonaio/sandbox:0.8.0", apiKey: "0d9a7b0e-a3ba-4605-8a68-eb230d494e98", } as any; const exclude = { excludePaths: ["apiKey"] }; expect(fingerprintEnvironmentSandboxProviderConfig(rawCredential, exclude)).toBe( fingerprintEnvironmentSandboxProviderConfig(secretRefCredential, exclude), ); // Without exclusion the credential change would (incorrectly) invalidate the template. expect(fingerprintEnvironmentSandboxProviderConfig(rawCredential)).not.toBe( fingerprintEnvironmentSandboxProviderConfig(secretRefCredential), ); // A meaningful base change (e.g. switching the base image) still changes the fingerprint. expect(fingerprintEnvironmentSandboxProviderConfig(rawCredential, exclude)).not.toBe( fingerprintEnvironmentSandboxProviderConfig( { ...rawCredential, image: "daytonaio/sandbox:0.9.0" } as any, exclude, ), ); }); }); describe("environmentCustomImageTemplateMatchesBaseConfig", () => { it("keeps captures across runtime-only edits but not base image changes", () => { const baseConfig = { provider: "daytona", image: "daytonaio/sandbox:0.8.0", timeoutMs: 300000, reuseLease: false, } as any; const template = { id: "template-1", environmentId: "env-1", provider: "daytona", templateKind: "snapshot", templateRef: "snapshot-active", sourceTemplateRef: "daytonaio/sandbox:0.8.0", sourceEnvironmentConfigFingerprint: fingerprintEnvironmentSandboxProviderConfig(baseConfig, { excludePaths: ENVIRONMENT_CUSTOM_IMAGE_CONFIG_FINGERPRINT_EXCLUDED_PATHS, }), status: "active", createdByUserId: null, createdByAgentId: null, capturedAt: null, lastUsedAt: null, supersededByTemplateId: null, metadata: null, createdAt: new Date("2026-07-09T00:00:00.000Z"), updatedAt: new Date("2026-07-09T00:00:00.000Z"), } as const; expect(environmentCustomImageTemplateMatchesBaseConfig({ template, baseConfig: { ...baseConfig, timeoutMs: 600000, reuseLease: true, cpu: 4, }, })).toBe(true); expect(environmentCustomImageTemplateMatchesBaseConfig({ template, baseConfig: { ...baseConfig, image: "daytonaio/sandbox:0.9.0", }, })).toBe(false); }); it("matches configs carrying a secret-ref credential when the capture excluded that path", () => { // Capture-time fingerprints exclude the provider's secret-ref paths (e.g. // daytona apiKey). The runtime re-check must exclude the same paths or a // config with any credential never matches and the template is dropped. const baseConfig = { provider: "daytona", image: "daytonaio/sandbox:0.8.0", apiKey: "raw-api-key-value", } as any; const template = { id: "template-1", environmentId: "env-1", provider: "daytona", templateKind: "snapshot", templateRef: "snapshot-active", sourceTemplateRef: "daytonaio/sandbox:0.8.0", sourceEnvironmentConfigFingerprint: fingerprintEnvironmentSandboxProviderConfig(baseConfig, { excludePaths: [ ...ENVIRONMENT_CUSTOM_IMAGE_CONFIG_FINGERPRINT_EXCLUDED_PATHS, "apiKey", ], }), status: "active", createdByUserId: null, createdByAgentId: null, capturedAt: null, lastUsedAt: null, supersededByTemplateId: null, metadata: null, createdAt: new Date("2026-07-09T00:00:00.000Z"), updatedAt: new Date("2026-07-09T00:00:00.000Z"), } as const; expect(environmentCustomImageTemplateMatchesBaseConfig({ template, baseConfig, secretRefExcludePaths: ["apiKey"], })).toBe(true); // A rotated credential still matches — credentials are not part of the // captured image identity. expect(environmentCustomImageTemplateMatchesBaseConfig({ template, baseConfig: { ...baseConfig, apiKey: "rotated-key" }, secretRefExcludePaths: ["apiKey"], })).toBe(true); // Without the exclusion the same config fails to match (the pre-fix bug). expect(environmentCustomImageTemplateMatchesBaseConfig({ template, baseConfig, })).toBe(false); }); }); describeEmbeddedPostgres("environmentCustomImageService reconciliation", () => { let stopDb: (() => Promise) | null = null; let db!: ReturnType; beforeAll(async () => { if (externalTestDatabase) { db = createDb(externalTestDatabase, { maxConnections: 6, idleTimeoutSeconds: 0, maxLifetimeSeconds: 0 }); return; } const started = await startEmbeddedPostgresTestDatabase("environment-custom-images-reconcile"); stopDb = started.cleanup; db = createDb(started.connectionString, { maxConnections: 6, idleTimeoutSeconds: 0, maxLifetimeSeconds: 0 }); }); afterEach(async () => { await db.delete(environmentCustomImageSetupSessions); await db.delete(environmentCustomImageTemplates); await db.delete(plugins); await db.delete(environments); await db.delete(companies); }); afterAll(async () => { await db?.$client.end({ timeout: 1 }); await stopDb?.(); }); async function seed() { const companyId = randomUUID(); const environmentId = randomUUID(); await db.insert(companies).values( { id: companyId, name: "Acme", issuePrefix: `A${companyId.slice(0, 4)}` }, ); await db.insert(environments).values({ id: environmentId, name: `Fake ${environmentId.slice(0, 8)}`, driver: "sandbox", status: "active", config: { provider: "fake-plugin", image: "fake:base", reuseLease: false, }, envVars: {}, }); await db.insert(plugins).values({ pluginKey: "paperclip.fake-sandbox-provider", packageName: "paperclip-plugin-fake-sandbox", version: "0.1.0", apiVersion: 1, categories: ["automation"], manifestJson: pluginManifest(), status: "ready", }); return { companyId, environmentId }; } it("re-links the active template on save when only non-identity fields change", async () => { const { companyId, environmentId } = await seed(); const workerManager = createWorkerManager(); const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" } }); const promoted = await service.finishSetupSession({ sessionId: started.session.id }); const baseConfig = { provider: "fake-plugin", image: "fake:base", reuseLease: false }; const nextConfig = { ...baseConfig, region: "eu-west" }; const relinked = await service.reconcileActiveTemplateForConfigChange({ environmentId, previous: { driver: "sandbox", config: baseConfig }, next: { driver: "sandbox", config: nextConfig }, }); expect(relinked.action).toBe("relinked"); if (relinked.action !== "relinked") throw new Error("expected relink"); expect(relinked.template.sourceEnvironmentConfigFingerprint) .not.toBe(promoted.template.sourceEnvironmentConfigFingerprint); // Re-running with the same configs is a no-op: the template already // matches the new config. const repeat = await service.reconcileActiveTemplateForConfigChange({ environmentId, previous: { driver: "sandbox", config: baseConfig }, next: { driver: "sandbox", config: nextConfig }, }); expect(repeat.action).toBe("none"); // The captured template keeps applying at runtime under the new config. await db.update(environments) .set({ config: nextConfig }) .where(eq(environments.id, environmentId)); const resolved = await resolveEnvironmentDriverConfigForRuntime(db, companyId, { id: environmentId, driver: "sandbox", config: nextConfig, }, { heartbeatRunId: randomUUID() }); expect(resolved.driver).toBe("sandbox"); expect(resolved.config).toMatchObject({ customTemplate: promoted.template.templateRef }); expect(resolved.config).not.toHaveProperty("image"); }); it("reports detached on save when a boot-source or provider identity field changes", async () => { const { environmentId } = await seed(); const workerManager = createWorkerManager(); const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" } }); const promoted = await service.finishSetupSession({ sessionId: started.session.id }); const baseConfig = { provider: "fake-plugin", image: "fake:base", reuseLease: false }; // Base image change: the user asked for a different base, so the capture // cannot be re-linked. const imageChange = await service.reconcileActiveTemplateForConfigChange({ environmentId, previous: { driver: "sandbox", config: baseConfig }, next: { driver: "sandbox", config: { ...baseConfig, image: "fake:other" } }, }); expect(imageChange.action).toBe("detached"); // Provider-declared identity path change (apiUrl): captured templates do // not exist on a different provider endpoint. const endpointChange = await service.reconcileActiveTemplateForConfigChange({ environmentId, previous: { driver: "sandbox", config: baseConfig }, next: { driver: "sandbox", config: { ...baseConfig, apiUrl: "https://other.example" } }, }); expect(endpointChange.action).toBe("detached"); // Binding-field change counts as a boot-source change too. const bindingChange = await service.reconcileActiveTemplateForConfigChange({ environmentId, previous: { driver: "sandbox", config: baseConfig }, next: { driver: "sandbox", config: { ...baseConfig, customTemplate: "someone-elses-snapshot" } }, }); expect(bindingChange.action).toBe("detached"); // Detach never mutates the stored fingerprint; rollback/disable stay // available and the old config still matches. const template = await service.getActiveTemplate({ environmentId, provider: "fake-plugin" }); expect(template?.sourceEnvironmentConfigFingerprint) .toBe(promoted.template.sourceEnvironmentConfigFingerprint); // A template that was already detached before the save is left alone. const alreadyDetached = await service.reconcileActiveTemplateForConfigChange({ environmentId, previous: { driver: "sandbox", config: { ...baseConfig, image: "fake:unrelated" } }, next: { driver: "sandbox", config: { ...baseConfig, image: "fake:unrelated", region: "eu" } }, }); expect(alreadyDetached.action).toBe("none"); }); it("reports whether the active template matches the saved config in the overview", async () => { const { environmentId } = await seed(); const workerManager = createWorkerManager(); const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" } }); await service.finishSetupSession({ sessionId: started.session.id }); const inSync = await service.getOverview({ environmentId }); expect(inSync.activeTemplateMatchesConfig).toBe(true); await db.update(environments) .set({ config: { provider: "fake-plugin", image: "fake:other", reuseLease: false } }) .where(eq(environments.id, environmentId)); const outOfSync = await service.getOverview({ environmentId }); expect(outOfSync.activeTemplate).not.toBeNull(); expect(outOfSync.activeTemplateMatchesConfig).toBe(false); // A boot-source field changed, so the overview attributes the drift and // names the field with its `from`/`to` values. expect(outOfSync.activeTemplateDrift?.classification).toBe("boot_source_drift"); expect(outOfSync.activeTemplateDrift?.driftedPaths).toEqual( expect.arrayContaining([{ path: "image", from: "fake:base", to: "fake:other" }]), ); }); it("attributes a knob-only overview change without naming a boot-source field", async () => { const { environmentId } = await seed(); const workerManager = createWorkerManager(); const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" } }); await service.finishSetupSession({ sessionId: started.session.id }); // A non-boot-relevant field changes. The fingerprint no longer matches, but // every boot-source value still matches, so the drift is knob-only. await db.update(environments) .set({ config: { provider: "fake-plugin", image: "fake:base", reuseLease: false, region: "eu" } }) .where(eq(environments.id, environmentId)); const overview = await service.getOverview({ environmentId }); expect(overview.activeTemplateMatchesConfig).toBe(false); expect(overview.activeTemplateDrift?.classification).toBe("knob_only"); expect(overview.activeTemplateDrift?.driftedPaths).toEqual([]); }); it("attributes an unclassified overview drift for a legacy template with no snapshot", async () => { const { environmentId } = await seed(); const service = environmentCustomImageService(db, { pluginWorkerManager: createWorkerManager() }); // A legacy template carries no boot-relevant snapshot. The overview must // fail closed and give no false "safe to relink" signal. await db.insert(environmentCustomImageTemplates).values({ environmentId, provider: "fake-plugin", templateKind: "snapshot", templateRef: "snapshot-legacy", sourceEnvironmentConfigFingerprint: "stale-fingerprint", status: "active", metadata: { runtimeConfigBinding: { field: "customTemplate", unsetFields: ["image"] } }, }); const overview = await service.getOverview({ environmentId }); expect(overview.activeTemplate).not.toBeNull(); expect(overview.activeTemplateDrift?.classification).toBe("unclassified"); expect(overview.activeTemplateDrift?.driftedPaths).toEqual([]); }); }); function secretPluginManifest() { return { id: "paperclip.fake-secret-sandbox-provider", apiVersion: 1, version: "0.1.0", displayName: "Fake Secret Sandbox Provider", categories: ["automation"], capabilities: ["environment.drivers.register"], entrypoints: { worker: "./dist/worker.js" }, environmentDrivers: [ { driverKey: "fake-secret-plugin", kind: "sandbox_provider", displayName: "Fake Secret Sandbox Provider", supportsInteractiveSetup: true, interactiveSetupConnectionTypes: ["ssh"], supportsTemplateCapture: true, templateRefKind: "snapshot", templateConfigBinding: { field: "customTemplate", unsetFields: ["image"] }, // apiUrl overlaps a secret exactly; auth.token is a child of secret // `auth`; credentials is a parent of secret `credentials.secret`. templateIdentityPaths: ["apiUrl", "auth.token", "credentials"], supportsTemplateDelete: true, configSchema: { type: "object", properties: { apiUrl: { type: "string", format: "secret-ref" }, auth: { type: "string", format: "secret-ref" }, credentials: { type: "object", properties: { secret: { type: "string", format: "secret-ref" } }, }, }, }, }, ], } as const; } function invalidIdentityPluginManifest() { return { id: "paperclip.fake-invalid-sandbox-provider", apiVersion: 1, version: "0.1.0", displayName: "Fake Invalid Identity Sandbox Provider", categories: ["automation"], capabilities: ["environment.drivers.register"], entrypoints: { worker: "./dist/worker.js" }, environmentDrivers: [ { driverKey: "fake-invalid-plugin", kind: "sandbox_provider", displayName: "Fake Invalid Identity Sandbox Provider", supportsInteractiveSetup: true, interactiveSetupConnectionTypes: ["ssh"], supportsTemplateCapture: true, templateRefKind: "snapshot", templateConfigBinding: { field: "customTemplate", unsetFields: ["image"] }, // A valid path plus two that must fail canonicalization. templateIdentityPaths: ["apiUrl", "bad path!", "a..b"], supportsTemplateDelete: true, configSchema: { type: "object" }, }, ], } as const; } function prototypeKeyIdentityPluginManifest() { return { id: "paperclip.fake-prototype-key-sandbox-provider", apiVersion: 1, version: "0.1.0", displayName: "Fake Prototype Key Sandbox Provider", categories: ["automation"], capabilities: ["environment.drivers.register"], entrypoints: { worker: "./dist/worker.js" }, environmentDrivers: [ { driverKey: "fake-prototype-key-plugin", kind: "sandbox_provider", displayName: "Fake Prototype Key Sandbox Provider", supportsInteractiveSetup: true, interactiveSetupConnectionTypes: ["ssh"], supportsTemplateCapture: true, templateRefKind: "snapshot", templateConfigBinding: { field: "customTemplate", unsetFields: ["image"] }, // Every prototype key has an identifier shape but names no own field. // Each must fail canonicalization and record only the marker. templateIdentityPaths: ["__proto__", "constructor", "prototype", "nested.__proto__"], supportsTemplateDelete: true, configSchema: { type: "object" }, }, ], } as const; } describeEmbeddedPostgres("environmentCustomImageService relink", () => { let stopDb: (() => Promise) | null = null; let db!: ReturnType; beforeAll(async () => { if (externalTestDatabase) { db = createDb(externalTestDatabase, { maxConnections: 6, idleTimeoutSeconds: 0, maxLifetimeSeconds: 0 }); return; } const started = await startEmbeddedPostgresTestDatabase("environment-custom-images-relink"); stopDb = started.cleanup; db = createDb(started.connectionString, { maxConnections: 6, idleTimeoutSeconds: 0, maxLifetimeSeconds: 0 }); }); afterEach(async () => { await db.delete(activityLog); await db.delete(environmentCustomImageSetupSessions); await db.delete(environmentCustomImageTemplates); await db.delete(plugins); await db.delete(environments); await db.delete(companies); }); afterAll(async () => { await db?.$client.end({ timeout: 1 }); await stopDb?.(); }); async function seed(opts?: { manifest?: ReturnType | ReturnType | ReturnType | ReturnType; config?: Record; }) { const manifest = opts?.manifest ?? pluginManifest(); const provider = manifest.environmentDrivers[0]!.driverKey; const companyId = randomUUID(); const environmentId = randomUUID(); await db.insert(companies).values( { id: companyId, name: "Acme", issuePrefix: `A${companyId.slice(0, 4)}` }, ); await db.insert(environments).values({ id: environmentId, name: `Fake ${environmentId.slice(0, 8)}`, driver: "sandbox", status: "active", config: opts?.config ?? { provider, image: "fake:base", reuseLease: false }, envVars: {}, }); await db.insert(plugins).values({ pluginKey: manifest.id, packageName: `paperclip-plugin-${provider}`, version: "0.1.0", apiVersion: 1, categories: ["automation"], manifestJson: manifest, status: "ready", }); return { companyId, environmentId, provider }; } const relinkActor = { actorType: "user" as const, actorId: "user-1", agentId: null, runId: null, agentApiKeyId: null, }; async function activeTemplateRow(environmentId: string) { return db .select() .from(environmentCustomImageTemplates) .where(eq(environmentCustomImageTemplates.environmentId, environmentId)) .orderBy(desc(environmentCustomImageTemplates.createdAt)) .then((rows) => rows[0]!); } async function relinkActivityRows(environmentId: string) { return db .select() .from(activityLog) .where(eq(activityLog.entityId, environmentId)); } it("keeps secret-ref values out of metadata, the response, and activity, and relinks with the flag", async () => { const config = { provider: "fake-secret-plugin", image: "fake:base", apiUrl: "https://secret-endpoint.example", auth: "auth-secret-value", credentials: { secret: "cred-secret-value", region: "eu" }, reuseLease: false, }; const { companyId, environmentId } = await seed({ manifest: secretPluginManifest(), config }); const service = environmentCustomImageService(db, { pluginWorkerManager: createWorkerManager() }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" } }); const promoted = await service.finishSetupSession({ sessionId: started.session.id }); // The public template response never carries the server-internal snapshot. expect((promoted.template.metadata as Record).bootRelevantConfig).toBeUndefined(); // The persisted snapshot lives in the row. Every secret-ref overlap is // excluded with no value (exact, child, parent). const persistedRow = await activeTemplateRow(environmentId); const boot = (persistedRow.metadata as Record).bootRelevantConfig as { values: Record; excludedPaths: string[]; }; expect(boot.excludedPaths).toEqual(expect.arrayContaining(["apiUrl", "auth.token", "credentials"])); expect(Object.keys(boot.values)).not.toContain("apiUrl"); expect(Object.keys(boot.values)).not.toContain("auth.token"); expect(Object.keys(boot.values)).not.toContain("credentials"); // Neither the persisted snapshot nor the response leaks a secret value. for (const metadataJson of [ JSON.stringify(persistedRow.metadata), JSON.stringify(promoted.template.metadata), ]) { expect(metadataJson).not.toContain("secret-endpoint.example"); expect(metadataJson).not.toContain("auth-secret-value"); expect(metadataJson).not.toContain("cred-secret-value"); } // An excluded path forces the fail-closed unclassified result: the flag is required. await expect(service.relinkActiveTemplate({ environmentId, actor: relinkActor, companyId })) .rejects.toMatchObject({ status: 409 }); const relinked = await service.relinkActiveTemplate({ environmentId, confirmBootSourceDrift: true, actor: relinkActor, companyId, }); expect(relinked.classification).toBe("unclassified"); const responseJson = JSON.stringify(relinked); expect(responseJson).not.toContain("secret-endpoint.example"); expect(responseJson).not.toContain("auth-secret-value"); expect(responseJson).not.toContain("cred-secret-value"); const activities = await relinkActivityRows(environmentId); const relinkActivity = activities.find((row) => row.action === "environment.custom_image_template.relinked"); expect(relinkActivity).toBeDefined(); const activityJson = JSON.stringify(relinkActivity!.details); expect(activityJson).not.toContain("secret-endpoint.example"); expect(activityJson).not.toContain("auth-secret-value"); expect(activityJson).not.toContain("cred-secret-value"); // Drift detail carries path names only, never a fingerprint value. expect(activityJson).not.toContain(promoted.template.sourceEnvironmentConfigFingerprint); }); it("keeps secret values and the fingerprint out of the overview payload", async () => { const config = { provider: "fake-secret-plugin", image: "fake:base", apiUrl: "https://secret-endpoint.example", auth: "auth-secret-value", credentials: { secret: "cred-secret-value", region: "eu" }, reuseLease: false, }; const { environmentId } = await seed({ manifest: secretPluginManifest(), config }); const service = environmentCustomImageService(db, { pluginWorkerManager: createWorkerManager() }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" } }); const promoted = await service.finishSetupSession({ sessionId: started.session.id }); // A boot-source field changes, so the overview reports drift and carries the // drifted paths. The payload must never leak a secret value or a fingerprint. await db.update(environments) .set({ config: { ...config, image: "fake:other" } }) .where(eq(environments.id, environmentId)); const overview = await service.getOverview({ environmentId }); // An excluded secret-ref path forces the fail-closed unclassified result. expect(overview.activeTemplateDrift?.classification).toBe("unclassified"); // The full overview never leaks a secret value, and the server-internal // snapshot never reaches the template response. const overviewJson = JSON.stringify(overview); expect(overviewJson).not.toContain("secret-endpoint.example"); expect(overviewJson).not.toContain("auth-secret-value"); expect(overviewJson).not.toContain("cred-secret-value"); expect(overviewJson).not.toContain("bootRelevantConfig"); // The drift attribution carries path names and non-secret values only, never // a fingerprint value. const driftJson = JSON.stringify(overview.activeTemplateDrift); expect(driftJson).not.toContain(promoted.template.sourceEnvironmentConfigFingerprint); expect(driftJson).not.toContain("secret-endpoint.example"); expect(driftJson).not.toContain("auth-secret-value"); expect(driftJson).not.toContain("cred-secret-value"); }); it("fails closed for legacy templates without a boot-relevant snapshot", async () => { const { companyId, environmentId } = await seed(); const service = environmentCustomImageService(db, { pluginWorkerManager: createWorkerManager() }); await db.insert(environmentCustomImageTemplates).values({ environmentId, provider: "fake-plugin", templateKind: "snapshot", templateRef: "snapshot-legacy", sourceEnvironmentConfigFingerprint: "stale-fingerprint", status: "active", metadata: { runtimeConfigBinding: { field: "customTemplate", unsetFields: ["image"] } }, }); await expect(service.relinkActiveTemplate({ environmentId, actor: relinkActor, companyId })) .rejects.toMatchObject({ status: 409 }); const beforeRow = await activeTemplateRow(environmentId); expect(beforeRow.sourceEnvironmentConfigFingerprint).toBe("stale-fingerprint"); const relinked = await service.relinkActiveTemplate({ environmentId, confirmBootSourceDrift: true, actor: relinkActor, companyId, }); expect(relinked.classification).toBe("unclassified"); const afterRow = await activeTemplateRow(environmentId); expect(afterRow.sourceEnvironmentConfigFingerprint).not.toBe("stale-fingerprint"); }); it("relinks a knob-only change without a flag and keeps the fail-closed runtime gate", async () => { const { companyId, environmentId } = await seed(); const service = environmentCustomImageService(db, { pluginWorkerManager: createWorkerManager() }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" } }); const promoted = await service.finishSetupSession({ sessionId: started.session.id }); // A non-boot field changes the fingerprint and detaches the template. await db.update(environments) .set({ config: { provider: "fake-plugin", image: "fake:base", reuseLease: false, region: "eu" } }) .where(eq(environments.id, environmentId)); const detached = await service.getOverview({ environmentId }); expect(detached.activeTemplateMatchesConfig).toBe(false); const relinked = await service.relinkActiveTemplate({ environmentId, actor: relinkActor, companyId }); expect(relinked.classification).toBe("knob_only"); const afterRelink = await service.getOverview({ environmentId }); expect(afterRelink.activeTemplateMatchesConfig).toBe(true); const resolved = await resolveEnvironmentDriverConfigForRuntime(db, companyId, { id: environmentId, driver: "sandbox", config: { provider: "fake-plugin", image: "fake:base", reuseLease: false, region: "eu" }, }, { heartbeatRunId: randomUUID() }); expect(resolved.config).toMatchObject({ customTemplate: promoted.template.templateRef }); // The gate stays fail-closed: a base image change detaches again. await db.update(environments) .set({ config: { provider: "fake-plugin", image: "fake:other", reuseLease: false, region: "eu" } }) .where(eq(environments.id, environmentId)); const afterImageChange = await service.getOverview({ environmentId }); expect(afterImageChange.activeTemplateMatchesConfig).toBe(false); }); it("requires the flag for boot-source drift and re-stamps only after confirmation", async () => { const { companyId, environmentId } = await seed(); const service = environmentCustomImageService(db, { pluginWorkerManager: createWorkerManager() }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" } }); await service.finishSetupSession({ sessionId: started.session.id }); await db.update(environments) .set({ config: { provider: "fake-plugin", image: "fake:other", reuseLease: false } }) .where(eq(environments.id, environmentId)); await expect(service.relinkActiveTemplate({ environmentId, actor: relinkActor, companyId })) .rejects.toMatchObject({ status: 409, details: { classification: "boot_source_drift", driftedPaths: expect.arrayContaining([ expect.objectContaining({ path: "image", from: "fake:base", to: "fake:other" }), ]), }, }); const relinked = await service.relinkActiveTemplate({ environmentId, confirmBootSourceDrift: true, actor: relinkActor, companyId, }); expect(relinked.classification).toBe("boot_source_drift"); const afterRelink = await service.getOverview({ environmentId }); expect(afterRelink.activeTemplateMatchesConfig).toBe(true); }); it("re-stamps only the active template and never a superseded one", async () => { const { companyId, environmentId } = await seed(); const service = environmentCustomImageService(db, { pluginWorkerManager: createWorkerManager() }); const first = await service.finishSetupSession({ sessionId: (await service.startSetupSession({ environmentId, actor: { userId: "user-1" } })).session.id, }); const second = await service.finishSetupSession({ sessionId: (await service.startSetupSession({ environmentId, actor: { userId: "user-1" } })).session.id, }); // The first template is now superseded; the second is active. const supersededBefore = await db .select() .from(environmentCustomImageTemplates) .where(eq(environmentCustomImageTemplates.id, first.template.id)) .then((rows) => rows[0]!); await db.update(environments) .set({ config: { provider: "fake-plugin", image: "fake:base", reuseLease: false, region: "eu" } }) .where(eq(environments.id, environmentId)); const relinked = await service.relinkActiveTemplate({ environmentId, actor: relinkActor, companyId }); expect(relinked.template.id).toBe(second.template.id); const supersededAfter = await db .select() .from(environmentCustomImageTemplates) .where(eq(environmentCustomImageTemplates.id, first.template.id)) .then((rows) => rows[0]!); expect(supersededAfter.status).toBe("superseded"); expect(supersededAfter.sourceEnvironmentConfigFingerprint) .toBe(supersededBefore.sourceEnvironmentConfigFingerprint); }); it("aborts with a conflict and writes no active template when none exists", async () => { const { companyId, environmentId } = await seed(); const service = environmentCustomImageService(db, { pluginWorkerManager: createWorkerManager() }); await expect(service.relinkActiveTemplate({ environmentId, actor: relinkActor, companyId })) .rejects.toMatchObject({ status: 404 }); const activities = await relinkActivityRows(environmentId); expect(activities.filter((row) => row.action === "environment.custom_image_template.relinked")).toHaveLength(0); }); it("classifies an invalid driver identity path as unclassified without treating it as value-bearing", async () => { const { companyId, environmentId } = await seed({ manifest: invalidIdentityPluginManifest(), config: { provider: "fake-invalid-plugin", image: "fake:base", reuseLease: false, } }); const service = environmentCustomImageService(db, { pluginWorkerManager: createWorkerManager() }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" } }); const promoted = await service.finishSetupSession({ sessionId: started.session.id }); expect((promoted.template.metadata as Record).bootRelevantConfig).toBeUndefined(); const persistedRow = await activeTemplateRow(environmentId); const boot = (persistedRow.metadata as Record).bootRelevantConfig as { values: Record; excludedPaths: string[]; }; expect(boot.excludedPaths).toContain("[unresolved-identity-path]"); // The raw invalid paths are never persisted and never value-bearing. const metadataJson = JSON.stringify(persistedRow.metadata); expect(metadataJson).not.toContain("bad path!"); expect(metadataJson).not.toContain("a..b"); expect(Object.keys(boot.values)).not.toContain("bad path!"); expect(Object.keys(boot.values)).not.toContain("a..b"); await expect(service.relinkActiveTemplate({ environmentId, actor: relinkActor, companyId })) .rejects.toMatchObject({ status: 409 }); const relinked = await service.relinkActiveTemplate({ environmentId, confirmBootSourceDrift: true, actor: relinkActor, companyId, }); expect(relinked.classification).toBe("unclassified"); }); it("fails closed for prototype-key driver identity paths and never persists them", async () => { const { companyId, environmentId } = await seed({ manifest: prototypeKeyIdentityPluginManifest(), config: { provider: "fake-prototype-key-plugin", image: "fake:base", reuseLease: false, } }); const service = environmentCustomImageService(db, { pluginWorkerManager: createWorkerManager() }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" } }); const promoted = await service.finishSetupSession({ sessionId: started.session.id }); expect((promoted.template.metadata as Record).bootRelevantConfig).toBeUndefined(); const persistedRow = await activeTemplateRow(environmentId); const boot = (persistedRow.metadata as Record).bootRelevantConfig as { values: Record; excludedPaths: string[]; }; // Every prototype key fails canonicalization and records only the marker. expect(boot.excludedPaths).toContain("[unresolved-identity-path]"); // No raw prototype-key path and no value for one persists in the snapshot. expect(boot.excludedPaths).not.toContain("__proto__"); expect(boot.excludedPaths).not.toContain("constructor"); expect(boot.excludedPaths).not.toContain("prototype"); expect(Object.keys(boot.values)).not.toContain("__proto__"); expect(Object.keys(boot.values)).not.toContain("constructor"); expect(Object.keys(boot.values)).not.toContain("prototype"); // The prototype of the values map stays clean; no key leaked onto it. expect(Object.getPrototypeOf(boot.values)).toBe(Object.prototype); const metadataJson = JSON.stringify(persistedRow.metadata); expect(metadataJson).not.toContain("__proto__"); expect(metadataJson).not.toContain("nested.__proto__"); // An unflagged relink fails closed with 409 and the unclassified class. await expect(service.relinkActiveTemplate({ environmentId, actor: relinkActor, companyId })) .rejects.toMatchObject({ status: 409, details: { classification: "unclassified" } }); // A confirmed relink succeeds and writes the relinked activity entry. const relinked = await service.relinkActiveTemplate({ environmentId, confirmBootSourceDrift: true, actor: relinkActor, companyId, }); expect(relinked.classification).toBe("unclassified"); const activities = await relinkActivityRows(environmentId); const relinkActivity = activities.find((row) => row.action === "environment.custom_image_template.relinked"); expect(relinkActivity).toBeDefined(); }); it("fails closed when the provider adds a boot-relevant identity path after capture", async () => { const { companyId, environmentId } = await seed({ config: { provider: "fake-plugin", image: "fake:base", region: "us", reuseLease: false, } }); const service = environmentCustomImageService(db, { pluginWorkerManager: createWorkerManager() }); const started = await service.startSetupSession({ environmentId, actor: { userId: "user-1" } }); await service.finishSetupSession({ sessionId: started.session.id }); // A knob-only region change detaches the template. On its own it would // relink without confirmation. await db.update(environments) .set({ config: { provider: "fake-plugin", image: "fake:base", region: "eu", reuseLease: false } }) .where(eq(environments.id, environmentId)); // The provider now declares `region` a boot-relevant identity path. The // capture snapshot never covered it, so the server cannot verify the boot // source and must not classify the drift as knob-only. const manifest = pluginManifest(); const nextManifest = { ...manifest, environmentDrivers: [ { ...manifest.environmentDrivers[0], templateIdentityPaths: ["apiUrl", "region"] }, ], }; await db.update(plugins) .set({ manifestJson: nextManifest }) .where(eq(plugins.pluginKey, manifest.id)); await expect(service.relinkActiveTemplate({ environmentId, actor: relinkActor, companyId })) .rejects.toMatchObject({ status: 409, details: { classification: "unclassified" } }); const relinked = await service.relinkActiveTemplate({ environmentId, confirmBootSourceDrift: true, actor: relinkActor, companyId, }); expect(relinked.classification).toBe("unclassified"); }); });