@tanstack/ai-sandbox 0.3.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +26 -0
  2. package/dist/esm/approvals.js +5 -4
  3. package/dist/esm/approvals.js.map +1 -1
  4. package/dist/esm/bridge-events.js +7 -6
  5. package/dist/esm/bridge-events.js.map +1 -1
  6. package/dist/esm/checkpoint-store.d.ts +147 -0
  7. package/dist/esm/checkpoint-store.js +267 -0
  8. package/dist/esm/checkpoint-store.js.map +1 -0
  9. package/dist/esm/chunk-identity.d.ts +0 -17
  10. package/dist/esm/chunk-identity.js +28 -5
  11. package/dist/esm/chunk-identity.js.map +1 -1
  12. package/dist/esm/contracts.d.ts +19 -0
  13. package/dist/esm/index.d.ts +11 -1
  14. package/dist/esm/index.js +12 -7
  15. package/dist/esm/memory-snapshot-types.d.ts +129 -0
  16. package/dist/esm/memory-snapshots.d.ts +6 -0
  17. package/dist/esm/memory-snapshots.js +490 -0
  18. package/dist/esm/memory-snapshots.js.map +1 -0
  19. package/dist/esm/middleware.d.ts +33 -1
  20. package/dist/esm/middleware.js +339 -94
  21. package/dist/esm/middleware.js.map +1 -1
  22. package/dist/esm/ngrok.d.ts +1 -1
  23. package/dist/esm/sandbox.d.ts +16 -0
  24. package/dist/esm/sandbox.js +62 -9
  25. package/dist/esm/sandbox.js.map +1 -1
  26. package/dist/esm/snapshot-operations.d.ts +65 -0
  27. package/dist/esm/snapshot-operations.js +317 -0
  28. package/dist/esm/snapshot-operations.js.map +1 -0
  29. package/dist/esm/snapshot-tools.d.ts +185 -0
  30. package/dist/esm/snapshot-tools.js +160 -0
  31. package/dist/esm/snapshot-tools.js.map +1 -0
  32. package/dist/esm/snapshots.d.ts +51 -0
  33. package/dist/esm/snapshots.js +350 -0
  34. package/dist/esm/snapshots.js.map +1 -0
  35. package/dist/esm/testkit/checkpoint-conformance.d.ts +2 -0
  36. package/dist/esm/testkit/checkpoint-conformance.js +453 -0
  37. package/dist/esm/testkit/checkpoint-conformance.js.map +1 -0
  38. package/dist/esm/testkit/checkpoint-fork-conformance.d.ts +18 -0
  39. package/dist/esm/testkit/checkpoint-fork-conformance.js +191 -0
  40. package/dist/esm/testkit/checkpoint-fork-conformance.js.map +1 -0
  41. package/dist/esm/testkit/conformance.d.ts +4 -0
  42. package/dist/esm/testkit/conformance.js +3 -1
  43. package/dist/esm/testkit/conformance.js.map +1 -1
  44. package/dist/esm/tool-history.js +4 -5
  45. package/dist/esm/tool-history.js.map +1 -1
  46. package/package.json +8 -3
  47. package/skills/ai-sandbox/SKILL.md +96 -8
  48. package/src/approvals.ts +13 -12
  49. package/src/bridge-events.ts +16 -10
  50. package/src/checkpoint-store.ts +652 -0
  51. package/src/chunk-identity.ts +36 -5
  52. package/src/contracts.ts +12 -0
  53. package/src/index.ts +56 -0
  54. package/src/memory-snapshot-types.ts +167 -0
  55. package/src/memory-snapshots.ts +936 -0
  56. package/src/middleware.ts +610 -160
  57. package/src/sandbox.ts +107 -6
  58. package/src/snapshot-operations.ts +540 -0
  59. package/src/snapshot-tools.ts +208 -0
  60. package/src/snapshots.ts +711 -0
  61. package/src/testkit/checkpoint-conformance.ts +472 -0
  62. package/src/testkit/checkpoint-fork-conformance.ts +299 -0
  63. package/src/testkit/conformance.ts +7 -0
  64. package/src/tool-history.ts +9 -13
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshots.js","names":[],"sources":["../../src/snapshots.ts"],"sourcesContent":["import type { SandboxHandle, SandboxFsStat } from './contracts'\nimport type {\n SandboxSnapshotArtifact,\n SandboxSnapshotEntry,\n} from './checkpoint-store'\nimport type { MemoryArtifactRecord as ArtifactRecord } from './memory-snapshot-types'\n\ntype SnapshotBlobStore = {\n get: (key: string) => Promise<{\n arrayBuffer: () => Promise<ArrayBuffer>\n } | null>\n head: (key: string) => Promise<unknown>\n put: (key: string, body: Uint8Array) => Promise<unknown>\n}\n\nexport interface SandboxSnapshotPolicy {\n /** Exact workspace projection hash, when known. */\n workspaceHash?: string\n include?: (path: string, kind: 'file' | 'dir') => boolean\n exclude?: (path: string, kind: 'file' | 'dir') => boolean\n redact?: (input: {\n path: string\n bytes: Uint8Array\n resolvedSecrets: Readonly<Record<string, string>>\n }) => Uint8Array\n}\n\nexport interface SandboxSnapshotBundle {\n blobs: SnapshotBlobStore\n /** Internal resolved workspace root. */\n workspaceRoot?: string\n /** Internal persistence stores used to capture immutable artifact bytes. */\n artifacts?: {\n listForThread: (threadId: string) => Promise<ReadonlyArray<ArtifactRecord>>\n }\n resolveArtifactBlobKey?: (record: ArtifactRecord) => string\n}\n\nexport type SandboxSnapshotErrorCode =\n | 'SANDBOX_SNAPSHOT_INVALID_TOOL_INPUT'\n | 'SANDBOX_SNAPSHOT_MISSING_SANDBOX'\n | 'SANDBOX_SNAPSHOT_MISSING_INSTANCES'\n | 'SANDBOX_SNAPSHOT_MISSING_PERSISTENCE_STORES'\n | 'SANDBOX_SNAPSHOT_MISSING_REUSABLE_SANDBOX'\n | 'SANDBOX_SNAPSHOT_REUSE_NONE'\n | 'SANDBOX_SNAPSHOT_MISSING_CHECKPOINT'\n | 'SANDBOX_SNAPSHOT_MISSING_CHECKPOINT_ARTIFACT'\n | 'SANDBOX_SNAPSHOT_FOREIGN_CHECKPOINT_ARTIFACT'\n | 'SANDBOX_SNAPSHOT_INVALID_ARTIFACT_BYTES'\n | 'SANDBOX_SNAPSHOT_FORK_UNAVAILABLE'\n | 'SANDBOX_SNAPSHOT_INVALID_PATH'\n | 'SANDBOX_SNAPSHOT_INVALID_WORKSPACE'\n | 'SANDBOX_SNAPSHOT_LSTAT_REQUIRED'\n | 'SANDBOX_SNAPSHOT_UNSUPPORTED_ENTRY'\n | 'SANDBOX_SNAPSHOT_MISSING_BLOB'\n | 'SANDBOX_SNAPSHOT_INVALID_BLOB'\n | 'SANDBOX_SNAPSHOT_ARTIFACT_SUPPORT_REQUIRED'\n | 'SANDBOX_SNAPSHOT_MISSING_ARTIFACT_BLOB'\n\nexport class SandboxSnapshotError extends Error {\n readonly code: SandboxSnapshotErrorCode\n constructor(code: SandboxSnapshotErrorCode, message: string) {\n super(message)\n this.name = 'SandboxSnapshotError'\n this.code = code\n }\n}\n\nconst DEFAULT_ROOT = '/workspace'\nconst PROJECTED_SKILL_ROOTS = new Set(['.claude', '.codex', '.grok'])\n\nfunction isFrameworkGeneratedSymlinkPath(path: string): boolean {\n if (path === 'CLAUDE.md' || path === 'GEMINI.md') return true\n const segments = path.split('/')\n return (\n segments.length === 3 &&\n PROJECTED_SKILL_ROOTS.has(segments[0] ?? '') &&\n segments[1] === 'skills'\n )\n}\n\nfunction defaultExcluded(path: string, workspaceHash?: string): boolean {\n const segments = path.split('/')\n return (\n isFrameworkGeneratedSymlinkPath(path) ||\n segments.some(\n (segment) =>\n segment === '.git' ||\n segment === 'node_modules' ||\n segment.startsWith('.env'),\n ) ||\n (workspaceHash !== undefined &&\n segments[0] === `.tanstack-projected-${workspaceHash}`)\n )\n}\n\nfunction isProtectedPath(path: string, workspaceHash?: string): boolean {\n return (\n workspaceHash !== undefined &&\n path.split('/')[0] === `.tanstack-projected-${workspaceHash}`\n )\n}\n\nconst FILE_BLOB_KEY = /^sandbox-files\\/sha256\\/[0-9a-f]{64}$/\n\nexport function defaultSandboxSnapshotPolicy(\n workspaceHash?: string,\n): SandboxSnapshotPolicy {\n return {\n workspaceHash,\n exclude: (path) => defaultExcluded(path, workspaceHash),\n }\n}\n\n/**\n * Keep default exclusions unless the caller passed `exclude`.\n * `include` or `redact` alone must not capture `.env`, `.git`, or\n * `node_modules`.\n */\nexport function resolveSandboxSnapshotPolicy(\n supplied: SandboxSnapshotPolicy | undefined,\n workspaceHash?: string,\n): SandboxSnapshotPolicy {\n const defaults = defaultSandboxSnapshotPolicy(\n workspaceHash ?? supplied?.workspaceHash,\n )\n if (supplied === undefined) return defaults\n const include = supplied.include\n const exclude = supplied.exclude\n const redact = supplied.redact\n const suppliedWorkspaceHash = supplied.workspaceHash\n return {\n ...(suppliedWorkspaceHash === undefined\n ? {}\n : { workspaceHash: suppliedWorkspaceHash }),\n ...(workspaceHash === undefined ? {} : { workspaceHash }),\n ...(include === undefined ? {} : { include }),\n exclude: exclude ?? defaults.exclude,\n ...(redact === undefined ? {} : { redact }),\n }\n}\n\nfunction normalize(path: string): string {\n if (path.includes('\\\\'))\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_PATH',\n `Unsafe snapshot path '${path}'`,\n )\n const value = path\n if (\n !value ||\n value.includes('\\0') ||\n value.startsWith('/') ||\n /^[A-Za-z]:/.test(value) ||\n value.endsWith('/')\n )\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_PATH',\n `Unsafe snapshot path '${path}'`,\n )\n const parts = value.split('/')\n if (\n parts.some((part) => !part || part === '.' || part === '..') ||\n parts.join('/') !== value\n )\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_PATH',\n `Unsafe snapshot path '${path}'`,\n )\n return value\n}\n\nfunction childPath(\n parent: string,\n child: { name: string; path: string },\n): { absolute: string; relative: string } {\n if (\n !child.name ||\n child.name.includes('/') ||\n child.name.includes('\\\\') ||\n child.name.includes('\\0') ||\n child.name === '.' ||\n child.name === '..'\n )\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_WORKSPACE',\n `Invalid workspace entry '${child.name}'`,\n )\n const absolute = `${parent}/${child.name}`\n if (child.path !== absolute)\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_WORKSPACE',\n `Invalid workspace entry path '${child.path}'`,\n )\n return { absolute, relative: child.name }\n}\n\nfunction lstat(\n handle: SandboxHandle,\n path: string,\n): Promise<SandboxFsStat | undefined> {\n if (!handle.fs.lstat)\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_LSTAT_REQUIRED',\n 'Snapshot operations require fs.lstat',\n )\n return handle.fs.lstat(path)\n}\n\nfunction assertSupported(stat: SandboxFsStat, path: string): void {\n if (\n stat.type === 'symlink' ||\n stat.type === 'other' ||\n (stat.type === 'file' && (stat.mode & 0o111) !== 0)\n )\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_UNSUPPORTED_ENTRY',\n `Unsupported entry '${path}'`,\n )\n}\n\nfunction included(\n path: string,\n kind: 'file' | 'dir',\n policy: SandboxSnapshotPolicy,\n): boolean {\n if (policy.exclude?.(path, kind)) return false\n return kind === 'dir' ? true : (policy.include?.(path, kind) ?? true)\n}\n\nasync function hash(bytes: Uint8Array): Promise<string> {\n // TypeScript requires an ArrayBuffer-backed view; Uint8Array can also use SharedArrayBuffer.\n const digest = await crypto.subtle.digest('SHA-256', new Uint8Array(bytes))\n return Array.from(new Uint8Array(digest), (byte) =>\n byte.toString(16).padStart(2, '0'),\n ).join('')\n}\n\nasync function putIfAbsent(\n blobs: SnapshotBlobStore,\n bytes: Uint8Array,\n keys: Map<string, string>,\n): Promise<{ key: string; size: number }> {\n const key = `sandbox-files/sha256/${await hash(bytes)}`\n if (!keys.has(key)) {\n if (!(await blobs.head(key))) await blobs.put(key, bytes)\n keys.set(key, key)\n }\n return { key, size: bytes.byteLength }\n}\n\nasync function redactBytes(\n bytes: Uint8Array,\n resolvedSecrets: Readonly<Record<string, string>>,\n): Promise<Uint8Array> {\n const output = bytes.slice()\n const redacted = new Uint8Array(bytes.length)\n const secrets = Object.values(resolvedSecrets)\n .filter(Boolean)\n .map((secret) => new TextEncoder().encode(secret))\n .sort((a, b) => b.length - a.length || compareBytes(a, b))\n for (const needle of secrets) {\n if (!needle.length || needle.length > bytes.length) continue\n for (let start = 0; start <= bytes.length - needle.length; start++) {\n let match = true\n for (let index = 0; index < needle.length; index++)\n if (bytes[start + index] !== needle[index]) {\n match = false\n break\n }\n if (!match) continue\n if (match) redacted.fill(1, start, start + needle.length)\n }\n }\n for (let index = 0; index < output.length; index++)\n if (redacted[index]) output[index] = 0\n return output\n}\n\nexport async function captureSandboxFiles(\n handle: SandboxHandle,\n bundle: SandboxSnapshotBundle,\n suppliedPolicy: SandboxSnapshotPolicy = defaultSandboxSnapshotPolicy(),\n resolvedSecrets: Readonly<Record<string, string>> = {},\n): Promise<{ files: Array<SandboxSnapshotEntry> }> {\n const policy = resolveSandboxSnapshotPolicy(\n suppliedPolicy,\n suppliedPolicy.workspaceHash,\n )\n if (!handle.fs.lstat)\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_LSTAT_REQUIRED',\n 'Snapshot capture requires fs.lstat',\n )\n const rootPath = bundle.workspaceRoot ?? DEFAULT_ROOT\n const root = await lstat(handle, rootPath)\n if (!root || root.type !== 'dir')\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_WORKSPACE',\n 'Snapshot workspace is missing or is not a directory',\n )\n assertSupported(root, rootPath)\n const files: Array<SandboxSnapshotEntry> = []\n const destinationKeys = new Map<string, string>()\n const walk = async (absolute: string, relative: string): Promise<boolean> => {\n const stat = await lstat(handle, absolute)\n if (!stat)\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_WORKSPACE',\n `Snapshot entry disappeared '${relative}'`,\n )\n assertSupported(stat, relative)\n if (stat.type !== 'file' && stat.type !== 'dir')\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_UNSUPPORTED_ENTRY',\n `Unsupported entry '${relative}'`,\n )\n if (\n relative &&\n (isProtectedPath(relative, policy.workspaceHash) ||\n policy.exclude?.(relative, stat.type))\n )\n return false\n if (stat.type === 'file') {\n const path = normalize(relative)\n if (policy.include && !policy.include(path, 'file')) return false\n let bytes = await handle.fs.readBytes(absolute)\n if (policy.redact) bytes = policy.redact({ path, bytes, resolvedSecrets })\n bytes = await redactBytes(bytes, resolvedSecrets)\n const blob = await putIfAbsent(bundle.blobs, bytes, destinationKeys)\n files.push({ path, kind: 'file', blobKey: blob.key, size: blob.size })\n return true\n }\n const children = await handle.fs.list(absolute)\n let hasCapturedChild = false\n for (const child of children) {\n const childEntry = childPath(absolute, child)\n const childRelative = relative\n ? `${relative}/${childEntry.relative}`\n : childEntry.relative\n if (\n isProtectedPath(childRelative, policy.workspaceHash) ||\n policy.exclude?.(childRelative, child.type)\n )\n continue\n hasCapturedChild =\n (await walk(childEntry.absolute, childRelative)) || hasCapturedChild\n }\n if (\n relative &&\n !hasCapturedChild &&\n (!policy.include || policy.include(relative, 'dir'))\n )\n files.push({ path: normalize(relative), kind: 'dir' })\n return (\n hasCapturedChild ||\n (relative !== '' && (!policy.include || policy.include(relative, 'dir')))\n )\n }\n await walk(rootPath, '')\n files.sort((a, b) => comparePath(a.path, b.path))\n return { files }\n}\n\ntype PlannedEntry = SandboxSnapshotEntry & { path: string }\ntype PlannedFile = Extract<PlannedEntry, { kind: 'file' }>\n\nfunction validateManifest(\n snapshot: {\n files: ReadonlyArray<SandboxSnapshotEntry>\n },\n policy: SandboxSnapshotPolicy,\n): Array<PlannedEntry> {\n const paths = new Map<string, PlannedEntry>()\n for (const entry of snapshot.files) {\n const path = normalize(entry.path)\n for (const ancestor of parents(path)) {\n if (\n isProtectedPath(ancestor, policy.workspaceHash) ||\n policy.exclude?.(ancestor, 'dir')\n )\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_PATH',\n `Excluded snapshot ancestor '${ancestor}'`,\n )\n }\n if (isProtectedPath(path, policy.workspaceHash))\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_PATH',\n `Protected snapshot path '${path}'`,\n )\n if (\n !included(path, entry.kind, policy) ||\n (entry.kind === 'dir' && policy.include?.(path, 'dir') === false)\n )\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_PATH',\n `Excluded snapshot path '${path}'`,\n )\n if (paths.has(path))\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_PATH',\n `Duplicate path '${path}'`,\n )\n if (entry.kind === 'file') {\n const { blobKey, size } = entry\n if (\n typeof blobKey !== 'string' ||\n !FILE_BLOB_KEY.test(blobKey) ||\n typeof size !== 'number' ||\n !Number.isSafeInteger(size) ||\n size < 0\n )\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_PATH',\n `Invalid file entry '${path}'`,\n )\n paths.set(path, { path, kind: 'file', blobKey, size })\n } else if (entry.kind === 'dir') paths.set(path, { path, kind: 'dir' })\n else\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_PATH',\n `Unknown snapshot entry '${path}'`,\n )\n }\n for (const [path] of paths)\n for (\n let index = path.indexOf('/');\n index !== -1;\n index = path.indexOf('/', index + 1)\n ) {\n const parent = paths.get(path.slice(0, index))\n if (parent?.kind === 'file')\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_PATH',\n `File ancestor '${parent.path}'`,\n )\n }\n return [...paths.values()]\n}\n\nasync function loadBlobs(\n entries: ReadonlyArray<PlannedEntry>,\n bundle: SandboxSnapshotBundle,\n): Promise<Map<string, Uint8Array>> {\n const blobs = new Map<string, Uint8Array>()\n for (const entry of entries)\n if (entry.kind === 'file' && !blobs.has(entry.blobKey)) {\n const object = await bundle.blobs.get(entry.blobKey)\n if (!object)\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_MISSING_BLOB',\n `Missing snapshot blob '${entry.blobKey}'`,\n )\n const bytes = new Uint8Array(await object.arrayBuffer())\n const expectedKey = `sandbox-files/sha256/${await hash(bytes)}`\n if (entry.blobKey !== expectedKey)\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_BLOB',\n `Invalid content for snapshot blob '${entry.blobKey}'`,\n )\n blobs.set(entry.blobKey, bytes)\n }\n for (const entry of entries)\n if (entry.kind === 'file') {\n const bytes = blobs.get(entry.blobKey)\n if (!bytes || bytes.byteLength !== entry.size)\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_BLOB',\n `Wrong size for snapshot blob '${entry.blobKey}'`,\n )\n }\n return blobs\n}\n\ntype CurrentEntry = { path: string; kind: 'file' | 'dir'; protected?: boolean }\n\nasync function scanCurrent(\n handle: SandboxHandle,\n absolute: string,\n relative: string,\n policy: SandboxSnapshotPolicy,\n): Promise<Array<CurrentEntry>> {\n const stat = await lstat(handle, absolute)\n if (!stat) return []\n assertSupported(stat, relative)\n if (stat.type === 'file') return [{ path: relative, kind: 'file' }]\n const paths: Array<CurrentEntry> = []\n for (const child of await handle.fs.list(absolute)) {\n const childEntry = childPath(absolute, child)\n const childRelative = relative\n ? `${relative}/${childEntry.relative}`\n : childEntry.relative\n if (isProtectedPath(childRelative, policy.workspaceHash)) {\n // Preserve the marker and its parent directories without inspecting the\n // protected tree. The restore planner uses this marker to avoid removing\n // an ancestor directory that contains it.\n paths.push({ path: childRelative, kind: 'dir', protected: true })\n continue\n }\n if (!included(childRelative, child.type, policy)) {\n // Excluded entries are outside the portable snapshot. Keep a protected\n // marker so removing a parent directory cannot remove them either.\n paths.push({ path: childRelative, kind: child.type, protected: true })\n continue\n }\n paths.push(\n ...(await scanCurrent(\n handle,\n childEntry.absolute,\n childRelative,\n policy,\n )),\n )\n }\n return relative ? [{ path: relative, kind: 'dir' }, ...paths] : paths\n}\n\nasync function scanDestination(\n handle: SandboxHandle,\n policy: SandboxSnapshotPolicy,\n rootPath: string,\n): Promise<Array<CurrentEntry>> {\n const root = await lstat(handle, rootPath)\n if (!root || root.type !== 'dir')\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_WORKSPACE',\n 'Snapshot workspace is missing or is not a directory',\n )\n assertSupported(root, rootPath)\n return scanCurrent(handle, rootPath, '', policy)\n}\n\nfunction parents(path: string): Array<string> {\n const values: Array<string> = []\n const parts = path.split('/')\n for (let length = 1; length < parts.length; length++)\n values.push(parts.slice(0, length).join('/'))\n return values\n}\n\nfunction comparePath(a: string, b: string): number {\n return compareBytes(new TextEncoder().encode(a), new TextEncoder().encode(b))\n}\n\nfunction compareBytes(a: Uint8Array, b: Uint8Array): number {\n for (let i = 0; i < Math.min(a.length, b.length); i++) {\n const left = a[i]\n const right = b[i]\n if (left !== right) return (left ?? 0) - (right ?? 0)\n }\n return a.length - b.length\n}\n\nfunction getRequiredBlob(\n blobs: ReadonlyMap<string, Uint8Array>,\n key: string,\n): Uint8Array {\n const bytes = blobs.get(key)\n if (!bytes) {\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_MISSING_BLOB',\n `Missing snapshot blob '${key}'`,\n )\n }\n return bytes\n}\n\nfunction depth(path: string): number {\n return path.split('/').length\n}\n\nfunction buildRestorePlan(\n entries: ReadonlyArray<PlannedEntry>,\n current: ReadonlyArray<CurrentEntry>,\n): {\n removes: Array<string>\n mkdirs: Array<string>\n writes: Array<PlannedFile>\n} {\n const desired = new Map<string, 'file' | 'dir'>()\n for (const entry of entries) {\n desired.set(entry.path, entry.kind)\n for (const parent of parents(entry.path)) desired.set(parent, 'dir')\n }\n const currentKinds = new Map(current.map((entry) => [entry.path, entry.kind]))\n const candidates = current\n .filter((entry) => desired.get(entry.path) !== entry.kind)\n .map((entry) => entry.path)\n .sort((a, b) => depth(a) - depth(b) || comparePath(a, b))\n const removes: Array<string> = []\n for (const path of candidates) {\n const containsProtectedEntry = current.some(\n (entry) =>\n entry.protected &&\n (entry.path === path || entry.path.startsWith(`${path}/`)),\n )\n if (containsProtectedEntry) continue\n if (removes.some((ancestor) => path.startsWith(`${ancestor}/`))) continue\n removes.push(path)\n }\n const mkdirs = [...desired]\n .filter(\n ([path, kind]) => kind === 'dir' && currentKinds.get(path) !== 'dir',\n )\n .map(([path]) => path)\n .sort((a, b) => depth(a) - depth(b) || comparePath(a, b))\n const writes = entries\n .filter((entry): entry is PlannedFile => entry.kind === 'file')\n .sort((a, b) => comparePath(a.path, b.path))\n return { removes, mkdirs, writes }\n}\n\nexport async function restoreSandboxFiles(\n handle: SandboxHandle,\n bundle: SandboxSnapshotBundle,\n snapshot: { files: ReadonlyArray<SandboxSnapshotEntry> },\n suppliedPolicy: SandboxSnapshotPolicy = defaultSandboxSnapshotPolicy(),\n): Promise<void> {\n const policy = resolveSandboxSnapshotPolicy(\n suppliedPolicy,\n suppliedPolicy.workspaceHash,\n )\n if (!handle.fs.lstat)\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_LSTAT_REQUIRED',\n 'Snapshot restore requires fs.lstat',\n )\n const entries = validateManifest(snapshot, policy)\n const rootPath = bundle.workspaceRoot ?? DEFAULT_ROOT\n const current = await scanDestination(handle, policy, rootPath)\n for (const entry of entries) {\n if (\n entry.kind === 'file' &&\n current.some(\n (currentEntry) =>\n currentEntry.protected &&\n currentEntry.path.startsWith(`${entry.path}/`),\n )\n )\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_INVALID_PATH',\n `Protected current descendant conflicts with '${entry.path}'`,\n )\n }\n const blobs = await loadBlobs(entries, bundle)\n const plan = buildRestorePlan(entries, current)\n // Restore is called only while a new sandbox is private to setup. It is not\n // safe for a caller that allows another process to change the workspace.\n for (const path of plan.removes) await handle.fs.remove(`${rootPath}/${path}`)\n for (const path of plan.mkdirs) await handle.fs.mkdir(`${rootPath}/${path}`)\n for (const entry of plan.writes)\n await handle.fs.write(\n `${rootPath}/${entry.path}`,\n getRequiredBlob(blobs, entry.blobKey),\n )\n}\n\nexport async function captureSandboxArtifacts(\n bundle: SandboxSnapshotBundle,\n threadId: string,\n resolvedSecrets: Readonly<Record<string, string>> = {},\n): Promise<ReadonlyArray<SandboxSnapshotArtifact>> {\n if (!bundle.artifacts)\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_ARTIFACT_SUPPORT_REQUIRED',\n 'Snapshot artifact capture requires an artifact store',\n )\n const records = await bundle.artifacts.listForThread(threadId)\n const loaded = new Map<string, Uint8Array>()\n const destinationKeys = new Map<string, string>()\n const resolveBlobKey =\n bundle.resolveArtifactBlobKey ??\n ((record: ArtifactRecord) =>\n record.blobKey ?? `artifacts/${record.runId}/${record.artifactId}`)\n for (const record of records) {\n const sourceKey = resolveBlobKey(record)\n if (loaded.has(sourceKey)) continue\n const source = await bundle.blobs.get(sourceKey)\n if (!source)\n throw new SandboxSnapshotError(\n 'SANDBOX_SNAPSHOT_MISSING_ARTIFACT_BLOB',\n `Missing artifact source blob '${sourceKey}'`,\n )\n loaded.set(sourceKey, new Uint8Array(await source.arrayBuffer()))\n }\n const output = []\n for (const record of records) {\n const sourceKey = resolveBlobKey(record)\n let bytes = getRequiredBlob(loaded, sourceKey)\n bytes = await redactBytes(bytes, resolvedSecrets)\n const key = `sandbox-artifacts/sha256/${await hash(bytes)}`\n if (!destinationKeys.has(key)) {\n if (!(await bundle.blobs.head(key))) await bundle.blobs.put(key, bytes)\n destinationKeys.set(key, key)\n }\n output.push({\n artifactId: record.artifactId,\n name: record.name,\n mimeType: record.mimeType,\n size: bytes.byteLength,\n blobKey: key,\n createdAt: record.createdAt,\n })\n }\n output.sort(\n (a, b) =>\n a.createdAt - b.createdAt || comparePath(a.artifactId, b.artifactId),\n )\n return Object.freeze(output.map((artifact) => Object.freeze(artifact)))\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { SandboxCheckpointStore, SandboxCheckpointStoreOptions } from '../checkpoint-store.js';
2
+ export declare function runSandboxCheckpointStoreConformance(name: string, makeStore: (options?: SandboxCheckpointStoreOptions) => SandboxCheckpointStore | Promise<SandboxCheckpointStore>, options?: SandboxCheckpointStoreOptions): void;
@@ -0,0 +1,453 @@
1
+ import { describe, expect, it } from "vitest";
2
+ //#region src/testkit/checkpoint-conformance.ts
3
+ var writers = /* @__PURE__ */ new WeakMap();
4
+ async function writerFor(store, threadId) {
5
+ let storeWriters = writers.get(store);
6
+ if (!storeWriters) {
7
+ storeWriters = /* @__PURE__ */ new Map();
8
+ writers.set(store, storeWriters);
9
+ }
10
+ let writer = storeWriters.get(threadId);
11
+ if (!writer) {
12
+ writer = store.acquireWriter(threadId);
13
+ storeWriters.set(threadId, writer);
14
+ }
15
+ return writer;
16
+ }
17
+ async function append(store, input) {
18
+ return store.append({
19
+ ...input,
20
+ writer: await writerFor(store, input.checkpoint.threadId)
21
+ });
22
+ }
23
+ async function deleteHead(store, input) {
24
+ return store.deleteHead({
25
+ ...input,
26
+ writer: await writerFor(store, input.threadId)
27
+ });
28
+ }
29
+ async function expectThreadState(store, staleCheckpointId, expected) {
30
+ expect(await store.getHead("thread-a")).toBe(expected.head);
31
+ expect(await store.list("thread-a")).toEqual(expected.list);
32
+ expect(await store.get(staleCheckpointId)).toEqual(expected.checkpoint);
33
+ expect(await store.listBlobReferences()).toEqual(expected.references);
34
+ }
35
+ function checkpoint(id, parentCheckpointId = null, threadId = "thread-a") {
36
+ const hash = (Number(id.replace(/\D/g, "")) || 1).toString(16).padStart(64, "0");
37
+ return {
38
+ id,
39
+ threadId,
40
+ parentCheckpointId,
41
+ createdAt: Number(id.replace(/\D/g, "")) || 1,
42
+ reason: "automatic",
43
+ files: [{
44
+ path: `${id}.txt`,
45
+ kind: "file",
46
+ blobKey: `sandbox-files/sha256/${hash}`,
47
+ size: 1
48
+ }],
49
+ conversation: [{
50
+ role: "user",
51
+ content: id
52
+ }],
53
+ artifacts: []
54
+ };
55
+ }
56
+ function runSandboxCheckpointStoreConformance(name, makeStore, options) {
57
+ describe(`SandboxCheckpointStore conformance: ${name}`, () => {
58
+ it("returns null and an empty list for an unknown thread", async () => {
59
+ const store = await makeStore(options);
60
+ expect(await store.get("missing")).toBeNull();
61
+ expect(await store.getHead("missing-thread")).toBeNull();
62
+ expect(await store.list("missing-thread")).toEqual([]);
63
+ });
64
+ it("appends a root and then a parent-linked checkpoint", async () => {
65
+ const store = await makeStore(options);
66
+ await expect(append(store, {
67
+ checkpoint: checkpoint("1"),
68
+ expectedHeadId: null
69
+ })).resolves.toEqual({ headId: "1" });
70
+ await expect(append(store, {
71
+ checkpoint: checkpoint("2", "1"),
72
+ expectedHeadId: "1"
73
+ })).resolves.toEqual({ headId: "2" });
74
+ expect(await store.getHead("thread-a")).toBe("2");
75
+ expect((await store.list("thread-a")).map((entry) => entry.id)).toEqual(["1", "2"]);
76
+ });
77
+ it("enforces expected-head and parent compare-and-swap rules", async () => {
78
+ const store = await makeStore(options);
79
+ await append(store, {
80
+ checkpoint: checkpoint("1"),
81
+ expectedHeadId: null
82
+ });
83
+ await expect(append(store, {
84
+ checkpoint: checkpoint("2"),
85
+ expectedHeadId: null
86
+ })).rejects.toMatchObject({ code: "SANDBOX_SNAPSHOT_STALE_HEAD" });
87
+ await expect(append(store, {
88
+ checkpoint: checkpoint("3"),
89
+ expectedHeadId: "1"
90
+ })).rejects.toMatchObject({ code: "SANDBOX_SNAPSHOT_PARENT_MISMATCH" });
91
+ });
92
+ it("rejects duplicate checkpoint ids", async () => {
93
+ const store = await makeStore(options);
94
+ await append(store, {
95
+ checkpoint: checkpoint("1"),
96
+ expectedHeadId: null
97
+ });
98
+ await expect(append(store, {
99
+ checkpoint: checkpoint("1"),
100
+ expectedHeadId: "1"
101
+ })).rejects.toMatchObject({ code: "SANDBOX_SNAPSHOT_DUPLICATE_ID" });
102
+ });
103
+ it("isolates checkpoints and heads by thread", async () => {
104
+ const store = await makeStore(options);
105
+ await append(store, {
106
+ checkpoint: checkpoint("a", null, "thread-a"),
107
+ expectedHeadId: null
108
+ });
109
+ await append(store, {
110
+ checkpoint: checkpoint("b", null, "thread-b"),
111
+ expectedHeadId: null
112
+ });
113
+ expect(await store.getHead("thread-a")).toBe("a");
114
+ expect(await store.getHead("thread-b")).toBe("b");
115
+ expect((await store.list("thread-a")).map((entry) => entry.id)).toEqual(["a"]);
116
+ expect((await store.list("thread-b")).map((entry) => entry.id)).toEqual(["b"]);
117
+ });
118
+ it("lists checkpoints by createdAt and then checkpoint id", async () => {
119
+ const store = await makeStore(options);
120
+ const first = {
121
+ ...checkpoint("z"),
122
+ createdAt: 2
123
+ };
124
+ const second = {
125
+ ...checkpoint("a", "z"),
126
+ createdAt: 1
127
+ };
128
+ const third = {
129
+ ...checkpoint("b", "a"),
130
+ createdAt: 1
131
+ };
132
+ await append(store, {
133
+ checkpoint: first,
134
+ expectedHeadId: null
135
+ });
136
+ await append(store, {
137
+ checkpoint: second,
138
+ expectedHeadId: "z"
139
+ });
140
+ await append(store, {
141
+ checkpoint: third,
142
+ expectedHeadId: "a"
143
+ });
144
+ expect((await store.list("thread-a")).map((entry) => entry.id)).toEqual([
145
+ "a",
146
+ "b",
147
+ "z"
148
+ ]);
149
+ });
150
+ it("orders checkpoint ids and blob keys by UTF-8 bytes", async () => {
151
+ const store = await makeStore(options);
152
+ const ids = [
153
+ "é",
154
+ "a",
155
+ "B",
156
+ "😀"
157
+ ];
158
+ const keys = [
159
+ "1",
160
+ "2",
161
+ "3",
162
+ "4"
163
+ ].map((value) => `sandbox-files/sha256/${value.padStart(64, "0")}`);
164
+ let parent = null;
165
+ for (const [index, id] of ids.entries()) {
166
+ const blobKey = keys[index];
167
+ if (!blobKey) throw new Error(`Missing blob key for '${id}'`);
168
+ await append(store, {
169
+ checkpoint: {
170
+ ...checkpoint(id, parent),
171
+ files: [{
172
+ path: `${id}.txt`,
173
+ kind: "file",
174
+ blobKey,
175
+ size: 1
176
+ }]
177
+ },
178
+ expectedHeadId: parent
179
+ });
180
+ parent = id;
181
+ }
182
+ expect((await store.list("thread-a")).map((entry) => entry.id)).toEqual([
183
+ "B",
184
+ "a",
185
+ "é",
186
+ "😀"
187
+ ]);
188
+ expect((await store.listBlobReferences()).map((entry) => entry.key)).toEqual([
189
+ `sandbox-files/sha256/${"0".repeat(63)}1`,
190
+ `sandbox-files/sha256/${"0".repeat(63)}2`,
191
+ `sandbox-files/sha256/${"0".repeat(63)}3`,
192
+ `sandbox-files/sha256/${"0".repeat(63)}4`
193
+ ]);
194
+ });
195
+ it("rejects malformed Unicode checkpoint ids and blob keys", async () => {
196
+ const store = await makeStore(options);
197
+ const malformed = "\ud800";
198
+ await expect(append(store, {
199
+ checkpoint: checkpoint(malformed),
200
+ expectedHeadId: null
201
+ })).rejects.toMatchObject({ code: "SANDBOX_SNAPSHOT_INVALID_ID" });
202
+ await expect(append(store, {
203
+ checkpoint: {
204
+ ...checkpoint("root"),
205
+ files: [{
206
+ path: "root.txt",
207
+ kind: "file",
208
+ blobKey: `sandbox-files/sha256/${malformed}${"0".repeat(63)}`,
209
+ size: 1
210
+ }]
211
+ },
212
+ expectedHeadId: null
213
+ })).rejects.toMatchObject({ code: "SANDBOX_SNAPSHOT_INVALID_ENTRY" });
214
+ });
215
+ it.each([
216
+ NaN,
217
+ Number.POSITIVE_INFINITY,
218
+ Number.NEGATIVE_INFINITY
219
+ ])("rejects non-finite createdAt without changing state: %s", async (createdAt) => {
220
+ const store = await makeStore(options);
221
+ const before = {
222
+ head: await store.getHead("thread-a"),
223
+ list: await store.list("thread-a"),
224
+ references: await store.listBlobReferences()
225
+ };
226
+ await expect(append(store, {
227
+ checkpoint: {
228
+ ...checkpoint("invalid"),
229
+ createdAt
230
+ },
231
+ expectedHeadId: null
232
+ })).rejects.toMatchObject({ code: "SANDBOX_SNAPSHOT_INVALID_ENTRY" });
233
+ expect(await store.getHead("thread-a")).toBe(before.head);
234
+ expect(await store.list("thread-a")).toEqual(before.list);
235
+ expect(await store.listBlobReferences()).toEqual(before.references);
236
+ });
237
+ it("keeps checkpoints immutable and reference counts distinct blob keys", async () => {
238
+ const store = await makeStore(options);
239
+ const value = {
240
+ ...checkpoint("1"),
241
+ files: [...checkpoint("1").files, {
242
+ path: "same.txt",
243
+ kind: "file",
244
+ blobKey: `sandbox-files/sha256/${"0".repeat(63)}1`,
245
+ size: 1
246
+ }]
247
+ };
248
+ await append(store, {
249
+ checkpoint: value,
250
+ expectedHeadId: null
251
+ });
252
+ const inputFile = value.files[0];
253
+ if (!inputFile) throw new Error("Expected input file");
254
+ inputFile.path = "input-mutated";
255
+ const loaded = await store.get("1");
256
+ expect(loaded).not.toBe(value);
257
+ if (!loaded) throw new Error("Expected stored checkpoint");
258
+ const loadedFile = loaded.files[0];
259
+ if (!loadedFile) throw new Error("Expected stored file");
260
+ expect(loadedFile.path).toBe("1.txt");
261
+ loadedFile.path = "changed";
262
+ expect((await store.get("1"))?.files[0]?.path).toBe("1.txt");
263
+ const listedFile = (await store.list("thread-a"))[0]?.files[0];
264
+ if (!listedFile) throw new Error("Expected listed file");
265
+ listedFile.path = "list-mutated";
266
+ expect((await store.list("thread-a"))[0]?.files[0]?.path).toBe("1.txt");
267
+ expect(await store.listBlobReferences()).toEqual([{
268
+ key: `sandbox-files/sha256/${"0".repeat(63)}1`,
269
+ references: 1
270
+ }]);
271
+ });
272
+ it("counts one reference per checkpoint for shared blob keys", async () => {
273
+ const store = await makeStore(options);
274
+ const shared = `sandbox-files/sha256/${"f".repeat(64)}`;
275
+ await append(store, {
276
+ checkpoint: {
277
+ ...checkpoint("1"),
278
+ files: [{
279
+ path: "a",
280
+ kind: "file",
281
+ blobKey: shared,
282
+ size: 1
283
+ }]
284
+ },
285
+ expectedHeadId: null
286
+ });
287
+ await append(store, {
288
+ checkpoint: {
289
+ ...checkpoint("2", "1"),
290
+ files: [{
291
+ path: "b",
292
+ kind: "file",
293
+ blobKey: shared,
294
+ size: 1
295
+ }]
296
+ },
297
+ expectedHeadId: "1"
298
+ });
299
+ expect(await store.listBlobReferences()).toEqual([{
300
+ key: shared,
301
+ references: 2
302
+ }]);
303
+ await deleteHead(store, {
304
+ threadId: "thread-a",
305
+ checkpointId: "2"
306
+ });
307
+ expect(await store.listBlobReferences()).toEqual([{
308
+ key: shared,
309
+ references: 1
310
+ }]);
311
+ });
312
+ it("allows one non-waiting writer lease per thread", async () => {
313
+ const store = await makeStore(options);
314
+ const lease = await store.acquireWriter("thread-a");
315
+ try {
316
+ await expect(store.acquireWriter("thread-a")).rejects.toMatchObject({ code: "SANDBOX_SNAPSHOT_WRITER_CONFLICT" });
317
+ const secondLease = await store.acquireWriter("thread-b");
318
+ try {
319
+ expect(secondLease).toBeDefined();
320
+ } finally {
321
+ await secondLease.release();
322
+ }
323
+ } finally {
324
+ await lease.release();
325
+ }
326
+ await (await store.acquireWriter("thread-a")).release();
327
+ });
328
+ it("extends a lease when renew succeeds", async () => {
329
+ const config = options ?? {};
330
+ let current = config.now?.() ?? 1e3;
331
+ const lease = await (await makeStore({
332
+ ...config,
333
+ now: () => current
334
+ })).acquireWriter("thread-a");
335
+ const originalExpiry = lease.expiresAt;
336
+ current += lease.renewAfterMs;
337
+ await expect(lease.renew()).resolves.toEqual({ expiresAt: current + (config.leaseDurationMs ?? 12e4) });
338
+ expect(lease.expiresAt).toBeGreaterThan(originalExpiry);
339
+ await lease.release();
340
+ });
341
+ it("does not let a stale lease release a replacement lease", async () => {
342
+ const config = options ?? {};
343
+ let current = config.now?.() ?? 1e3;
344
+ const store = await makeStore({
345
+ ...config,
346
+ now: () => current
347
+ });
348
+ const old = await store.acquireWriter("thread-a");
349
+ current += (config.leaseDurationMs ?? 12e4) + 1;
350
+ const replacement = await store.acquireWriter("thread-a");
351
+ await old.release();
352
+ await expect(store.acquireWriter("thread-a")).rejects.toMatchObject({ code: "SANDBOX_SNAPSHOT_WRITER_CONFLICT" });
353
+ await replacement.release();
354
+ });
355
+ it("rejects stale leases after expiry takeover without changing state", async () => {
356
+ const config = options ?? {};
357
+ let current = config.now?.() ?? Date.now();
358
+ const takeoverStore = await makeStore({
359
+ ...config,
360
+ now: () => current
361
+ });
362
+ const old = await takeoverStore.acquireWriter("thread-a");
363
+ await takeoverStore.append({
364
+ checkpoint: checkpoint("root"),
365
+ expectedHeadId: null,
366
+ writer: old
367
+ });
368
+ current += (config.leaseDurationMs ?? 12e4) + 1;
369
+ const beforeExpiry = {
370
+ head: await takeoverStore.getHead("thread-a"),
371
+ list: await takeoverStore.list("thread-a"),
372
+ checkpoint: await takeoverStore.get("root"),
373
+ references: await takeoverStore.listBlobReferences()
374
+ };
375
+ await expect(old.renew()).rejects.toMatchObject({ code: "SANDBOX_SNAPSHOT_WRITER_LOST" });
376
+ await expect(takeoverStore.append({
377
+ checkpoint: checkpoint("stale", "root"),
378
+ expectedHeadId: "root",
379
+ writer: old
380
+ })).rejects.toMatchObject({ code: "SANDBOX_SNAPSHOT_WRITER_LOST" });
381
+ await expectThreadState(takeoverStore, "stale", {
382
+ ...beforeExpiry,
383
+ checkpoint: null
384
+ });
385
+ await expect(takeoverStore.deleteHead({
386
+ threadId: "thread-a",
387
+ checkpointId: "root",
388
+ writer: old
389
+ })).rejects.toMatchObject({ code: "SANDBOX_SNAPSHOT_WRITER_LOST" });
390
+ await expectThreadState(takeoverStore, "root", beforeExpiry);
391
+ const next = await takeoverStore.acquireWriter("thread-a");
392
+ expect(next.fence).toBeGreaterThan(old.fence);
393
+ const before = {
394
+ head: await takeoverStore.getHead("thread-a"),
395
+ list: await takeoverStore.list("thread-a"),
396
+ checkpoint: await takeoverStore.get("root"),
397
+ references: await takeoverStore.listBlobReferences()
398
+ };
399
+ await expect(old.renew()).rejects.toMatchObject({ code: "SANDBOX_SNAPSHOT_WRITER_LOST" });
400
+ await expect(takeoverStore.append({
401
+ checkpoint: checkpoint("stale", "root"),
402
+ expectedHeadId: "root",
403
+ writer: old
404
+ })).rejects.toMatchObject({ code: "SANDBOX_SNAPSHOT_WRITER_LOST" });
405
+ await expectThreadState(takeoverStore, "stale", {
406
+ ...before,
407
+ checkpoint: null
408
+ });
409
+ await expect(takeoverStore.deleteHead({
410
+ threadId: "thread-a",
411
+ checkpointId: "root",
412
+ writer: old
413
+ })).rejects.toMatchObject({ code: "SANDBOX_SNAPSHOT_WRITER_LOST" });
414
+ await expectThreadState(takeoverStore, "root", before);
415
+ await old.release();
416
+ await expectThreadState(takeoverStore, "stale", {
417
+ ...before,
418
+ checkpoint: null
419
+ });
420
+ await next.release();
421
+ });
422
+ it("deletes only the current head and transitions to its parent", async () => {
423
+ const store = await makeStore(options);
424
+ await append(store, {
425
+ checkpoint: checkpoint("1"),
426
+ expectedHeadId: null
427
+ });
428
+ await append(store, {
429
+ checkpoint: checkpoint("2", "1"),
430
+ expectedHeadId: "1"
431
+ });
432
+ await expect(deleteHead(store, {
433
+ threadId: "thread-a",
434
+ checkpointId: "1"
435
+ })).rejects.toMatchObject({ code: "SANDBOX_SNAPSHOT_NOT_HEAD" });
436
+ await deleteHead(store, {
437
+ threadId: "thread-a",
438
+ checkpointId: "2"
439
+ });
440
+ expect(await store.getHead("thread-a")).toBe("1");
441
+ await deleteHead(store, {
442
+ threadId: "thread-a",
443
+ checkpointId: "1"
444
+ });
445
+ expect(await store.getHead("thread-a")).toBeNull();
446
+ expect(await store.listBlobReferences()).toEqual([]);
447
+ });
448
+ });
449
+ }
450
+ //#endregion
451
+ export { runSandboxCheckpointStoreConformance };
452
+
453
+ //# sourceMappingURL=checkpoint-conformance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoint-conformance.js","names":[],"sources":["../../../src/testkit/checkpoint-conformance.ts"],"sourcesContent":["import { describe, expect, it } from 'vitest'\nimport type {\n SandboxCheckpoint,\n SandboxCheckpointStore,\n SandboxCheckpointStoreOptions,\n SandboxCheckpointWriter,\n} from '../checkpoint-store'\n\nconst writers = new WeakMap<\n SandboxCheckpointStore,\n Map<string, Promise<SandboxCheckpointWriter>>\n>()\nasync function writerFor(store: SandboxCheckpointStore, threadId: string) {\n let storeWriters = writers.get(store)\n if (!storeWriters) {\n storeWriters = new Map()\n writers.set(store, storeWriters)\n }\n let writer = storeWriters.get(threadId)\n if (!writer) {\n writer = store.acquireWriter(threadId)\n storeWriters.set(threadId, writer)\n }\n return writer\n}\nasync function append(\n store: SandboxCheckpointStore,\n input: { checkpoint: SandboxCheckpoint; expectedHeadId: string | null },\n) {\n return store.append({\n ...input,\n writer: await writerFor(store, input.checkpoint.threadId),\n })\n}\nasync function deleteHead(\n store: SandboxCheckpointStore,\n input: { threadId: string; checkpointId: string },\n) {\n return store.deleteHead({\n ...input,\n writer: await writerFor(store, input.threadId),\n })\n}\n\nasync function expectThreadState(\n store: SandboxCheckpointStore,\n staleCheckpointId: string,\n expected: {\n head: string | null\n list: Array<SandboxCheckpoint>\n checkpoint: SandboxCheckpoint | null\n references: Array<{ key: string; references: number }>\n },\n) {\n expect(await store.getHead('thread-a')).toBe(expected.head)\n expect(await store.list('thread-a')).toEqual(expected.list)\n expect(await store.get(staleCheckpointId)).toEqual(expected.checkpoint)\n expect(await store.listBlobReferences()).toEqual(expected.references)\n}\n\nfunction checkpoint(\n id: string,\n parentCheckpointId: string | null = null,\n threadId = 'thread-a',\n): SandboxCheckpoint {\n const hash = (Number(id.replace(/\\D/g, '')) || 1)\n .toString(16)\n .padStart(64, '0')\n return {\n id,\n threadId,\n parentCheckpointId,\n createdAt: Number(id.replace(/\\D/g, '')) || 1,\n reason: 'automatic',\n files: [\n {\n path: `${id}.txt`,\n kind: 'file',\n blobKey: `sandbox-files/sha256/${hash}`,\n size: 1,\n },\n ],\n conversation: [{ role: 'user', content: id }],\n artifacts: [],\n }\n}\n\nexport function runSandboxCheckpointStoreConformance(\n name: string,\n makeStore: (\n options?: SandboxCheckpointStoreOptions,\n ) => SandboxCheckpointStore | Promise<SandboxCheckpointStore>,\n options?: SandboxCheckpointStoreOptions,\n): void {\n describe(`SandboxCheckpointStore conformance: ${name}`, () => {\n it('returns null and an empty list for an unknown thread', async () => {\n const store = await makeStore(options)\n expect(await store.get('missing')).toBeNull()\n expect(await store.getHead('missing-thread')).toBeNull()\n expect(await store.list('missing-thread')).toEqual([])\n })\n\n it('appends a root and then a parent-linked checkpoint', async () => {\n const store = await makeStore(options)\n await expect(\n append(store, { checkpoint: checkpoint('1'), expectedHeadId: null }),\n ).resolves.toEqual({ headId: '1' })\n await expect(\n append(store, {\n checkpoint: checkpoint('2', '1'),\n expectedHeadId: '1',\n }),\n ).resolves.toEqual({ headId: '2' })\n expect(await store.getHead('thread-a')).toBe('2')\n expect((await store.list('thread-a')).map((entry) => entry.id)).toEqual([\n '1',\n '2',\n ])\n })\n\n it('enforces expected-head and parent compare-and-swap rules', async () => {\n const store = await makeStore(options)\n await append(store, { checkpoint: checkpoint('1'), expectedHeadId: null })\n await expect(\n append(store, { checkpoint: checkpoint('2'), expectedHeadId: null }),\n ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_STALE_HEAD' })\n await expect(\n append(store, { checkpoint: checkpoint('3'), expectedHeadId: '1' }),\n ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_PARENT_MISMATCH' })\n })\n\n it('rejects duplicate checkpoint ids', async () => {\n const store = await makeStore(options)\n await append(store, { checkpoint: checkpoint('1'), expectedHeadId: null })\n await expect(\n append(store, { checkpoint: checkpoint('1'), expectedHeadId: '1' }),\n ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_DUPLICATE_ID' })\n })\n\n it('isolates checkpoints and heads by thread', async () => {\n const store = await makeStore(options)\n await append(store, {\n checkpoint: checkpoint('a', null, 'thread-a'),\n expectedHeadId: null,\n })\n await append(store, {\n checkpoint: checkpoint('b', null, 'thread-b'),\n expectedHeadId: null,\n })\n expect(await store.getHead('thread-a')).toBe('a')\n expect(await store.getHead('thread-b')).toBe('b')\n expect((await store.list('thread-a')).map((entry) => entry.id)).toEqual([\n 'a',\n ])\n expect((await store.list('thread-b')).map((entry) => entry.id)).toEqual([\n 'b',\n ])\n })\n\n it('lists checkpoints by createdAt and then checkpoint id', async () => {\n const store = await makeStore(options)\n const first = { ...checkpoint('z'), createdAt: 2 }\n const second = { ...checkpoint('a', 'z'), createdAt: 1 }\n const third = { ...checkpoint('b', 'a'), createdAt: 1 }\n await append(store, { checkpoint: first, expectedHeadId: null })\n await append(store, { checkpoint: second, expectedHeadId: 'z' })\n await append(store, { checkpoint: third, expectedHeadId: 'a' })\n expect((await store.list('thread-a')).map((entry) => entry.id)).toEqual([\n 'a',\n 'b',\n 'z',\n ])\n })\n\n it('orders checkpoint ids and blob keys by UTF-8 bytes', async () => {\n const store = await makeStore(options)\n const ids = ['é', 'a', 'B', '😀']\n const keys = ['1', '2', '3', '4'].map(\n (value) => `sandbox-files/sha256/${value.padStart(64, '0')}`,\n )\n let parent: string | null = null\n for (const [index, id] of ids.entries()) {\n const blobKey = keys[index]\n if (!blobKey) throw new Error(`Missing blob key for '${id}'`)\n await append(store, {\n checkpoint: {\n ...checkpoint(id, parent),\n files: [\n {\n path: `${id}.txt`,\n kind: 'file',\n blobKey,\n size: 1,\n },\n ],\n },\n expectedHeadId: parent,\n })\n parent = id\n }\n expect((await store.list('thread-a')).map((entry) => entry.id)).toEqual([\n 'B',\n 'a',\n 'é',\n '😀',\n ])\n expect(\n (await store.listBlobReferences()).map((entry) => entry.key),\n ).toEqual([\n `sandbox-files/sha256/${'0'.repeat(63)}1`,\n `sandbox-files/sha256/${'0'.repeat(63)}2`,\n `sandbox-files/sha256/${'0'.repeat(63)}3`,\n `sandbox-files/sha256/${'0'.repeat(63)}4`,\n ])\n })\n\n it('rejects malformed Unicode checkpoint ids and blob keys', async () => {\n const store = await makeStore(options)\n const malformed = '\\uD800'\n await expect(\n append(store, {\n checkpoint: checkpoint(malformed),\n expectedHeadId: null,\n }),\n ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_INVALID_ID' })\n await expect(\n append(store, {\n checkpoint: {\n ...checkpoint('root'),\n files: [\n {\n path: 'root.txt',\n kind: 'file',\n blobKey: `sandbox-files/sha256/${malformed}${'0'.repeat(63)}`,\n size: 1,\n },\n ],\n },\n expectedHeadId: null,\n }),\n ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_INVALID_ENTRY' })\n })\n\n it.each([Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY])(\n 'rejects non-finite createdAt without changing state: %s',\n async (createdAt) => {\n const store = await makeStore(options)\n const before = {\n head: await store.getHead('thread-a'),\n list: await store.list('thread-a'),\n references: await store.listBlobReferences(),\n }\n await expect(\n append(store, {\n checkpoint: { ...checkpoint('invalid'), createdAt },\n expectedHeadId: null,\n }),\n ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_INVALID_ENTRY' })\n expect(await store.getHead('thread-a')).toBe(before.head)\n expect(await store.list('thread-a')).toEqual(before.list)\n expect(await store.listBlobReferences()).toEqual(before.references)\n },\n )\n\n it('keeps checkpoints immutable and reference counts distinct blob keys', async () => {\n const store = await makeStore(options)\n const value = {\n ...checkpoint('1'),\n files: [\n ...checkpoint('1').files,\n {\n path: 'same.txt',\n kind: 'file' as const,\n blobKey: `sandbox-files/sha256/${'0'.repeat(63)}1`,\n size: 1,\n },\n ],\n }\n await append(store, { checkpoint: value, expectedHeadId: null })\n const inputFile = value.files[0]\n if (!inputFile) throw new Error('Expected input file')\n inputFile.path = 'input-mutated'\n const loaded = await store.get('1')\n expect(loaded).not.toBe(value)\n if (!loaded) throw new Error('Expected stored checkpoint')\n const loadedFile = loaded.files[0]\n if (!loadedFile) throw new Error('Expected stored file')\n expect(loadedFile.path).toBe('1.txt')\n loadedFile.path = 'changed'\n expect((await store.get('1'))?.files[0]?.path).toBe('1.txt')\n const listed = await store.list('thread-a')\n const listedCheckpoint = listed[0]\n const listedFile = listedCheckpoint?.files[0]\n if (!listedFile) throw new Error('Expected listed file')\n listedFile.path = 'list-mutated'\n expect((await store.list('thread-a'))[0]?.files[0]?.path).toBe('1.txt')\n expect(await store.listBlobReferences()).toEqual([\n { key: `sandbox-files/sha256/${'0'.repeat(63)}1`, references: 1 },\n ])\n })\n\n it('counts one reference per checkpoint for shared blob keys', async () => {\n const store = await makeStore(options)\n const shared = `sandbox-files/sha256/${'f'.repeat(64)}`\n await append(store, {\n checkpoint: {\n ...checkpoint('1'),\n files: [{ path: 'a', kind: 'file', blobKey: shared, size: 1 }],\n },\n expectedHeadId: null,\n })\n await append(store, {\n checkpoint: {\n ...checkpoint('2', '1'),\n files: [{ path: 'b', kind: 'file', blobKey: shared, size: 1 }],\n },\n expectedHeadId: '1',\n })\n expect(await store.listBlobReferences()).toEqual([\n { key: shared, references: 2 },\n ])\n await deleteHead(store, { threadId: 'thread-a', checkpointId: '2' })\n expect(await store.listBlobReferences()).toEqual([\n { key: shared, references: 1 },\n ])\n })\n\n it('allows one non-waiting writer lease per thread', async () => {\n const store = await makeStore(options)\n const lease = await store.acquireWriter('thread-a')\n try {\n await expect(store.acquireWriter('thread-a')).rejects.toMatchObject({\n code: 'SANDBOX_SNAPSHOT_WRITER_CONFLICT',\n })\n const secondLease = await store.acquireWriter('thread-b')\n try {\n expect(secondLease).toBeDefined()\n } finally {\n await secondLease.release()\n }\n } finally {\n await lease.release()\n }\n const nextLease = await store.acquireWriter('thread-a')\n await nextLease.release()\n })\n\n it('extends a lease when renew succeeds', async () => {\n const config = options ?? {}\n let current = config.now?.() ?? 1_000\n const store = await makeStore({ ...config, now: () => current })\n const lease = await store.acquireWriter('thread-a')\n const originalExpiry = lease.expiresAt\n\n current += lease.renewAfterMs\n await expect(lease.renew()).resolves.toEqual({\n expiresAt: current + (config.leaseDurationMs ?? 120_000),\n })\n expect(lease.expiresAt).toBeGreaterThan(originalExpiry)\n await lease.release()\n })\n\n it('does not let a stale lease release a replacement lease', async () => {\n const config = options ?? {}\n let current = config.now?.() ?? 1_000\n const store = await makeStore({ ...config, now: () => current })\n const old = await store.acquireWriter('thread-a')\n current += (config.leaseDurationMs ?? 120_000) + 1\n const replacement = await store.acquireWriter('thread-a')\n\n await old.release()\n await expect(store.acquireWriter('thread-a')).rejects.toMatchObject({\n code: 'SANDBOX_SNAPSHOT_WRITER_CONFLICT',\n })\n await replacement.release()\n })\n\n it('rejects stale leases after expiry takeover without changing state', async () => {\n const config = options ?? {}\n let current = config.now?.() ?? Date.now()\n const takeoverStore = await makeStore({ ...config, now: () => current })\n const old = await takeoverStore.acquireWriter('thread-a')\n await takeoverStore.append({\n checkpoint: checkpoint('root'),\n expectedHeadId: null,\n writer: old,\n })\n current += (config.leaseDurationMs ?? 120_000) + 1\n const beforeExpiry = {\n head: await takeoverStore.getHead('thread-a'),\n list: await takeoverStore.list('thread-a'),\n checkpoint: await takeoverStore.get('root'),\n references: await takeoverStore.listBlobReferences(),\n }\n await expect(old.renew()).rejects.toMatchObject({\n code: 'SANDBOX_SNAPSHOT_WRITER_LOST',\n })\n await expect(\n takeoverStore.append({\n checkpoint: checkpoint('stale', 'root'),\n expectedHeadId: 'root',\n writer: old,\n }),\n ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_WRITER_LOST' })\n await expectThreadState(takeoverStore, 'stale', {\n ...beforeExpiry,\n checkpoint: null,\n })\n await expect(\n takeoverStore.deleteHead({\n threadId: 'thread-a',\n checkpointId: 'root',\n writer: old,\n }),\n ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_WRITER_LOST' })\n await expectThreadState(takeoverStore, 'root', beforeExpiry)\n const next = await takeoverStore.acquireWriter('thread-a')\n expect(next.fence).toBeGreaterThan(old.fence)\n const before = {\n head: await takeoverStore.getHead('thread-a'),\n list: await takeoverStore.list('thread-a'),\n checkpoint: await takeoverStore.get('root'),\n references: await takeoverStore.listBlobReferences(),\n }\n await expect(old.renew()).rejects.toMatchObject({\n code: 'SANDBOX_SNAPSHOT_WRITER_LOST',\n })\n await expect(\n takeoverStore.append({\n checkpoint: checkpoint('stale', 'root'),\n expectedHeadId: 'root',\n writer: old,\n }),\n ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_WRITER_LOST' })\n await expectThreadState(takeoverStore, 'stale', {\n ...before,\n checkpoint: null,\n })\n await expect(\n takeoverStore.deleteHead({\n threadId: 'thread-a',\n checkpointId: 'root',\n writer: old,\n }),\n ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_WRITER_LOST' })\n await expectThreadState(takeoverStore, 'root', before)\n await old.release()\n await expectThreadState(takeoverStore, 'stale', {\n ...before,\n checkpoint: null,\n })\n await next.release()\n })\n\n it('deletes only the current head and transitions to its parent', async () => {\n const store = await makeStore(options)\n await append(store, { checkpoint: checkpoint('1'), expectedHeadId: null })\n await append(store, {\n checkpoint: checkpoint('2', '1'),\n expectedHeadId: '1',\n })\n await expect(\n deleteHead(store, { threadId: 'thread-a', checkpointId: '1' }),\n ).rejects.toMatchObject({ code: 'SANDBOX_SNAPSHOT_NOT_HEAD' })\n await deleteHead(store, { threadId: 'thread-a', checkpointId: '2' })\n expect(await store.getHead('thread-a')).toBe('1')\n await deleteHead(store, { threadId: 'thread-a', checkpointId: '1' })\n expect(await store.getHead('thread-a')).toBeNull()\n expect(await store.listBlobReferences()).toEqual([])\n })\n })\n}\n"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { ModelMessage } from '@tanstack/ai';
2
+ import { ForkCapableSandboxCheckpointStore } from '../checkpoint-store.js';
3
+ /** Combined stores required to exercise an atomic checkpoint fork. */
4
+ export interface SandboxCheckpointForkConformanceInput {
5
+ persistence: {
6
+ stores: {
7
+ messages: {
8
+ loadThread: (threadId: string) => Promise<Array<ModelMessage>>;
9
+ saveThread: (threadId: string, messages: Array<ModelMessage>) => Promise<void>;
10
+ };
11
+ };
12
+ };
13
+ checkpoints: ForkCapableSandboxCheckpointStore;
14
+ }
15
+ export interface SandboxCheckpointForkConformanceFactory {
16
+ (): SandboxCheckpointForkConformanceInput | Promise<SandboxCheckpointForkConformanceInput>;
17
+ }
18
+ export declare function runSandboxCheckpointForkConformance(name: string, makeSnapshots: SandboxCheckpointForkConformanceFactory): void;