openai/codex · error · anyhow::Error

invalid agent identity JWT format

Error message

invalid agent identity JWT format

What it means

An inject_request_headers entry selected secret_file as its secret source, but the string is empty or whitespace-only. parse_secret_file requires a usable path because the proxy reads the file's contents at request time to build the header value, so a blank path can never resolve. Raised during validate_injected_headers at config load and again when hooks compile.

Source

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

    let jwk = jwks
        .find(&kid)
        .with_context(|| format!("agent identity JWT kid {kid} is not trusted"))?;
    let decoding_key = DecodingKey::from_jwk(jwk).context("failed to build JWT decoding key")?;
    let mut validation = Validation::new(Algorithm::RS256);
    validation.set_audience(&[AGENT_IDENTITY_JWT_AUDIENCE]);
    validation.set_issuer(&[AGENT_IDENTITY_JWT_ISSUER]);
    validation.required_spec_claims.insert("iss".to_string());
    validation.required_spec_claims.insert("aud".to_string());
    decode::<AgentIdentityJwtClaims>(jwt, &decoding_key, &validation)
        .map(|data| data.claims)
        .context("failed to verify agent identity JWT")
}

fn decode_agent_identity_jwt_payload<T: DeserializeOwned>(jwt: &str) -> Result<T> {
    let mut parts = jwt.split('.');
    let (_header_b64, payload_b64, _sig_b64) = match (parts.next(), parts.next(), parts.next()) {
        (Some(h), Some(p), Some(s)) if !h.is_empty() && !p.is_empty() && !s.is_empty() => (h, p, s),
        _ => anyhow::bail!("invalid agent identity JWT format"),
    };
    anyhow::ensure!(parts.next().is_none(), "invalid agent identity JWT format");

    let payload_bytes = URL_SAFE_NO_PAD
        .decode(payload_b64)
        .context("agent identity JWT payload is not valid base64url")?;
    serde_json::from_slice(&payload_bytes).context("agent identity JWT payload is not valid JSON")
}

pub fn sign_task_registration_payload(
    key: AgentIdentityKey<'_>,
    timestamp: &str,
) -> Result<String> {
    let signing_key = signing_key_from_private_key_pkcs8_base64(key.private_key_pkcs8_base64)?;
    let payload = format!("{}:{timestamp}", key.agent_runtime_id);
    Ok(BASE64_STANDARD.encode(signing_key.sign(payload.as_bytes()).to_bytes()))
}

View on GitHub (pinned to 339751715c)

Solutions

  1. Set secret_file to a non-empty absolute path the proxy can read: secret_file = "/etc/codex/github.token"
  2. If the secret should come from the environment instead, remove secret_file and set secret_env_var
  3. Check file permissions — the proxy process must be able to read it at request time

Example fix

// config.toml — before
[[network.mitm_hooks.actions.inject_request_headers]]
name = "authorization"
secret_file = ""

// after
[[network.mitm_hooks.actions.inject_request_headers]]
name = "authorization"
secret_file = "/etc/codex/github.token"
prefix = "Bearer "
Defensive patterns

Strategy: validation

Validate before calling

for header in &hook.actions.inject_request_headers {
    if let Some(file) = header.secret_file.as_deref() {
        if file.trim().is_empty() {
            return Err(anyhow!("{} has a blank secret_file", header.name));
        }
    }
}

Type guard

fn secret_file_nonempty(header: &InjectedHeaderConfig) -> bool {
    header.secret_file.as_deref().map_or(true, |f| !f.trim().is_empty())
}

Prevention

When it happens

Trigger: [[network.mitm_hooks.actions.inject_request_headers]] with secret_env_var removed and secret_file = "" (or whitespace-only) — validation aborts config load before the proxy starts.

Common situations: A placeholder empty string that was never replaced; migration from secret_env_var where the env key was deleted but the file field was never filled in.

Related errors


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