{"record":{"id":"4b75038a9618005f","repo":"JuliusBrussee/caveman","slug":"cave-sandbox-credential-capability-ambiguous","errorCode":"cave_sandbox_credential_capability_ambiguous","errorMessage":"cave_sandbox_credential_capability_ambiguous","messagePattern":"cave_sandbox_credential_capability_ambiguous","errorType":"error_code","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":606,"sourceCode":"\nexport function validateSandboxCredentialEnv(names: readonly string[]): void {\n  const capabilities = new Set<SandboxCredentialCapability>();\n  for (const name of names) {\n    const denied = typeof name === \"string\" &&\n      (SANDBOX_CREDENTIAL_DENY_NAMES.has(name) ||\n        SANDBOX_CREDENTIAL_DENY_PREFIXES.some((prefix) => name.startsWith(prefix)));\n    const capability = denied || typeof name !== \"string\"\n      ? undefined\n      : SANDBOX_CREDENTIAL_ENV_TO_CAPABILITY.get(name);\n    if (capability === undefined) {\n      // Keep profile-controlled names out of errors. This is a policy result,\n      // not a diagnostic surface, and the name may itself identify a secret.\n      throw new Error(\"cave_sandbox_credential_env_not_allowlisted\");\n    }\n    capabilities.add(capability);\n  }\n  if (capabilities.size > 1) {\n    throw new Error(\"cave_sandbox_credential_capability_ambiguous\");\n  }\n}\n\n/** Build the complete environment for an isolated tool child. No spread of\n * `process.env`: only deterministic runtime baseline plus an exact provider\n * capability selected by the validated live profile. */\nexport function buildSandboxToolEnv(names: readonly string[] = []): NodeJS.ProcessEnv {\n  validateSandboxCredentialEnv(names);\n  const env: NodeJS.ProcessEnv = {\n    LANG: process.env.LANG ?? \"C\",\n    LC_ALL: process.env.LC_ALL ?? \"C\",\n    PATH: process.env.PATH ?? \"\",\n    TZ: process.env.TZ ?? \"UTC\",\n    CAVE_EVAL_FIXTURE: \"1\",\n  };\n  for (const name of names) {\n    const value = process.env[name];\n    if (value === undefined) throw new Error(\"cave_sandbox_credential_missing\");","sourceCodeStart":588,"sourceCodeEnd":624,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/runtime.ts#L588-L624","documentation":"validateSandboxCredentialEnv maps each requested credential env name to a provider capability; if the set of names spans more than one distinct capability, the request is ambiguous about which provider's credential to inject and is rejected.","triggerScenarios":"A sandbox profile or buildSandboxToolEnv call requests credential env vars that map to two different provider capabilities (e.g. both an Anthropic key and an OpenAI key) in a single sandbox tool environment.","commonSituations":"Copy-pasting a profile and leaving credentials for two providers; merging profiles without pruning credential env lists; a shared team profile that accumulated multiple providers' keys.","solutions":["Request credential env vars for exactly one provider capability per sandbox env.","Split the sandbox tool run into separate runs per provider.","Prune unneeded credential names from the profile.","Verify the capability mapping by reading which env names map to which capability in the library."],"exampleFix":"// before\ncredentialEnv: [\"ANTHROPIC_API_KEY\", \"OPENAI_API_KEY\"]; // two capabilities\n// after\ncredentialEnv: [\"ANTHROPIC_API_KEY\"];","handlingStrategy":"validation","validationCode":"const caps = new Set(names.map((n: string) => SANDBOX_CREDENTIAL_ENV_TO_CAPABILITY.get(n)));\nif (caps.size > 1) throw new Error(\"credential env spans multiple provider capabilities\");","typeGuard":"const isSingleCapability = (names: string[]): boolean =>\n  new Set(names.map((n: string) => SANDBOX_CREDENTIAL_ENV_TO_CAPABILITY.get(n))).size <= 1;","tryCatchPattern":"try {\n  buildSandboxToolEnv(names);\n} catch (e) {\n  if (e.message === \"cave_sandbox_credential_capability_ambiguous\") {\n    // split names by capability and run per provider\n  } else throw e;\n}","preventionTips":["One provider's credentials per sandbox environment.","Prune multi-provider leftovers from shared team profiles.","Derive the credential list from the profile's provider field, not a hardcoded merged list."],"tags":["sandbox","env","security","ambiguity"],"backgroundTag":"conflicting-config-options","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}