{"record":{"id":"83bdbdbdaa4c6617","repo":"JuliusBrussee/caveman","slug":"cave-live-eval-sandbox-profile-invalid","errorCode":null,"errorMessage":"cave_live_eval_sandbox_profile_invalid","messagePattern":"cave_live_eval_sandbox_profile_invalid","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/cli.ts","lineNumber":1250,"sourceCode":"  credentialEnv: readonly string[];\n}> {\n  const path = fixture.tools.sandbox;\n  if (!path) throw new Error(\"cave_live_eval_sandbox_profile_missing\");\n  const fullPath = resolve(root, path);\n  const relativePath = relative(root, fullPath);\n  if (relativePath === \"..\" || relativePath.startsWith(\"../\") ||\n      relativePath.startsWith(\"..\\\\\") || isAbsolute(relativePath)) {\n    throw new Error(\"cave_live_eval_sandbox_profile_escapes_root\");\n  }\n  const parsed = JSON.parse(await readFile(fullPath, \"utf8\")) as Record<string, unknown>;\n  const keys = Object.keys(parsed).sort();\n  const expected = [\"child_process\", \"credential_env\", \"network\", \"schema_version\"];\n  if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index]) ||\n      parsed.schema_version !== 1 || typeof parsed.network !== \"boolean\" ||\n      typeof parsed.child_process !== \"boolean\" || !Array.isArray(parsed.credential_env) ||\n      parsed.credential_env.some((name) => typeof name !== \"string\" ||\n        !/^[A-Z][A-Z0-9_]{0,127}$/.test(name))) {\n    throw new Error(\"cave_live_eval_sandbox_profile_invalid\");\n  }\n  validateSandboxCredentialEnv(parsed.credential_env as string[]);\n  return {\n    network: parsed.network,\n    childProcess: parsed.child_process,\n    credentialEnv: parsed.credential_env as string[],\n  };\n}\n\nfunction contextIRIsContentBlind(ir: Awaited<ReturnType<typeof lowerContext>>[\"ir\"]): boolean {\n  const allowed = new Set([\n    \"id\", \"kind\", \"stability\", \"safety\", \"priority\", \"recovery\", \"cacheRegion\",\n    \"privacy\", \"opaque\", \"ttlTurns\", \"provenanceDigest\", \"tokenCount\", \"bodyHandle\",\n  ]);\n  return ir.segments.every((segment) =>\n    Object.keys(segment).every((key) => allowed.has(key)) &&\n    /^cave_local_sha256:[0-9a-f]{64}$/.test(segment.bodyHandle) &&\n    /^[0-9a-f]{64}$/.test(segment.provenanceDigest));","sourceCodeStart":1232,"sourceCodeEnd":1268,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/cli.ts#L1232-L1268","documentation":"Thrown by `loadEvalSandboxProfile` when the profile JSON parses but fails a strict schema check: the object must have exactly the four sorted keys `child_process, credential_env, network, schema_version` (no extras, none missing), `schema_version === 1`, `network` and `child_process` boolean, and `credential_env` an array of strings each matching `^[A-Z][A-Z0-9_]{0,127}$`. Any deviation — extra key, wrong type, malformed env name — throws before `validateSandboxCredentialEnv` even runs.","triggerScenarios":"A sandbox profile with an added convenience key (e.g. `\"notes\": ...`), `schema_version: 2` or missing, `network: \"false\"` as a string, or a credential env name like `github_token` (lowercase) or one longer than 128 chars. Hand-edited profiles are the usual source.","commonSituations":"Authors adding comments/metadata into the JSON; profiles written for a future schema version; env-var lists copied from shell exports with lowercase names or leading digits.","solutions":["Rewrite the profile to exactly the four keys with schema_version 1 and correct types.","Normalize credential_env entries to UPPER_SNAKE_CASE (start A-Z, then A-Z0-9_, max 128 chars).","Validate profiles in a pre-test step or CI lint so drift is caught before `caveman build`."],"exampleFix":"// before\n{ \"schema_version\": 1, \"network\": \"true\", \"child_process\": false, \"credential_env\": [\"api-key\"], \"notes\": \"tmp\" }\n\n// after\n{ \"schema_version\": 1, \"network\": true, \"child_process\": false, \"credential_env\": [\"API_KEY\"] }","handlingStrategy":"validation","validationCode":"function isValidSandboxProfile(parsed: unknown): boolean {\n  if (typeof parsed !== \"object\" || parsed === null) return false;\n  const p = parsed as Record<string, unknown>;\n  const keys = Object.keys(p).sort();\n  const expected = [\"child_process\", \"credential_env\", \"network\", \"schema_version\"];\n  return keys.length === 4 && keys.every((k, i) => k === expected[i]) &&\n    p.schema_version === 1 && typeof p.network === \"boolean\" &&\n    typeof p.child_process === \"boolean\" &&\n    Array.isArray(p.credential_env) &&\n    (p.credential_env as unknown[]).every((n) => typeof n === \"string\" && /^[A-Z][A-Z0-9_]{0,127}$/.test(n));\n}","typeGuard":"type SandboxProfile = { schema_version: 1; network: boolean; child_process: boolean; credential_env: string[] };\nfunction isSandboxProfile(v: unknown): v is SandboxProfile {\n  return isValidSandboxProfile(v);\n}","tryCatchPattern":"try {\n  await build(args);\n} catch (error) {\n  if (error instanceof Error && error.message === \"cave_live_eval_sandbox_profile_invalid\") {\n    // rewrite the profile to the exact 4-key schema with UPPER_SNAKE_CASE env names\n  } else throw error;\n}","preventionTips":["Generate profiles from a typed helper or JSON schema, never by hand.","CI-lint every sandbox profile against the 4-key schema before build.","Keep env names UPPER_SNAKE_CASE, max 128 chars, and never add extra keys."],"tags":["sandbox","validation","eval","schema"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}