{"record":{"id":"4a0be90e228519fb","repo":"BigPizzaV3/CodexPlusPlus","slug":"invalid-recovery-key","errorCode":null,"errorMessage":"Invalid recovery key","messagePattern":"Invalid recovery key","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/native_browser.rs","lineNumber":466,"sourceCode":"    );\n    ensure!(\n        read_regular(&target, MAX_SERVICE)? == current,\n        \"Concurrent runtime change\"\n    );\n    atomic_write(&target, &candidate)?;\n    ensure!(\n        read_regular(&target, MAX_SERVICE)? == candidate,\n        \"Runtime write verification failed\"\n    );\n    Ok(())\n}\n\nfn recovery_material(\n    paths: &BrowserPaths,\n    key: &str,\n    contract: &RuntimeContract,\n) -> Result<(Journal, Vec<u8>, Vec<u8>)> {\n    ensure!(key_valid(key), \"Invalid recovery key\");\n    let dir = paths.state_root.join(key);\n    let journal: Journal = serde_json::from_slice(&read_regular(&dir.join(\"journal.json\"), 4096)?)?;\n    let original = read_regular(&dir.join(\"original.mjs\"), MAX_SERVICE)?;\n    ensure!(\n        journal.candidate_sha.len() == 64\n            && journal.candidate_sha.bytes().all(|b| b.is_ascii_hexdigit()),\n        \"Invalid candidate hash\"\n    );\n    let candidate = read_regular(\n        &dir.join(format!(\"candidate-{}.mjs\", journal.candidate_sha)),\n        MAX_SERVICE,\n    )?;\n    ensure!(\n        journal.schema == 1\n            && (journal.original_sha == contract.service_sha\n                || journal.original_sha == ORIGINAL_SHA)\n            && sha(&original) == journal.original_sha\n            && journal.candidate_sha == sha(&candidate)","sourceCodeStart":448,"sourceCodeEnd":484,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/native_browser.rs#L448-L484","documentation":"`recovery_material` validates the per-runtime recovery key before touching any state: `key_valid(key)` must pass and the key is then used as a directory name under the state root. An invalid key means the caller passed a key that does not conform to the expected runtime-key format, so loading the journal would be meaningless or unsafe (path-shaped keys could also be a traversal risk; `plain_path` guards elsewhere).","triggerScenarios":"Calling `reconcile`/`restore_all`/`verify_restored_state` (or the test helper `stored_candidate_allows_adapter_upgrade_without_overwriting_external_changes`) with a `key` string that fails `key_valid` — e.g. empty string, a path containing separators, or a name not produced by `discover`/`selected_key`.","commonSituations":"Hard-coding a guessed runtime key instead of using the one returned by discovery; passing a user-supplied or config-stored key after the plugin renamed its cache directory; constructing keys by hand in tests/tools.","solutions":["Obtain the key from `discover(&paths)` (the `.mcp.json` selected_key) instead of hard-coding it.","Inspect the key for path separators or unexpected characters; only use directory names enumerated from `plugins/cache/openai-bundled/unified-computer-use`.","Refresh stored keys after a codex/plugin update that changes cache directory names.","Use the public `reconcile(paths, enabled)` API, which discovers keys itself, rather than internal key-taking functions."],"exampleFix":"// before\nprepare(&paths, \"unified-computer-use\", &contract)?;\n// after\nlet key = discover(&paths)?.ok_or_else(|| anyhow!(\"no runtime found\"))?;\nprepare(&paths, &key, &contract)?;","handlingStrategy":"validation","validationCode":"fn is_valid_key(key: &str) -> bool {\n    !key.is_empty()\n        && !key.contains('/')\n        && !key.contains('\\\\')\n        && key != \".\" && key != \"..\"\n        && key.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')\n}","typeGuard":"fn as_runtime_key(s: &str) -> Option<&str> {\n    if is_valid_key(s) { Some(s) } else { None }\n}","tryCatchPattern":"let Some(key) = as_runtime_key(&user_key) else {\n    bail!(\"bad runtime key; discover it instead of hard-coding\");\n};\nprepare(&paths, key, &contract)?;","preventionTips":["Always derive the key from discover()/directory enumeration, never hard-code it","Validate keys are single path components before passing them","Refresh stored keys after plugin cache renames/upgrades","Prefer the public reconcile(paths, enabled) API that discovers keys itself"],"tags":["invalid-argument","path-safety","recovery"],"backgroundTag":"invalid-identifier-format","analyzedSha":"b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6","analyzedAt":"2026-09-19T23:35:21.129Z","contentChangedAt":"2026-09-19T23:35:21.129Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}