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

  1. Obtain the key from `discover(&paths)` (the `.mcp.json` selected_key) instead of hard-coding it.
  2. Inspect the key for path separators or unexpected characters; only use directory names enumerated from `plugins/cache/openai-bundled/unified-computer-use`.
  3. Refresh stored keys after a codex/plugin update that changes cache directory names.
  4. 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

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


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)