{"record":{"id":"af6ebd8f52e678f0","repo":"paperclipai/paperclip","slug":"sandbox-runtime-asset-key-collides-with-a-reserved","errorCode":null,"errorMessage":"sandbox runtime asset key collides with a reserved runtime archive name: ${key}","messagePattern":"sandbox runtime asset key collides with a reserved runtime archive name: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/adapter-utils/src/sandbox-managed-runtime.ts","lineNumber":436,"sourceCode":"// git-backed workspace, under `<runtimeRootDir>/git-workspace-upload.tar`. Each\n// asset stages under `<runtimeRootDir>/<key>-upload.tar`, so an asset key equal to\n// one of these stems resolves to the same remote archive path. Reserve the stems.\nconst RESERVED_RUNTIME_ASSET_KEYS = new Set([\"workspace\", \"git-workspace\"]);\n\n// Reject an asset key before any path is built from it. An asset key becomes a\n// remote directory (`<runtimeRootDir>/<key>`), a remote archive name\n// (`<key>-upload.tar`), and a host temp file (`<key>.tar`). A path separator or\n// `..` in the key escapes those roots. A reserved stem makes the asset archive\n// share a path with the workspace archive; under concurrent sync the asset task\n// and the workspace task then write or upload the same archive at the same time,\n// which fails extraction nondeterministically or puts asset bytes in the\n// workspace. Fail closed on both cases.\nfunction assertRuntimeAssetKeyIsSafe(key: string): void {\n  if (key.length === 0 || key.includes(\"/\") || key.includes(\"\\\\\") || key.includes(\"..\")) {\n    throw new Error(`sandbox runtime asset key is not a simple path segment: ${key}`);\n  }\n  if (RESERVED_RUNTIME_ASSET_KEYS.has(key)) {\n    throw new Error(`sandbox runtime asset key collides with a reserved runtime archive name: ${key}`);\n  }\n}\n\nexport function parseSandboxRemoteExecutionSpec(value: unknown): SandboxRemoteExecutionSpec | null {\n  const parsed = asObject(value);\n  const transport = asString(parsed.transport).trim();\n  const provider = asString(parsed.provider).trim();\n  const sandboxId = asString(parsed.sandboxId).trim();\n  const remoteCwd = asString(parsed.remoteCwd).trim();\n  const timeoutMs = asNumber(parsed.timeoutMs);\n\n  if (\n    transport !== \"sandbox\" ||\n    provider.length === 0 ||\n    sandboxId.length === 0 ||\n    remoteCwd.length === 0 ||\n    !Number.isFinite(timeoutMs) ||\n    timeoutMs <= 0","sourceCodeStart":418,"sourceCodeEnd":454,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/packages/adapter-utils/src/sandbox-managed-runtime.ts#L418-L454","documentation":"Thrown by assertRuntimeAssetKeyIsSafe when a runtime asset key is exactly \"workspace\" or \"git-workspace\". Those stems are reserved: the asset archive would share its path with the workspace archive, and under concurrent sync the two tasks would write/upload the same file simultaneously, corrupting extraction or leaking asset bytes into the workspace, so the runtime fails closed.","triggerScenarios":"A SandboxRemoteExecutionSpec runtimeAssets entry uses key \"workspace\" or \"git-workspace\" (e.g. author intended to ship a prebuilt workspace); assertRuntimeAssetKeyIsSafe rejects it before sync at sandbox-managed-runtime.ts:774.","commonSituations":"Reusing a natural directory name as the asset key; converting an older manifest that happened to name its asset \"workspace\"; tooling that defaults keys to the top-level folder name being archived.","solutions":["Rename the key to any unreserved single-segment value, e.g. \"workspace-assets\" or \"ws-overlay\".","Treat the reserved set {\"workspace\", \"git-workspace\"} as off-limits when generating keys programmatically (mirror RESERVED_RUNTIME_ASSET_KEYS).","If you meant to sync the workspace itself, use the workspace sync path, not a runtime asset."],"exampleFix":"// before\nconst assets = [{ key: \"workspace\", source: archivePath }];\n\n// after\nconst assets = [{ key: \"workspace-overlay\", source: archivePath }];","handlingStrategy":"validation","validationCode":"const RESERVED = new Set([\"workspace\", \"git-workspace\"]);\nfunction isUsableRuntimeAssetKey(key: string): boolean {\n  return isSafeRuntimeAssetKey(key) && !RESERVED.has(key);\n}","typeGuard":"function isUnreservedAssetKey(key: string): boolean {\n  return key !== \"workspace\" && key !== \"git-workspace\";\n}","tryCatchPattern":"try {\n  buildRuntimeAssets(assets);\n} catch (error) {\n  if (error instanceof Error && error.message.includes(\"reserved runtime archive name\")) {\n    assets = assets.map((a) => a.key === badKey ? { ...a, key: `${a.key}-assets` } : a);\n  } else throw error;\n}","preventionTips":["Treat {\"workspace\", \"git-workspace\"} as a blocklist when generating keys.","Do not name assets after top-level runtime directories.","Add a manifest lint step that flags reserved keys before submission."],"tags":["sandbox","assets","validation","reserved-name","concurrency"],"backgroundTag":"reserved-name-collision","analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-21T17:58:32.592Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}