mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-06 22:54:18 +01:00
Qualify Claude defaults against pinned Daytona snapshot identities
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
2e107b341f
commit
7bd7699c9c
18 files changed
+325
-31
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user