openai/codex · error · anyhow::Error

failed to decrypt encrypted task id

Error message

failed to decrypt encrypted task id

What it means

Host::parse runs normalize_host (trim whitespace, strip brackets and a single :port, lowercase, drop trailing dots, canonicalize IP literals) and rejects a result that comes out empty. Host is the unit used for network policy evaluation, so callers are whatever feeds hosts into policy: config domain entries or host strings extracted from requests. Inputs that normalize to empty include an empty string, whitespace-only input, ":8080" (empty host before the port), and a lone "." (nothing left after trimming trailing dots).

Source

Thrown at codex-rs/agent-identity/src/lib.rs:421

    }
    let encrypted_task_id = response
        .encrypted_task_id
        .or(response.encrypted_task_id_camel)
        .context("agent task registration response omitted task id")?;
    decrypt_task_id_response(key, &encrypted_task_id)
}

pub fn decrypt_task_id_response(
    key: AgentIdentityKey<'_>,
    encrypted_task_id: &str,
) -> Result<String> {
    let signing_key = signing_key_from_private_key_pkcs8_base64(key.private_key_pkcs8_base64)?;
    let ciphertext = BASE64_STANDARD
        .decode(encrypted_task_id)
        .context("encrypted task id is not valid base64")?;
    let plaintext = curve25519_secret_key_from_signing_key(&signing_key)
        .unseal(&ciphertext)
        .map_err(|_| anyhow::anyhow!("failed to decrypt encrypted task id"))?;
    String::from_utf8(plaintext).context("decrypted task id is not valid UTF-8")
}

pub fn generate_agent_key_material() -> Result<GeneratedAgentKeyMaterial> {
    let mut seed_material = [0u8; AGENT_IDENTITY_KEY_SEED_BYTES];
    OsRng
        .try_fill_bytes(&mut seed_material)
        .context("failed to generate agent identity private key seed material")?;
    // Ed25519 stores a 32-byte seed, so derive it from all sampled seed material.
    let mut digest = Sha512::new();
    digest.update(AGENT_IDENTITY_KEY_DERIVATION_CONTEXT);
    digest.update(seed_material);
    let digest = digest.finalize();
    let mut secret_key_bytes = [0u8; 32];
    secret_key_bytes.copy_from_slice(&digest[..32]);
    let signing_key = SigningKey::from_bytes(&secret_key_bytes);
    let private_key_pkcs8 = signing_key
        .to_pkcs8_der()

View on GitHub (pinned to 339751715c)

Solutions

  1. Reject or skip empty host strings before calling Host::parse
  2. When splitting host:port yourself, verify the host part is non-empty after the split
  3. If hosts come from URLs, parse with url::Url and require .host_str() to return Some and be non-empty

Example fix

// before
let host = Host::parse(raw)?; // raw may be "" or ":443" from a bad config entry

// after
if raw.trim().is_empty() || raw.trim().trim_end_matches('.').is_empty() {
    continue; // skip malformed entry instead of failing policy build
}
let host = Host::parse(raw)?;
Defensive patterns

Strategy: validation

Validate before calling

// Mirror of normalize_host for pre-checking callers
fn parseable_host(input: &str) -> bool {
    let h = input.trim();
    let h = h.strip_prefix('[').and_then(|r| r.split(']').next()).unwrap_or(h);
    let h = if h.matches(':').count() == 1 { h.split(':').next().unwrap_or("") } else { h };
    !h.to_ascii_lowercase().trim_end_matches('.').is_empty()
}
if !parseable_host(raw) {
    return Err(anyhow!("unusable host string {raw:?}"));
}
let host = Host::parse(raw)?;

Type guard

fn is_nonempty_host_str(input: &str) -> bool {
    !input.trim().is_empty() && input.trim() != "." && !input.trim().starts_with(':')
}

Try / catch

let host = match Host::parse(raw) {
    Ok(host) => host,
    Err(err) if err.to_string().contains("host is empty") => continue, // skip bad entry
    Err(err) => return Err(err),
};

Prevention

When it happens

Trigger: Calling Host::parse with an empty, whitespace-only, port-only (":443"), or dot-only (".") string — typically not hand-written but passed through from an empty host in a config list, or from URL handling where the authority was missing (e.g. a URL like http:///path with no host).

Common situations: Config lists containing empty strings from templating; upstream code splitting host:port and passing the empty half; request paths where a malformed URL yielded no host.

Related errors


AI-assisted analysis of openai/codex@339751715c (2026-08-25). Data as JSON: /api/errors/04945fd7f281581c. Report an issue: GitHub.