BigPizzaV3/CodexPlusPlus · error
Invalid recovery key
Error message
Invalid recovery key
What it means
`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).
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.
Example fix
// before
prepare(&paths, "unified-computer-use", &contract)?;
// after
let key = discover(&paths)?.ok_or_else(|| anyhow!("no runtime found"))?;
prepare(&paths, &key, &contract)?; Defensive patterns
Strategy: validation
Validate before calling
fn is_valid_key(key: &str) -> bool {
!key.is_empty()
&& !key.contains('/')
&& !key.contains('\\')
&& key != "." && key != ".."
&& key.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
} Type guard
fn as_runtime_key(s: &str) -> Option<&str> {
if is_valid_key(s) { Some(s) } else { None }
} Try / catch
let Some(key) = as_runtime_key(&user_key) else {
bail!("bad runtime key; discover it instead of hard-coding");
};
prepare(&paths, key, &contract)?; Prevention
- 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
When it happens
Trigger: 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`.
Common situations: 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.
Understand the failure class
Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.
Related errors
- Backup source database is not an allowed local storage path
- Concurrent recovery change
- 无效的市场主题下载文件名
- External runtime change prevents recovery
- Invalid candidate hash
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19).
Data as JSON: /api/errors/4a0be90e228519fb.
Report an issue: GitHub.
Appendix: source
Thrown at crates/codex-plus-core/src/native_browser.rs:466
);
ensure!(
read_regular(&target, MAX_SERVICE)? == current,
"Concurrent runtime change"
);
atomic_write(&target, &candidate)?;
ensure!(
read_regular(&target, MAX_SERVICE)? == candidate,
"Runtime write verification failed"
);
Ok(())
}
fn recovery_material(
paths: &BrowserPaths,
key: &str,
contract: &RuntimeContract,
) -> Result<(Journal, Vec<u8>, Vec<u8>)> {
ensure!(key_valid(key), "Invalid recovery key");
let dir = paths.state_root.join(key);
let journal: Journal = serde_json::from_slice(&read_regular(&dir.join("journal.json"), 4096)?)?;
let original = read_regular(&dir.join("original.mjs"), MAX_SERVICE)?;
ensure!(
journal.candidate_sha.len() == 64
&& journal.candidate_sha.bytes().all(|b| b.is_ascii_hexdigit()),
"Invalid candidate hash"
);
let candidate = read_regular(
&dir.join(format!("candidate-{}.mjs", journal.candidate_sha)),
MAX_SERVICE,
)?;
ensure!(
journal.schema == 1
&& (journal.original_sha == contract.service_sha
|| journal.original_sha == ORIGINAL_SHA)
&& sha(&original) == journal.original_sha
&& journal.candidate_sha == sha(&candidate)View on GitHub (pinned to b1ed92e5e4)