{"record":{"id":"3cf4948d42f9e766","repo":"paperclipai/paperclip","slug":"sandbox-runtime-asset-key-is-not-a-simple-path-seg","errorCode":null,"errorMessage":"sandbox runtime asset key is not a simple path segment: ${key}","messagePattern":"sandbox runtime asset key is not a simple path segment: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/adapter-utils/src/sandbox-managed-runtime.ts","lineNumber":442,"sourceCode":"   * `access` is not `\"rw\"`.\n   */\n  writablePath?: string;\n}\n\n/**\n * A control command run against the sandbox after a sync operation's files have\n * landed. Mirrors the plugin SDK `PluginPostUploadCommand`; kept as a local\n * structural type so `adapter-utils` does not depend on the plugin SDK. Ordered\n * within {@link SandboxSyncOperation.postUploadCommands} and executed in array\n * order, fail-fast (first non-zero exit or timeout aborts the operation).\n *\n * SECURITY — command origin (Stage-1 design review, condition C1). `command` is\n * a **Paperclip/adapter-authored control operation**: it may be supplied ONLY by\n * core/adapter code. No server route, issue/comment content, project/workspace\n * file content, provider-plugin callback, or arbitrary adapter config may supply\n * a raw `command` string; any path embedded in it MUST be built by adapter/core\n * helpers from already-confined paths and shell-quoted (C3). Providers treat the\n * command as **opaque** — execute or reject, never rewrite/concatenate/append.\n */\nexport interface SandboxPostUploadCommand {\n  /** The opaque, adapter-authored shell command to run after upload. */\n  command: string;\n  /**\n   * Working directory for the command. When present, MUST be an absolute POSIX\n   * path confined under the operation's allowed sandbox target root (C2). When\n   * absent, defaults to the runtime's stable command cwd — never a process\n   * default cwd.\n   */\n  cwd?: string;\n  /** Optional per-command timeout in milliseconds. */\n  timeoutMs?: number;\n}\n\n/**\n * An ordered, opaque unit of work handed to the native sync transport. The\n * `operationId` is an opaque, non-sensitive token authored by the orchestrator","sourceCodeStart":424,"sourceCodeEnd":460,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/packages/adapter-utils/src/sandbox-managed-runtime.ts#L424-L460","documentation":"Thrown by assertRuntimeAssetKeyIsSafe (called from the sandbox managed runtime before building any paths, sandbox-managed-runtime.ts:774) when a runtime asset key is empty or contains \"/\", \"\\\\\", or \"..\". The key becomes a remote directory, an archive name, and a host temp file, so a separator or traversal sequence could escape those roots; the runtime fails closed instead of building the path.","triggerScenarios":"A SandboxRemoteExecutionSpec's runtimeAssets entry uses a key derived from a file path (\"cache/node_modules\"), a Windows path (\"assets\\\\bin\"), a traversal (\"../secret\"), or an empty string; path building is rejected before any sync starts.","commonSituations":"Generating keys from user input or workspace-relative paths without normalizing; accepting asset manifests from config files or APIs where callers paste paths instead of names; Windows-developed manifests leaking backslash separators.","solutions":["Use a flat, single-segment key: letters, digits, dot, dash, underscore (e.g. \"build-cache\", \"node_modules-cache\").","When deriving a key from a path, take path.basename() and strip anything outside a safe alphabet.","Reject empty keys and any key containing \"/\", \"\\\\\", or \"..\" at the boundary where the asset list enters your system.","Keep multi-level layouts inside the asset archive, not in the key."],"exampleFix":"// before\nconst key = userInputPath; // \"tools/../workspace\"\n\n// after\nconst key = sanitizeAssetKey(userInputPath);\nfunction sanitizeAssetKey(raw: string): string {\n  const key = path.basename(raw.trim());\n  if (!key || key.includes(\"..\") || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(key)) {\n    throw new Error(`invalid runtime asset key: ${JSON.stringify(raw)}`);\n  }\n  return key;\n}","handlingStrategy":"validation","validationCode":"const RUNTIME_ASSET_KEY_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\nfunction isSafeRuntimeAssetKey(key: string): boolean {\n  return key.length > 0 && !key.includes(\"/\") && !key.includes(\"\\\\\") &&\n    !key.includes(\"..\") && RUNTIME_ASSET_KEY_RE.test(key);\n}\nconst assets = manifest.assets.filter((a) => isSafeRuntimeAssetKey(a.key));","typeGuard":"function isSafeRuntimeAssetKey(key: unknown): key is string {\n  return typeof key === \"string\" && key.length > 0 &&\n    !key.includes(\"/\") && !key.includes(\"\\\\\") && !key.includes(\"..\");\n}","tryCatchPattern":"try {\n  buildRuntimeAssets(assets);\n} catch (error) {\n  if (error instanceof Error && error.message.includes(\"not a simple path segment\")) {\n    return rejectManifest(error); // surface which key is bad; do not sanitize silently\n  }\n  throw error;\n}","preventionTips":["Generate keys from path.basename() plus an allowlist alphabet.","Validate asset manifests at ingestion (API/config boundary), not at sync time.","Never accept raw filesystem paths or user strings as asset keys."],"tags":["sandbox","path-traversal","validation","security","assets"],"backgroundTag":"path-traversal-rejected","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-08-21T17:58:32.592Z","contentChangedAt":"2026-08-21T17:58:32.592Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}