From cf19e086ae4a0142c8b4314771c55f73b294805b Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 12 Oct 2026 16:17:12 -0500 Subject: [PATCH] fix(environments): bind platform facts to the effective immutable image Reject named template bindings over an immutable base image. Preserve the measured company and custom-image identity without claiming compatibility for a different boot source. Co-Authored-By: Paperclip --- .../src/__tests__/environment-probe.test.ts | 19 +++++++++++++++++-- server/src/services/environment-config.ts | 2 +- .../environment-custom-image-runtime.ts | 5 +++-- server/src/services/environment-probe.ts | 8 +++++--- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/server/src/__tests__/environment-probe.test.ts b/server/src/__tests__/environment-probe.test.ts index 125e8838fd..71f2c42555 100644 --- a/server/src/__tests__/environment-probe.test.ts +++ b/server/src/__tests__/environment-probe.test.ts @@ -69,7 +69,7 @@ describe("probeEnvironment", () => { expect(await readSandboxPlatformQualification({} as any, environment, "foreign-company")).toBeNull(); expect(await readSandboxPlatformQualification({} as any, { ...environment, config: { ...environment.config, image: `registry.example.test/runtime@sha256:${"b".repeat(64)}` } }, "company")).toBeNull(); vi.mocked(configRuntime.resolveEnvironmentDriverConfigForRuntime).mockImplementation(async (_db, _company, _env, options) => { - options?.onResolvedCustomImageTemplate?.({ id: "changed-template", capturedAt: null }); + options?.onResolvedCustomImageTemplate?.({ id: "changed-template", capturedAt: null, templateKind: "image", bindingField: "image" }); return { driver: "sandbox", config: environment.config }; }); expect(await readSandboxPlatformQualification({} as any, environment, "company")).toBeNull(); @@ -117,9 +117,24 @@ describe("probeEnvironment", () => { const identity = vi.fn(); const result = await resolveActiveEnvironmentCustomImageTemplateForRuntime(db as never, { environmentId: "env", baseConfig: { provider: "daytona", reuseLease: false }, runtimeConfig: { provider: "daytona", reuseLease: false }, readOnly: true, onResolvedTemplate: identity }); expect(result).toMatchObject({ image: row.templateRef }); - expect(identity).toHaveBeenCalledWith({ id: "template", capturedAt: capturedAt.toISOString() }); + expect(identity).toHaveBeenCalledWith({ id: "template", capturedAt: capturedAt.toISOString(), templateKind: "image", bindingField: "image" }); expect(update).not.toHaveBeenCalled(); }); + it.each(["provider_template", "unknown"])("does not qualify a named active %s boot binding over a pinned base image", async templateKind => { + const image = `registry.example.test/runtime@sha256:${"a".repeat(64)}`; + const environment = { id: "env", driver: "sandbox", status: "active", config: { provider: "daytona", image }, metadata: null } as any; + vi.spyOn(configRuntime, "resolveEnvironmentDriverConfigForRuntime").mockImplementation(async (_db, _company, _env, options) => { + const bindingField = templateKind === "provider_template" ? "template" : "templateRef"; + options?.onResolvedCustomImageTemplate?.({ id: "active-template", capturedAt: null, templateKind, bindingField }); + return { driver: "sandbox", config: { ...environment.config, [bindingField]: "mutable-template-name" } }; + }); + mockRuntimeAcquireRunLease.mockResolvedValue({ lease: { id: "lease", provider: "daytona", metadata: { image } } }); + const result = await probeEnvironment({} as any, environment, { companyId: "company", acquireSandboxRuntimeLease: true }); + expect(result.ok).toBe(true); + expect(result.details?.platformQualificationRecorded).toBe(false); + expect(mockRuntimeExecute).not.toHaveBeenCalled(); + expect(mockRecordPlatformProbe).toHaveBeenCalledTimes(1); + }); it("returns false when an atomic probe record loses its config comparison", async () => { const { environmentService } = await vi.importActual("../services/environments.js"); const returning = vi.fn().mockResolvedValue([]); diff --git a/server/src/services/environment-config.ts b/server/src/services/environment-config.ts index 27ae4e876f..002cea6a47 100644 --- a/server/src/services/environment-config.ts +++ b/server/src/services/environment-config.ts @@ -661,7 +661,7 @@ export async function resolveEnvironmentDriverConfigForRuntime( // configuration and tooling so the test reflects what real agent runs use. applyCustomImageTemplate?: boolean; readOnlyCustomImageTemplate?: boolean; - onResolvedCustomImageTemplate?: (identity: { id: string; capturedAt: string | null }) => void; + onResolvedCustomImageTemplate?: (identity: { id: string; capturedAt: string | null; templateKind: string; bindingField: string }) => void; }, ): Promise { const parsed = parseEnvironmentDriverConfig(environment); diff --git a/server/src/services/environment-custom-image-runtime.ts b/server/src/services/environment-custom-image-runtime.ts index 1ed7dc1a18..9babca70c1 100644 --- a/server/src/services/environment-custom-image-runtime.ts +++ b/server/src/services/environment-custom-image-runtime.ts @@ -560,7 +560,7 @@ export async function resolveActiveEnvironmentCustomImageTemplateForRuntime( secretRefExcludePaths?: Iterable; now?: Date; readOnly?: boolean; - onResolvedTemplate?: (identity: { id: string; capturedAt: string | null }) => void; + onResolvedTemplate?: (identity: { id: string; capturedAt: string | null; templateKind: string; bindingField: string }) => void; }, ): Promise { const row = await db @@ -591,7 +591,8 @@ export async function resolveActiveEnvironmentCustomImageTemplateForRuntime( // match. Runtime-only knobs such as lease reuse, timeouts, and resource hints // are excluded from the fingerprint so those edits do not discard the capture. const now = input.now ?? new Date(); - input.onResolvedTemplate?.({ id: row.id, capturedAt: row.capturedAt?.toISOString() ?? null }); + input.onResolvedTemplate?.({ id: row.id, capturedAt: row.capturedAt?.toISOString() ?? null, + templateKind: row.templateKind ?? "unknown", bindingField: resolveEnvironmentCustomImageRuntimeConfigBinding(row).field }); if (!input.readOnly) await db .update(environmentCustomImageTemplates) .set({ lastUsedAt: now, updatedAt: now }) diff --git a/server/src/services/environment-probe.ts b/server/src/services/environment-probe.ts index bd1b2dafbc..8ecad38bc6 100644 --- a/server/src/services/environment-probe.ts +++ b/server/src/services/environment-probe.ts @@ -34,15 +34,17 @@ export function parseSandboxPlatformFacts(output: string): PlatformFacts { ...(libc === "glibc" ? { glibcVersion: version?.trim() ?? "unknown" } : {}) }; } async function sandboxBinding(db: Db, environment: Environment, companyId: string) { - let template: { id: string; capturedAt: string | null } | null = null; + const selectedTemplate: { value: { id: string; capturedAt: string | null; templateKind: string; bindingField: string } | null } = { value: null }; const parsed = await resolveEnvironmentDriverConfigForRuntime(db, companyId, environment, { applyCustomImageTemplate: true, readOnlyCustomImageTemplate: true, - onResolvedCustomImageTemplate: identity => { template = identity; }, + onResolvedCustomImageTemplate: identity => { selectedTemplate.value = identity; }, }); if (parsed.driver !== "sandbox") return null; const image = parsed.config.image; // Names, mutable tags, provider defaults and snapshot names are not immutable identities. - if (!isImmutableImage(image) || record(parsed.config).snapshot) return null; + const template = selectedTemplate.value; + if (!isImmutableImage(image) || ["snapshot", "template", "templateRef"].some(key => record(parsed.config)[key]) + || (template && (template.templateKind !== "image" || template.bindingField !== "image"))) return null; return { baseConfigFingerprint: fingerprint(environment.config), runtimeConfigFingerprint: fingerprint(parsed.config as unknown as Record), image, template }; }