Qualify Claude defaults against pinned Daytona snapshot identities

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-15 14:11:29 -05:00
1 parent 2e107b341f
commit 7bd7699c9c
18 files changed
+325 -31

No files matched your search

@@ -1709,3 +1709,21 @@ Docker context/native compilation control:
[38622133915](https://github.com/paperclipai/paperclip/actions/runs/38622133915).
The behavior PR checklist now reflects those completed gates. This does not
qualify or publish the still-local platform image consumer.
### Hosted named-snapshot repair — 2026-10-15
The failed UI journey is retained unchanged. The correction extends explicit
Claude Connect qualification to Daytona snapshots: the plugin resolves a stable
provider ID and creates by that ID, the server measures the actual sandbox and
records v2 company/config/account/template-bound proof after owned cleanup, and
native acquisition pins that same ID. Existing image v1 readers remain intact;
ordinary Legacy acquisition and Codex setup eligibility are unchanged. Discovery
performs no new provider calls. Lookup timeout cannot continue into allocation.
A no-prompt live check with the pinned Daytona SDK 0.203.0 created an owned sandbox
from the managed snapshot ID, measured Linux x86_64 / glibc 2.41, and deleted it.
Receipt: `/private/tmp/claude-test-deployment-20261015/snapshot-id-live-probe.json`.
This is provider identity/architecture evidence, not native Claude task proof.
The new candidate still needs cloud checks, deployment, and a fresh ordinary UI
journey through creation, useful task, revision and downloaded artifacts.
@@ -95,7 +95,8 @@ Both probe endpoints require a board actor that is local-implicit or an instance
administrator; ordinary agent identity and company membership alone are denied.
The saved sandbox probe also resolves company/environment and secret access
before allocating a fresh lease. Its measurement is company-bound to an immutable
image digest, effective provider configuration and active custom-image identity;
image digest or verified Daytona snapshot ID, effective provider configuration
and active custom-image identity;
only confirmed cleanup permits recording it. A draft probe does not record saved
qualification. Reading stored qualification does not lease/provision a target and
does not grant an agent permission to probe or manage that target.
@@ -117,7 +118,8 @@ at the scoped commit. A connection-only success or a fixture cannot certify
native compatibility, account authentication or agent access.
Scoped Verification: `not-run` — on one authorized owned target, compare pinned
image proof with mutable/snapshot and changed-config controls; require independent
image and provider snapshot-ID proof with mutable-image, unverified-snapshot
and changed-config/account controls; require independent
cleanup evidence before recording proof. Assert agent/non-admin board denial,
secret-context denial where the actor lacks access and forged-proof rejection.
Verify API reads hide the private record and metadata-only updates cannot overwrite it. Then use
+6 -3
View File
@@ -42,8 +42,8 @@ Automated: [environment UI](../../ui/src/pages/CompanyEnvironments.test.tsx), [d
Manual: Save a disposable target, reload it, and run its available probe. Verify both success and missing-credential/reachability errors identify the target. Confirm unavailable providers are not offered as working run targets.
For an immutable sandbox image, pin the OCI image with `@sha256` and explicitly
probe the saved environment in the intended company. The existing connection
For an immutable sandbox image, pin the OCI image with `@sha256`, or select
a Daytona snapshot whose provider ID can be verified, and explicitly probe the saved environment in the intended company. The existing connection
probe allocates a fresh lease and can measure its OS, architecture and libc.
Qualification is recorded only when the selected image/configuration matches
the live lease and cleanup is confirmed. Inspect `platformQualificationRecorded`
@@ -52,7 +52,10 @@ the image has native runtime qualification.
The measured record is server-owned, company-bound and tied to the effective
configuration and active custom-image identity. Mutable tags, provider defaults
and name-only snapshots do not qualify. Changing the target or image invalidates
and snapshots without verified provider identities do not qualify. Daytona
qualification creates by provider ID and binds native execution to that same ID;
a deleted ID fails instead of resolving a replacement name. Changing the target,
account, image or active captured template invalidates
the record; normal metadata edits cannot forge or overwrite it, and API reads
omit the private metadata. Reading this evidence for discovery or creation does
not allocate a sandbox. The draft `probe-config` checks are not a saved-image
+5 -3
View File
@@ -68,14 +68,16 @@ Use [runner validation](./runs-adapters.md#adapter-setup) and
[environment qualification](./execution-environments.md#environment-settings)
for the prerequisites; a successful login alone is insufficient.
For a new Claude agent on an explicit immutable sandbox image, use Connect to
For a new Claude agent on an explicit immutable sandbox image or a Daytona
named snapshot, use Connect to
qualify the selected environment through the existing permission-gated probe.
Do not preprobe outside onboarding when claiming this first-use path. Fresh
scoped discovery determines the runner for both the account test and hire;
the measured Legacy result remains valid. An instance-admin denial, incomplete
cleanup, missing lease or changed configuration stops setup before the adapter
test or hire. Named snapshots and mutable image tags do not gain qualification
through this flow. [Helper controls](../../ui/src/lib/qualify-claude-setup.test.ts)
test or hire. Daytona resolves the selected snapshot to its immutable provider
ID before creating the measured lease; native execution pins that same ID.
Mutable image tags and other providers’ named templates remain unqualified. [Helper controls](../../ui/src/lib/qualify-claude-setup.test.ts)
and mounted wizard/new-agent tests use mocks; live managed acceptance remains a
separate gate.
For a new Codex agent with Auto on a saved immutable OCI sandbox, Connect first
+18 -2
View File
@@ -234,10 +234,26 @@ for an image pinned with `@sha256`, after confirmed lease cleanup. The record is
server-owned and bound to the company, effective provider/image configuration
and active custom-image identity. Creation and discovery read this record without
allocating a sandbox. Configuration or image changes invalidate it. Mutable tags,
provider defaults and name-only snapshots do not establish compatibility; they
keep automatic Legacy selection until there is verifiable immutable image evidence.
provider defaults and unverified snapshot names do not establish compatibility;
they keep automatic Legacy selection until there is verifiable immutable image
or Daytona snapshot-ID evidence. See managed snapshot qualification below.
A connection-only success does not establish native readiness.
### Managed Daytona snapshot qualification
Claude's automatic Connect flow can qualify a named Daytona snapshot through the
existing administrator-only saved environment probe. The bundled Daytona plugin
resolves the name to its provider snapshot ID, creates the probe sandbox by ID,
and returns its account and creation identity. OS, architecture and libc facts
are recorded only after confirmed cleanup. Native execution uses that same
snapshot ID; discovery does not allocate or perform provider lookups. A deleted
snapshot fails at the provider rather than silently selecting a replacement with
the same name. Configuration, resolved credentials and active captured-template
changes require another probe. Other named provider templates and mutable OCI
tags remain unqualified. A provider plugin without snapshot identity support
returns an actionable setup failure; it cannot certify a native default.
On Linux, Codex's sandboxed filesystem modes also require the host to permit its
unprivileged namespaces. A no-privilege Docker container can prohibit nested
namespace creation even when installation and binary compatibility pass.
@@ -141,6 +141,50 @@ describe("Daytona sandbox provider plugin", () => {
__resetDaytonaSandboxHandleCacheForTest();
});
const snapshotRecord = { id: "a6b9e7d0-f886-471a-b461-640dd03e8d0f", name: "managed-snapshot", organizationId: "provider-account", createdAt: "2026-10-07T23:19:57.133Z" };
const snapshotProbe = { driverKey: "daytona", companyId: "company-1", environmentId: "env-1", runId: "probe-1",
resolveSnapshotIdentity: true, config: { apiKey: "fixture-key", snapshot: "managed-snapshot", reuseLease: false } };
it("qualifies a named snapshot by creating from its immutable provider ID", async () => {
mockSnapshotGet.mockResolvedValue(snapshotRecord);
mockCreate.mockResolvedValue(createMockSandbox());
const lease = await plugin.definition.onEnvironmentAcquireLease!(snapshotProbe);
expect(mockSnapshotGet).toHaveBeenCalledWith("managed-snapshot");
expect(mockCreate.mock.calls[0][0].snapshot).toBe(snapshotRecord.id);
expect(lease.metadata).toMatchObject({ snapshot: "managed-snapshot", snapshotIdentity: {
...snapshotRecord, provider: "daytona", requestedRef: "managed-snapshot",
} });
});
it("does not resolve snapshot names during ordinary acquisition", async () => {
mockCreate.mockResolvedValue(createMockSandbox());
await plugin.definition.onEnvironmentAcquireLease!({ ...snapshotProbe, resolveSnapshotIdentity: undefined });
expect(mockSnapshotGet).not.toHaveBeenCalled();
expect(mockCreate.mock.calls[0][0].snapshot).toBe("managed-snapshot");
});
it.each([{ id: "mutable-name" }, { organizationId: "" }, { name: "different-name" }, { createdAt: "invalid" }])(
"rejects unverified snapshot identity %j before allocation", async invalid => {
mockSnapshotGet.mockResolvedValue({ ...snapshotRecord, ...invalid });
await expect(plugin.definition.onEnvironmentAcquireLease!(snapshotProbe)).rejects.toThrow("immutable snapshot identity");
expect(mockCreate).not.toHaveBeenCalled();
},
);
it("never allocates after a snapshot identity lookup times out", async () => {
vi.useFakeTimers();
try {
let resolveLookup!: (value: typeof snapshotRecord) => void;
mockSnapshotGet.mockReturnValue(new Promise(resolve => { resolveLookup = resolve; }));
const result = plugin.definition.onEnvironmentAcquireLease!(snapshotProbe).catch(error => error);
await vi.advanceTimersByTimeAsync(10_001);
expect(await result).toBeInstanceOf(Error);
resolveLookup(snapshotRecord);
await vi.advanceTimersByTimeAsync(1);
expect(mockCreate).not.toHaveBeenCalled();
} finally { vi.useRealTimers(); }
});
it("declares environment lifecycle handlers", async () => {
expect(await plugin.definition.onHealth?.()).toEqual({
status: "ok",
@@ -210,6 +210,8 @@ type DaytonaSnapshotService = {
delete?: (snapshot: unknown) => Promise<void>;
};
type SnapshotIdentity = { provider: "daytona"; requestedRef: string; id: string; name: string; organizationId: string; createdAt: string };
const WORKSPACE_SENTINEL_RELATIVE_PATH = ".paperclip-runtime/reusable-sandbox-lease.json";
// Quota-safety defaults (minutes). Daytona counts *stopped* sandboxes against
@@ -672,6 +674,7 @@ function leaseMetadata(input: {
resumedFromState?: string | null;
sandboxState?: string | null;
workspaceSentinel?: WorkspaceSentinelResult;
snapshotIdentity?: SnapshotIdentity;
}) {
return {
provider: "daytona",
@@ -681,6 +684,7 @@ function leaseMetadata(input: {
sandboxState: input.sandboxState ?? input.sandbox.state ?? null,
image: input.config.image,
snapshot: input.config.snapshot,
...(input.snapshotIdentity ? { snapshotIdentity: input.snapshotIdentity } : {}),
target: input.sandbox.target,
timeoutMs: input.config.timeoutMs,
reuseLease: input.config.reuseLease,
@@ -956,6 +960,7 @@ async function createSandbox(
purpose?: string;
onCreateAttempt?: (cleanup: PluginEnvironmentCreationCleanup) => void;
onCreateFailure?: () => PluginEnvironmentAcquisitionDiagnostic;
onSnapshotIdentity?: (identity: SnapshotIdentity) => void;
} = {},
): Promise<Sandbox> {
const resourceRequestError = validateRuntimeResourceRequest(config);
@@ -963,6 +968,23 @@ async function createSandbox(
throw new Error(resourceRequestError);
}
const client = createDaytonaClient(config);
let createConfig = config;
if ("resolveSnapshotIdentity" in params && params.resolveSnapshotIdentity && config.snapshot && !config.image) {
// This read happens only during explicit qualification. The SDK does not
// bound snapshot.get; a timed-out read must never continue into allocation.
const snapshot = await withLivenessTimeout("snapshot.identity", Math.min(config.timeoutMs || 10_000, 10_000),
() => client.snapshot.get(config.snapshot!));
if (!/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(snapshot.id ?? "")
|| !snapshot.name || !snapshot.organizationId || !snapshot.createdAt
|| (config.snapshot !== snapshot.id && config.snapshot !== snapshot.name)
|| !Number.isFinite(Date.parse(String(snapshot.createdAt)))) {
throw new Error("Daytona did not return an immutable snapshot identity. Retry the connection probe or select Legacy runner.");
}
const identity: SnapshotIdentity = { provider: "daytona", requestedRef: config.snapshot,
id: snapshot.id, name: snapshot.name, organizationId: snapshot.organizationId, createdAt: new Date(snapshot.createdAt).toISOString() };
createConfig = { ...config, snapshot: identity.id };
options.onSnapshotIdentity?.(identity);
}
// The SDK can create a sandbox and then throw while waiting for it to start.
// Keep an attempt-specific provider name so that failure cannot lose ownership.
const attemptId = randomUUID();
@@ -977,7 +999,7 @@ async function createSandbox(
}), "paperclip-create-attempt": attemptId };
// The SDK mutates params.labels (for example, code-toolbox-language).
// Preserve our immutable ownership snapshot for validation and retry.
const createParams = { ...buildCreateParams(config, { ...labels }), name };
const createParams = { ...buildCreateParams(createConfig, { ...labels }), name };
const cleanup: PluginEnvironmentCreationCleanup = {
providerLeaseId: name, companyId: params.companyId, environmentId: params.environmentId,
...("runId" in params ? { runId: params.runId } : {}),
@@ -2491,6 +2513,7 @@ const plugin = definePlugin({
params: PluginEnvironmentAcquireLeaseParams,
): Promise<PluginEnvironmentLease> {
const config = parseDriverConfig(params.config);
let snapshotIdentity: SnapshotIdentity | undefined;
// One budget covers creation, setup, and inline cleanup. The host leaves
// 30 seconds beyond this deadline to receive/journal the cleanup record.
const budgetMs = config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_DAYTONA_OPERATION_TIMEOUT_MS;
@@ -2534,8 +2557,9 @@ const plugin = definePlugin({
};
const acquire = async (): Promise<PluginEnvironmentLease> => {
const sandbox = await createSandbox(params, config, {
onCreateAttempt: (attempt) => { cleanup = attempt; },
onCreateAttempt: (attempt) => { assertActive(); cleanup = attempt; },
onCreateFailure: observeFailure,
onSnapshotIdentity: identity => { snapshotIdentity = identity; },
});
try {
assertActive();
@@ -2595,6 +2619,7 @@ const plugin = definePlugin({
remoteCwd,
resumedLease: false,
workspaceSentinel,
snapshotIdentity,
}),
};
} catch (error) {
+5
View File
@@ -635,6 +635,11 @@ export interface PluginEnvironmentRunnerIngressEndpointParams
export interface PluginEnvironmentAcquireLeaseParams extends PluginEnvironmentDriverBaseParams {
runId: string;
/** Explicit platform probes may request a provider-owned immutable snapshot
* identity. Supporting providers create by that ID and return snapshotIdentity
* in lease metadata. Older providers may omit it; the host then cannot qualify
* a mutable snapshot name. Ordinary discovery must not allocate a sandbox. */
resolveSnapshotIdentity?: boolean;
workspaceMode?: string;
requestedCwd?: string;
agentId?: string;
@@ -50,6 +50,35 @@ describe("probeEnvironment", () => {
mockEnvironmentRuntimeService.mockClear();
});
it("qualifies a named Daytona snapshot only after an ID-pinned lease and owned cleanup", async () => {
const environment = { id: "env", driver: "sandbox", status: "active", config: { provider: "daytona", snapshot: "managed-snapshot" }, metadata: null } as any;
const identity = { provider: "daytona", requestedRef: "managed-snapshot", id: "a6b9e7d0-f886-471a-b461-640dd03e8d0f", name: "managed-snapshot", organizationId: "provider-account", createdAt: "2026-10-07T23:19:57.133Z" };
vi.spyOn(configRuntime, "resolveEnvironmentDriverConfigForRuntime").mockResolvedValue({ driver: "sandbox", config: environment.config });
mockRuntimeAcquireRunLease.mockResolvedValue({ lease: { id: "lease", provider: "daytona", metadata: { snapshot: "managed-snapshot", snapshotIdentity: identity } } });
mockRuntimeExecute.mockResolvedValue({ exitCode: 0, stdout: "Linux\nx86_64\nglibc\nglibc 2.39\n" });
const result = await probeEnvironment({} as any, environment, { companyId: "company", acquireSandboxRuntimeLease: true });
expect(result.details?.platformQualificationRecorded).toBe(true);
expect(mockRuntimeAcquireRunLease).toHaveBeenCalledWith(expect.objectContaining({ resolveSnapshotIdentity: true }));
const proof = mockRecordPlatformProbe.mock.calls[1]![0].proof;
expect(proof).toMatchObject({ v: 2, snapshotIdentity: identity });
environment.metadata = { [SANDBOX_PLATFORM_PROBE_METADATA_KEY]: { company: proof } };
expect(await readSandboxPlatformQualification({} as any, environment, "company")).toMatchObject({ platform: "linux", architecture: "x64", glibcVersion: "glibc 2.39" });
expect(await readSandboxPlatformQualification({} as any, environment, "foreign-company")).toBeNull();
expect(mockRuntimeAcquireRunLease).toHaveBeenCalledTimes(1);
});
it.each(["missing", "wrong-reference", "missing-owner", "cleanup"])("does not qualify a named snapshot after %s", async failure => {
const environment = { id: "env", driver: "sandbox", status: "active", config: { provider: "daytona", snapshot: "managed-snapshot" } } as any;
const identity = { provider: "daytona", requestedRef: failure === "wrong-reference" ? "other" : "managed-snapshot", id: "a6b9e7d0-f886-471a-b461-640dd03e8d0f", name: "managed-snapshot", organizationId: failure === "missing-owner" ? "" : "provider-account", createdAt: "2026-10-07T23:19:57.133Z" };
vi.spyOn(configRuntime, "resolveEnvironmentDriverConfigForRuntime").mockResolvedValue({ driver: "sandbox", config: environment.config });
mockRuntimeAcquireRunLease.mockResolvedValue({ lease: { id: "lease", provider: "daytona", metadata: { snapshot: "managed-snapshot", ...(failure === "missing" ? {} : { snapshotIdentity: identity }) } } });
mockRuntimeExecute.mockResolvedValue({ exitCode: 0, stdout: "Linux\nx86_64\nglibc\nglibc 2.39\n" });
if (failure === "cleanup") mockRuntimeReleaseRunLease.mockResolvedValue({ id: "lease", companyId: "company", environmentId: "env", status: "released", cleanupStatus: "failed" });
const result = await probeEnvironment({} as any, environment, { companyId: "company", acquireSandboxRuntimeLease: true });
expect(result.details?.platformQualificationRecorded).toBe(false);
expect(mockRecordPlatformProbe).toHaveBeenCalledTimes(1);
});
it.each(["2.38", "2.39", "musl"])("records actual immutable-image facts for %s and reads them without another lease", async libc => {
const image = `registry.example.test/runtime@sha256:${"a".repeat(64)}`;
const environment = { id: "env", driver: "sandbox", status: "active", config: { provider: "daytona", image, reuseLease: false }, metadata: null } as any;
@@ -322,6 +322,37 @@ describeEmbeddedPostgres("environmentRuntimeService", () => {
} as unknown as PluginWorkerManager;
}
it.each(["paperclip_runner", "claude_local", null])("pins qualified snapshots only for native execution (%s)", async adapterType => {
const pluginId = randomUUID();
const { companyId, environment: baseEnvironment, runId } = await seedEnvironment();
const config = { provider: "daytona", snapshot: "managed-snapshot", reuseLease: false, timeoutMs: 1234 };
await insertSandboxProviderPlugin(pluginId, { environmentDrivers: [{ driverKey: "daytona", kind: "sandbox_provider", displayName: "Daytona", configSchema: { type: "object" } }] });
await environmentService(db).update(baseEnvironment.id, { driver: "sandbox", config });
const environment = { ...baseEnvironment, driver: "sandbox", config };
const parsed = await resolveEnvironmentDriverConfigForRuntime(db, companyId, environment);
const snapshotId = "a6b9e7d0-f886-471a-b461-640dd03e8d0f";
environment.metadata = { paperclipSandboxPlatformProbes: { [companyId]: {
v: 2, companyId, snapshot: config.snapshot, template: null,
baseConfigFingerprint: createHash("sha256").update(stableStringify(config)).digest("hex"),
runtimeConfigFingerprint: createHash("sha256").update(stableStringify(parsed.config)).digest("hex"),
snapshotIdentity: { provider: "daytona", requestedRef: config.snapshot, id: snapshotId, name: config.snapshot, organizationId: "provider-account", createdAt: "2026-10-07T23:19:57.133Z" },
} } };
const worker = createFreshPluginLeaseWorker(pluginId);
vi.mocked(worker.call).mockImplementation(async (_id, method, params: any) => {
if (method !== "environmentAcquireLease") throw new Error(`Unexpected ${method}`);
return { providerLeaseId: "snapshot-lease", metadata: { provider: "daytona", snapshot: params.config.snapshot, remoteCwd: "/workspace" } };
});
const service = environmentRuntimeService(db, { pluginWorkerManager: worker });
const acquired = await service.acquireRunLease({ companyId, environment, issueId: null, heartbeatRunId: runId,
persistedExecutionWorkspace: null, adapterType, ...(adapterType === null ? { resolveSnapshotIdentity: true } : {}) });
const expectedSnapshot = adapterType === "paperclip_runner" ? snapshotId : config.snapshot;
expect(worker.call).toHaveBeenCalledWith(pluginId, "environmentAcquireLease", expect.objectContaining({
config: expect.objectContaining({ snapshot: expectedSnapshot }),
...(adapterType === null ? { resolveSnapshotIdentity: true } : {}),
}), expect.any(Number));
expect(acquired.lease.metadata?.snapshot).toBe(expectedSnapshot);
});
function insertReusableExecutionWorkspace(executionWorkspaceId: string, companyId: string, projectId: string) {
return db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import type { Environment, SandboxEnvironmentConfig } from "@paperclipai/shared";
import { fingerprintEnvironmentSandboxProviderConfig as fingerprint, SANDBOX_PLATFORM_PROBE_METADATA_KEY } from "../services/environment-custom-image-runtime.js";
import { qualifiedSandboxSnapshotId, type SandboxTemplateIdentity } from "../services/sandbox-snapshot-qualification.js";
function fixture() {
const config = { provider: "daytona", snapshot: "managed-snapshot", apiKey: "fixture-account" } as SandboxEnvironmentConfig;
const template: SandboxTemplateIdentity = { id: "template", capturedAt: "2026-10-15T00:00:00Z", templateKind: "snapshot", bindingField: "snapshot" };
const identity = { provider: "daytona", requestedRef: config.snapshot, id: "a6b9e7d0-f886-471a-b461-640dd03e8d0f", name: config.snapshot, organizationId: "provider-account", createdAt: "2026-10-07T23:19:57.133Z" };
const proof = { v: 2, companyId: "company", snapshot: config.snapshot, template,
baseConfigFingerprint: fingerprint(config), runtimeConfigFingerprint: fingerprint(config), snapshotIdentity: identity };
const environment = { config, metadata: { [SANDBOX_PLATFORM_PROBE_METADATA_KEY]: { company: proof } } } as unknown as Environment;
return { config, template, identity, proof, environment };
}
describe("qualified snapshot execution binding", () => {
it("pins the measured provider ID without a network lookup", () => {
const f = fixture();
expect(qualifiedSandboxSnapshotId(f.environment, "company", f.config, f.template)).toBe(f.identity.id);
});
it.each(["company", "base-config", "account", "template", "snapshot", "image", "provider", "identity"])("rejects changed %s", change => {
const f = fixture();
let company = "company";
if (change === "company") company = "foreign-company";
if (change === "base-config") f.environment.config = { ...f.config, target: "different" };
if (change === "account") f.config = { ...f.config, apiKey: "different-account" };
if (change === "template") f.template = { ...f.template!, capturedAt: "2026-10-16T00:00:00Z" };
if (change === "snapshot") f.config = { ...f.config, snapshot: "new-snapshot" };
if (change === "image") f.config = { ...f.config, image: "other-image" };
if (change === "provider") f.config = { ...f.config, provider: "other-provider" };
if (change === "identity") f.identity.id = "mutable-name";
expect(qualifiedSandboxSnapshotId(f.environment, company, f.config, f.template)).toBeNull();
});
});
+21 -9
View File
@@ -14,6 +14,7 @@ import { environmentRuntimeService } from "./environment-runtime.js";
import { environmentService } from "./environments.js";
import { fingerprintEnvironmentSandboxProviderConfig, SANDBOX_PLATFORM_PROBE_METADATA_KEY } from "./environment-custom-image-runtime.js";
import type { SandboxEnvironmentConfig } from "@paperclipai/shared";
import { isQualifiableDaytonaSnapshot, qualifiedSandboxSnapshotId, readSnapshotIdentity, type SandboxTemplateIdentity } from "./sandbox-snapshot-qualification.js";
export const SANDBOX_PLATFORM_PROBE_COMMAND = "uname -s; uname -m; if version=$(getconf GNU_LIBC_VERSION 2>/dev/null) && [ -n \"$version\" ]; then printf 'glibc\\n%s\\n' \"$version\"; else case \"$(ldd --version 2>&1)\" in *musl*) printf 'musl\\n' ;; *) printf 'unknown\\n' ;; esac; fi";
type PlatformFacts = { platform: string; architecture: string; libc: "glibc" | "musl" | "unknown"; glibcVersion?: string };
@@ -34,7 +35,7 @@ export function parseSandboxPlatformFacts(output: string): PlatformFacts {
...(libc === "glibc" ? { glibcVersion: version?.trim() ?? "unknown" } : {}) };
}
async function sandboxBinding(db: Db, environment: Environment, companyId: string) {
const selectedTemplate: { value: { id: string; capturedAt: string | null; templateKind: string; bindingField: string } | null } = { value: null };
const selectedTemplate: { value: SandboxTemplateIdentity } = { value: null };
const parsed = await resolveEnvironmentDriverConfigForRuntime(db, companyId, environment, {
applyCustomImageTemplate: true, readOnlyCustomImageTemplate: true,
onResolvedCustomImageTemplate: identity => { selectedTemplate.value = identity; },
@@ -42,6 +43,10 @@ async function sandboxBinding(db: Db, environment: Environment, companyId: strin
if (parsed.driver !== "sandbox") return null;
const image = parsed.config.image;
const template = selectedTemplate.value;
if (isQualifiableDaytonaSnapshot(parsed.config, template)) {
return { baseConfigFingerprint: fingerprint(environment.config), runtimeConfigFingerprint: fingerprint(parsed.config),
snapshot: parsed.config.snapshot, template, qualifiedSnapshotId: qualifiedSandboxSnapshotId(environment, companyId, parsed.config, template) };
}
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<string, unknown>), image, template };
@@ -49,9 +54,11 @@ async function sandboxBinding(db: Db, environment: Environment, companyId: strin
export async function readSandboxPlatformQualification(db: Db, environment: Environment, companyId: string): Promise<PlatformFacts | null> {
const proof = record(record(environment.metadata?.[SANDBOX_PLATFORM_PROBE_METADATA_KEY])[companyId]);
if (proof.v !== 1 || proof.companyId !== companyId || proof.baseConfigFingerprint !== fingerprint(environment.config)) return null;
if (![1, 2].includes(Number(proof.v)) || proof.companyId !== companyId || proof.baseConfigFingerprint !== fingerprint(environment.config)) return null;
const binding = await sandboxBinding(db, environment, companyId);
if (!binding || proof.runtimeConfigFingerprint !== binding.runtimeConfigFingerprint || proof.image !== binding.image
if (!binding || (proof.v === 2 ? !("qualifiedSnapshotId" in binding && binding.qualifiedSnapshotId)
: !("image" in binding) || proof.image !== binding.image)
|| proof.runtimeConfigFingerprint !== binding.runtimeConfigFingerprint
|| JSON.stringify(proof.template) !== JSON.stringify(binding.template)) return null;
const facts = record(proof.facts);
if (typeof facts.platform !== "string" || typeof facts.architecture !== "string"
@@ -140,6 +147,7 @@ export async function probeEnvironment(
heartbeatRunId: null,
persistedExecutionWorkspace: null,
adapterType: null,
resolveSnapshotIdentity: true,
applyCustomImageTemplate: options.applyCustomImageTemplate === true,
});
const metadata = leaseRecord.lease.metadata ?? {};
@@ -149,11 +157,13 @@ export async function probeEnvironment(
: null;
details = { provider, providerLeaseId: leaseRecord.lease.providerLeaseId, leaseId: leaseRecord.lease.id,
leasePolicy: leaseRecord.lease.leasePolicy, metadata, platformQualificationRecorded: false,
platformQualificationDiagnostic: "An immutable OCI image digest is required; this connection probe does not qualify a mutable image or snapshot name." };
if (isImmutableImage(metadata.image) && !metadata.snapshot) {
platformQualificationDiagnostic: "Platform qualification requires an immutable OCI image or a Daytona snapshot with a verified provider ID. Update the provider plugin and probe again, or select Legacy runner." };
if ((isImmutableImage(metadata.image) && !metadata.snapshot) || metadata.snapshotIdentity) {
try {
const binding = await sandboxBinding(db, environment, resolvedCompanyId);
if (binding && metadata.image === binding.image) {
const snapshotIdentity = binding && "snapshot" in binding ? readSnapshotIdentity(metadata.snapshotIdentity, binding.snapshot) : null;
if (binding && (("image" in binding && metadata.image === binding.image && !metadata.snapshot)
|| (snapshotIdentity && metadata.snapshot === binding.snapshot && provider === "daytona"))) {
const platform = await runtime.execute({ environment: probeEnvironmentConfig, lease: leaseRecord.lease,
command: "sh", args: ["-c", SANDBOX_PLATFORM_PROBE_COMMAND],
bypassSession: true, timeoutMs: 10_000, commandTimeoutMs: 10_000 });
@@ -163,10 +173,12 @@ export async function probeEnvironment(
if (facts.platform !== "unknown" && facts.architecture !== "unknown"
&& (facts.platform !== "linux" || facts.libc !== "unknown")
&& (facts.libc !== "glibc" || /^(?:glibc\s+)?\d+\.\d+(?:\.\d+)?$/.test(facts.glibcVersion ?? ""))) {
qualification = { v: 1, companyId: resolvedCompanyId, ...binding, facts, probedAt: new Date().toISOString() };
const { qualifiedSnapshotId: _qualifiedSnapshotId, ...recordedBinding } = { qualifiedSnapshotId: null, ...binding };
qualification = { v: snapshotIdentity ? 2 : 1, companyId: resolvedCompanyId, ...recordedBinding,
...(snapshotIdentity ? { snapshotIdentity } : {}), facts, probedAt: new Date().toISOString() };
delete details.platformQualificationDiagnostic;
} else details.platformQualificationDiagnostic = "The image's OS, architecture or libc could not be identified. Legacy remains the creation default.";
} else details.platformQualificationDiagnostic = "The live lease does not match the selected immutable image configuration. Probe again.";
} else details.platformQualificationDiagnostic = "The sandbox OS, architecture or libc could not be identified. Legacy remains the creation default.";
} else details.platformQualificationDiagnostic = "The live lease does not match the selected image or snapshot identity. Probe again.";
} catch {
details.platformQualificationDiagnostic = "Platform qualification could not complete; connection readiness does not establish native compatibility. Check the image and probe again.";
}
+13 -1
View File
@@ -43,6 +43,7 @@ import {
} from "@paperclipai/adapter-utils/acpx-engine/startup-timing";
import { environmentService } from "./environments.js";
import { instanceSettingsService } from "./instance-settings.js";
import { qualifiedSandboxSnapshotId, type SandboxTemplateIdentity } from "./sandbox-snapshot-qualification.js";
import { verifyNativeHarnessBackupStamp } from "./native-runtime/native-harness-backup-stamp.js";
import {
collectEnvironmentSecretRefs,
@@ -454,6 +455,8 @@ export interface EnvironmentDriverAcquireInput {
* configured default adapter.
*/
adapterType: string | null;
/** Resolve an immutable provider snapshot identity for an explicit platform probe. */
resolveSnapshotIdentity?: boolean;
/**
* Force applying the active custom-image template even when issueId and
* heartbeatRunId are null. Operator-initiated `Test` probes set this so the
@@ -1844,14 +1847,19 @@ function createSandboxEnvironmentDriver(
async acquireRunLease(input) {
const storedParsed = parseEnvironmentDriverConfig(input.environment);
const selectedTemplate: { value: SandboxTemplateIdentity } = { value: null };
const parsed = await resolveEnvironmentDriverConfigForRuntime(db, input.companyId, input.environment, {
issueId: input.issueId,
heartbeatRunId: input.heartbeatRunId,
applyCustomImageTemplate: input.applyCustomImageTemplate ?? false,
onResolvedCustomImageTemplate: identity => { selectedTemplate.value = identity; },
});
if (parsed.driver !== "sandbox" || storedParsed.driver !== "sandbox") {
throw new Error(`Expected sandbox environment config for driver "${input.environment.driver}".`);
}
const pinnedSnapshotId = input.adapterType === "paperclip_runner"
? qualifiedSandboxSnapshotId(input.environment, input.companyId, parsed.config, selectedTemplate.value) : null;
if (pinnedSnapshotId) parsed.config = { ...parsed.config, snapshot: pinnedSnapshotId };
// Check if this provider should be handled by a plugin.
if (!isBuiltinSandboxProvider(parsed.config.provider)) {
@@ -1882,7 +1890,8 @@ function createSandboxEnvironmentDriver(
}
const workerConfig = stripSandboxProviderEnvelope(parsed.config);
const storedConfig = storedParsed.config;
const storedConfig = pinnedSnapshotId ? { ...storedParsed.config, snapshot: pinnedSnapshotId } : storedParsed.config;
if (pinnedSnapshotId) delete storedConfig.image;
const providerConfigForLease = sandboxConfigForLeaseMetadata(storedConfig);
// Require the reusable-lease capability AND a worker that verifies the
// reuse methods. The provider must first opt in through the declaration:
@@ -2122,6 +2131,7 @@ function createSandboxEnvironmentDriver(
// environment's default adapter image (a pi agent then runs in the
// opencode image and the harness binary is missing at exec time).
adapterType: input.adapterType ?? undefined,
...(input.resolveSnapshotIdentity ? { resolveSnapshotIdentity: true } : {}),
// Forward a caller deadline so the provider configures a provider-side
// expiry at or before it and returns the real provider expiry.
...(requestedExpiresAtParam(input.requestedExpiresAt) !== undefined
@@ -4000,6 +4010,7 @@ export function environmentRuntimeService(
executionWorkspaceSettings?: IssueExecutionWorkspaceSettings | null;
/** The agent's adapter type for this run (mixed-harness environments). */
adapterType?: string | null;
resolveSnapshotIdentity?: boolean;
/**
* Force applying the active custom-image template even for ad-hoc (no
* issue/run) invocations. Operator `Test` probes set this so the runtime
@@ -4037,6 +4048,7 @@ export function environmentRuntimeService(
executionWorkspaceMode: leaseContext.executionWorkspaceMode,
executionWorkspaceSettings: input.executionWorkspaceSettings ?? null,
adapterType: input.adapterType ?? null,
resolveSnapshotIdentity: input.resolveSnapshotIdentity,
applyCustomImageTemplate: input.applyCustomImageTemplate ?? false,
requestedExpiresAt: input.requestedExpiresAt ?? null,
assertCompanyBinding: input.assertCompanyBinding,
@@ -0,0 +1,53 @@
import type { Environment, SandboxEnvironmentConfig } from "@paperclipai/shared";
import { fingerprintEnvironmentSandboxProviderConfig, SANDBOX_PLATFORM_PROBE_METADATA_KEY } from "./environment-custom-image-runtime.js";
export type SandboxTemplateIdentity = { id: string; capturedAt: string | null; templateKind: string; bindingField: string } | null;
export type SandboxSnapshotIdentity = {
provider: "daytona";
requestedRef: string;
id: string;
name: string;
organizationId: string;
createdAt: string;
};
export function readSnapshotIdentity(value: unknown, reference: unknown): SandboxSnapshotIdentity | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const identity = value as Record<string, unknown>;
if (identity.provider !== "daytona" || identity.requestedRef !== reference
|| typeof identity.id !== "string" || !/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(identity.id)
|| typeof identity.name !== "string" || !identity.name.trim()
|| (reference !== identity.id && reference !== identity.name)
|| typeof identity.organizationId !== "string" || !identity.organizationId.trim()
|| typeof identity.createdAt !== "string" || !Number.isFinite(Date.parse(identity.createdAt))) return null;
return identity as SandboxSnapshotIdentity;
}
export function isQualifiableDaytonaSnapshot(
input: SandboxEnvironmentConfig, template: SandboxTemplateIdentity,
): input is SandboxEnvironmentConfig & Record<string, unknown> & { snapshot: string } {
const config = input as unknown as Record<string, unknown>;
return config.provider === "daytona" && typeof config.snapshot === "string" && !!config.snapshot.trim()
&& !config.image && !config.template && !config.templateRef
&& (!template || (template.templateKind === "snapshot" && template.bindingField === "snapshot"));
}
/** Discovery reads only saved, company-scoped proof. Execution pins the same
* provider ID rather than resolving the mutable name again. A new config or
* captured template needs a new probe; deleting the pinned snapshot fails at
* the provider instead of silently selecting its replacement. */
export function qualifiedSandboxSnapshotId(
environment: Environment, companyId: string, config: SandboxEnvironmentConfig, template: SandboxTemplateIdentity,
): string | null {
if (!isQualifiableDaytonaSnapshot(config, template)) return null;
const proofs = environment.metadata?.[SANDBOX_PLATFORM_PROBE_METADATA_KEY];
if (!proofs || typeof proofs !== "object" || Array.isArray(proofs)) return null;
const proof = (proofs as Record<string, unknown>)[companyId];
if (!proof || typeof proof !== "object" || Array.isArray(proof)) return null;
const value = proof as Record<string, unknown>;
if (value.v !== 2 || value.companyId !== companyId
|| value.baseConfigFingerprint !== fingerprintEnvironmentSandboxProviderConfig(environment.config as SandboxEnvironmentConfig)
|| value.runtimeConfigFingerprint !== fingerprintEnvironmentSandboxProviderConfig(config)
|| value.snapshot !== config.snapshot || JSON.stringify(value.template) !== JSON.stringify(template)) return null;
return readSnapshotIdentity(value.snapshotIdentity, config.snapshot)?.id ?? null;
}
+2 -2
View File
@@ -1974,10 +1974,10 @@ describe("OnboardingWizard restore-gate (stale localStorage across accounts)", (
return { root, queryClient };
}
it.each(["paperclip", "legacy"] as const)("qualifies managed Claude during Connect and tests/hire with measured %s configuration", async measured => {
it.each([["paperclip", "image"], ["legacy", "image"], ["paperclip", "snapshot"], ["legacy", "snapshot"]] as const)("qualifies managed Claude during Connect and tests/hire with measured %s configuration from %s", async (measured, source) => {
mockAdapterBuild.buildAdapterConfig.mockImplementation(values => buildClaudeLocalConfig(values!));
mockEnvironmentsApi.list.mockResolvedValue([{ ...SANDBOX_ENVIRONMENT, config: {
provider: "daytona", image: `registry.example.test/runtime@sha256:${"a".repeat(64)}`,
provider: "daytona", ...(source === "snapshot" ? { snapshot: "managed-snapshot" } : { image: `registry.example.test/runtime@sha256:${"a".repeat(64)}` }),
} }]);
const discovery = (runner: "paperclip" | "legacy") => [
setupAdapterInfo({ type: "claude_local", loaded: true, disabled: false, defaultRunner: runner, supportedRunners: [runner] }),
+6
View File
@@ -19,6 +19,12 @@ function setup() {
}
describe("Claude explicit setup qualification", () => {
it("qualifies a named Daytona snapshot through the normal Connect action", async () => {
const s = setup();
const input: Parameters<typeof qualifyClaudeSetup>[0] = { ...s, environment: { driver: "sandbox", config: { provider: "daytona", snapshot: "managed-snapshot" } } };
expect((await qualifyClaudeSetup(input))?.runner).toBe("paperclip");
expect(s.probeEnvironment).toHaveBeenCalledTimes(1);
});
it.each(["paperclip", "legacy"] as const)("uses fresh discovery and honors measured %s selection", async runner => {
const s = setup();
s.readAdapters.mockResolvedValueOnce(discovery(runner));
+4 -3
View File
@@ -8,10 +8,11 @@ export function needsClaudeSetupQualification(input: {
environment?: Pick<Environment, "driver" | "config"> | null;
}): boolean {
const config = input.environment?.config;
const immutableImage = typeof config?.image === "string" && /^[^\s@]+@sha256:[a-f0-9]{64}$/.test(config.image) && !config.snapshot;
const daytonaSnapshot = config?.provider === "daytona" && typeof config.snapshot === "string" && !!config.snapshot.trim() && !config.image;
return !input.savedAgentId && input.adapterType === "claude_local" && input.runner === "auto"
&& input.environment?.driver === "sandbox" && typeof config?.image === "string"
&& /^[^\s@]+@sha256:[a-f0-9]{64}$/.test(config.image)
&& !config.snapshot && !config.template && !config.templateRef;
&& input.environment?.driver === "sandbox" && (immutableImage || daytonaSnapshot)
&& !config?.template && !config?.templateRef;
}
export async function qualifyClaudeSetup(input: Parameters<typeof needsClaudeSetupQualification>[0] & {
+5 -4
View File
@@ -10,11 +10,12 @@ import {
describe("New agent setup", () => {
it.each([
["paperclip", "Connect"], ["legacy", "Connect"],
["paperclip", "Finish setup"], ["legacy", "Finish setup"],
] as const)("qualifies new managed Claude and saves measured %s defaults through %s", async (measured, step) => {
["paperclip", "Connect", "image"], ["legacy", "Connect", "image"],
["paperclip", "Finish setup", "image"], ["legacy", "Finish setup", "image"],
["paperclip", "Connect", "snapshot"], ["paperclip", "Finish setup", "snapshot"],
] as const)("qualifies new managed Claude and saves measured %s defaults through %s from %s", async (measured, step, source) => {
const image = `registry.example.test/runtime@sha256:${"a".repeat(64)}`;
envApi.list.mockResolvedValue([{ id: "sandbox-1", driver: "sandbox", config: { provider: "daytona", image } }]);
envApi.list.mockResolvedValue([{ id: "sandbox-1", driver: "sandbox", config: { provider: "daytona", ...(source === "snapshot" ? { snapshot: "managed-snapshot" } : { image }) } }]);
settings.get.mockResolvedValue({ defaultEnvironmentId: "sandbox-1" });
settings.getExperimental.mockResolvedValue({ enableManagedSandboxOnly: true, enableNativeRunner: false });
api.getClaudeOAuthTokenStatus.mockResolvedValue({ secretId: "saved-oauth", latestVersion: 1 });