use router_protocol::TransportKind;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use thiserror::Error;
const MAX_PROBE_BYTES: usize = 16 * 1024;
pub const CURRENT_STANDALONE_CLI_VERSION: &str = "0.148.0";
pub const CURRENT_DESKTOP_APP_VERSION: &str = "26.721.41059";
pub const CURRENT_DESKTOP_BUILD: u32 = 5848;
pub const CURRENT_DESKTOP_CODEX_VERSION: &str = "0.147.0-alpha.6.5";
pub const SUPPORTED_DISCOVERY_QUOTA_RPCS: [AppServerRpc; 4] = [
AppServerRpc::AccountRead,
AppServerRpc::AccountRateLimitsRead,
AppServerRpc::ModelList,
AppServerRpc::ModelProviderCapabilitiesRead,
];
const FORBIDDEN_FIELD_NAMES: [&[u8]; 9] = [
b"access_token",
b"accesstoken",
b"refresh_token",
b"refreshtoken",
b"id_token",
b"idtoken",
b"authorization",
b"api_key",
b"apikey",
];
const FORBIDDEN_INPUT_MARKERS: [&[u8]; 5] = [
b"codex_access_token",
b"openai_api_key",
b"requires_openai_auth",
b"auth.json",
b"keychain",
];
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(transparent)]
pub struct CodexCliVersion(String);
impl CodexCliVersion {
pub fn parse(value: impl Into<String>) -> Result<Self, CapabilityInputError> {
let value = value.into();
if value.is_empty()
|| value.len() > 64
|| !value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'+'))
{
return Err(CapabilityInputError::InvalidInput);
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl<'de> Deserialize<'de> for CodexCliVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::parse(value).map_err(serde::de::Error::custom)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InstalledCodexCli {
pub version: CodexCliVersion,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InstalledCodexDesktop {
pub app_version: CodexCliVersion,
pub build: u32,
pub embedded_codex_version: CodexCliVersion,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AppServerTransport {
OwnedStdio,
ManagedUnixSocket,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AppServerAuthMode {
ChatgptManaged,
ApiKey,
ExternalTokens,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum AppServerRpc {
#[serde(rename = "account/read")]
AccountRead,
#[serde(rename = "account/rateLimits/read")]
AccountRateLimitsRead,
#[serde(rename = "model/list")]
ModelList,
#[serde(rename = "modelProvider/capabilities/read")]
ModelProviderCapabilitiesRead,
}
impl AppServerRpc {
pub const fn as_str(self) -> &'static str {
match self {
Self::AccountRead => "account/read",
Self::AccountRateLimitsRead => "account/rateLimits/read",
Self::ModelList => "model/list",
Self::ModelProviderCapabilitiesRead => "modelProvider/capabilities/read",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CodexAppServerProbe {
pub standalone_cli: InstalledCodexCli,
pub desktop: InstalledCodexDesktop,
pub transport: AppServerTransport,
pub auth_mode: AppServerAuthMode,
pub managed_daemon_available: bool,
pub advertised_rpcs: BTreeSet<AppServerRpc>,
}
impl CodexAppServerProbe {
pub fn from_json(input: &[u8]) -> Result<Self, CapabilityInputError> {
if input.is_empty() || input.len() > MAX_PROBE_BYTES {
return Err(CapabilityInputError::InvalidInput);
}
if contains_forbidden_json_key(input)
|| FORBIDDEN_INPUT_MARKERS
.iter()
.any(|marker| contains_ascii_case_insensitive(input, marker))
{
return Err(CapabilityInputError::SensitiveInputRejected);
}
serde_json::from_slice(input).map_err(|_| CapabilityInputError::InvalidInput)
}
pub fn current_observed() -> Self {
Self {
standalone_cli: InstalledCodexCli {
version: CodexCliVersion(CURRENT_STANDALONE_CLI_VERSION.to_owned()),
},
desktop: InstalledCodexDesktop {
app_version: CodexCliVersion(CURRENT_DESKTOP_APP_VERSION.to_owned()),
build: CURRENT_DESKTOP_BUILD,
embedded_codex_version: CodexCliVersion(CURRENT_DESKTOP_CODEX_VERSION.to_owned()),
},
transport: AppServerTransport::OwnedStdio,
auth_mode: AppServerAuthMode::ChatgptManaged,
managed_daemon_available: false,
advertised_rpcs: SUPPORTED_DISCOVERY_QUOTA_RPCS.into_iter().collect(),
}
}
pub fn capability_receipt(&self) -> CodexAppServerCapabilityReceipt {
let mut unavailable_reasons = vec![
CodexSubscriptionUnavailableReason::NoRawInferenceRpc,
CodexSubscriptionUnavailableReason::DesktopExternalAttachmentUnsupported,
];
if !self.matches_current_versions()
|| self.advertised_rpcs != SUPPORTED_DISCOVERY_QUOTA_RPCS.into_iter().collect()
{
unavailable_reasons.push(CodexSubscriptionUnavailableReason::ProtocolMismatch);
}
if !self.managed_daemon_available {
unavailable_reasons.push(CodexSubscriptionUnavailableReason::MissingManagedDaemon);
}
if self.auth_mode != AppServerAuthMode::ChatgptManaged {
unavailable_reasons.push(CodexSubscriptionUnavailableReason::UnsupportedAuthMode);
}
CodexAppServerCapabilityReceipt {
standalone_cli: self.standalone_cli.clone(),
desktop: self.desktop.clone(),
transport: self.transport,
auth_mode: self.auth_mode,
supported_discovery_quota_rpcs: self.advertised_rpcs.clone(),
openai_transport: TransportKind::HarnessManaged,
raw_inference_dispatch: false,
desktop_external_attach: false,
unavailable_reasons,
}
}
fn matches_current_versions(&self) -> bool {
self.standalone_cli.version.as_str() == CURRENT_STANDALONE_CLI_VERSION
&& self.desktop.app_version.as_str() == CURRENT_DESKTOP_APP_VERSION
&& self.desktop.build == CURRENT_DESKTOP_BUILD
&& self.desktop.embedded_codex_version.as_str() == CURRENT_DESKTOP_CODEX_VERSION
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CodexSubscriptionUnavailableReason {
NoRawInferenceRpc,
DesktopExternalAttachmentUnsupported,
ProtocolMismatch,
MissingManagedDaemon,
UnsupportedAuthMode,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct CodexAppServerCapabilityReceipt {
pub standalone_cli: InstalledCodexCli,
pub desktop: InstalledCodexDesktop,
pub transport: AppServerTransport,
pub auth_mode: AppServerAuthMode,
pub supported_discovery_quota_rpcs: BTreeSet<AppServerRpc>,
openai_transport: TransportKind,
raw_inference_dispatch: bool,
desktop_external_attach: bool,
pub unavailable_reasons: Vec<CodexSubscriptionUnavailableReason>,
}
impl CodexAppServerCapabilityReceipt {
pub const fn openai_transport(&self) -> TransportKind {
self.openai_transport
}
pub const fn raw_inference_dispatch(&self) -> bool {
self.raw_inference_dispatch
}
pub const fn desktop_external_attach(&self) -> bool {
self.desktop_external_attach
}
pub const fn dispatch_available(&self) -> bool {
false
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
pub enum CapabilityInputError {
#[error("the capability observation is invalid")]
InvalidInput,
#[error("the capability observation contained forbidden sensitive material")]
SensitiveInputRejected,
}
fn contains_ascii_case_insensitive(haystack: &[u8], needle: &[u8]) -> bool {
haystack.windows(needle.len()).any(|window| {
window
.iter()
.zip(needle)
.all(|(left, right)| left.eq_ignore_ascii_case(right))
})
}
fn contains_forbidden_json_key(input: &[u8]) -> bool {
let mut cursor = 0;
while cursor < input.len() {
if input[cursor] != b'"' {
cursor += 1;
continue;
}
let key_start = cursor + 1;
let mut key_end = key_start;
let mut escaped = false;
while key_end < input.len() {
match input[key_end] {
b'\\' => {
escaped = true;
key_end = key_end.saturating_add(2);
}
b'"' => break,
_ => key_end += 1,
}
}
if key_end == input.len() {
return false;
}
let mut after = key_end + 1;
while after < input.len() && input[after].is_ascii_whitespace() {
after += 1;
}
if !escaped
&& input.get(after) == Some(&b':')
&& FORBIDDEN_FIELD_NAMES
.iter()
.any(|name| contains_ascii_case_insensitive(&input[key_start..key_end], name))
{
return true;
}
cursor = key_end + 1;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn current_probe_json() -> serde_json::Value {
json!({
"standalone_cli": { "version": CURRENT_STANDALONE_CLI_VERSION },
"desktop": {
"app_version": CURRENT_DESKTOP_APP_VERSION,
"build": CURRENT_DESKTOP_BUILD,
"embedded_codex_version": CURRENT_DESKTOP_CODEX_VERSION
},
"transport": "owned_stdio",
"auth_mode": "chatgpt_managed",
"managed_daemon_available": false,
"advertised_rpcs": [
"account/read",
"account/rateLimits/read",
"model/list",
"modelProvider/capabilities/read"
]
})
}
fn parse(value: &serde_json::Value) -> Result<CodexAppServerProbe, CapabilityInputError> {
CodexAppServerProbe::from_json(
&serde_json::to_vec(value).expect("serialize capability fixture"),
)
}
#[test]
fn discovery_and_quota_rpc_allowlist_is_exact() {
assert_eq!(
SUPPORTED_DISCOVERY_QUOTA_RPCS.map(AppServerRpc::as_str),
[
"account/read",
"account/rateLimits/read",
"model/list",
"modelProvider/capabilities/read"
]
);
let mut unsupported = current_probe_json();
unsupported["advertised_rpcs"] = json!(["turn/start"]);
assert_eq!(parse(&unsupported), Err(CapabilityInputError::InvalidInput));
}
#[test]
fn unknown_and_token_shaped_fields_are_rejected_without_echo() {
let mut unknown = current_probe_json();
unknown["future_contract"] = json!(true);
assert_eq!(parse(&unknown), Err(CapabilityInputError::InvalidInput));
let canary = "secret-capability-canary";
for field in ["access_token", "refreshToken", "authorization", "api_key"] {
let mut sensitive = current_probe_json();
sensitive[field] = json!(canary);
let error = parse(&sensitive).expect_err("sensitive input must be rejected");
assert_eq!(error, CapabilityInputError::SensitiveInputRejected);
assert!(!format!("{error:?} {error}").contains(canary));
}
}
#[test]
fn forbidden_auth_sources_are_rejected_even_when_nested_in_values() {
for canary in [
"CODEX_ACCESS_TOKEN",
"OPENAI_API_KEY",
"requires_openai_auth",
"/tmp/.codex/auth.json",
] {
let payload = format!("{{\"future_contract\":\"{canary}\"}}");
assert_eq!(
CodexAppServerProbe::from_json(payload.as_bytes()),
Err(CapabilityInputError::SensitiveInputRejected)
);
}
}
#[test]
fn version_mismatch_is_explicit_and_remains_fail_closed() {
let mut mismatched = current_probe_json();
mismatched["standalone_cli"]["version"] = json!("0.999.0");
let receipt = parse(&mismatched)
.expect("valid mismatch fixture")
.capability_receipt();
assert!(
receipt
.unavailable_reasons
.contains(&CodexSubscriptionUnavailableReason::ProtocolMismatch)
);
assert!(!receipt.dispatch_available());
}
#[test]
fn missing_managed_daemon_is_explicit() {
let receipt = CodexAppServerProbe::current_observed().capability_receipt();
assert!(
receipt
.unavailable_reasons
.contains(&CodexSubscriptionUnavailableReason::MissingManagedDaemon)
);
}
#[test]
fn unsupported_auth_mode_is_explicit() {
let mut api_key = current_probe_json();
api_key["auth_mode"] = json!("api_key");
let receipt = parse(&api_key)
.expect("valid unsupported auth fixture")
.capability_receipt();
assert!(
receipt
.unavailable_reasons
.contains(&CodexSubscriptionUnavailableReason::UnsupportedAuthMode)
);
assert!(!receipt.dispatch_available());
}
#[test]
fn current_receipt_is_harness_managed_and_cannot_dispatch() {
let receipt = CodexAppServerProbe::current_observed().capability_receipt();
assert_eq!(receipt.openai_transport(), TransportKind::HarnessManaged);
assert!(!receipt.raw_inference_dispatch());
assert!(!receipt.desktop_external_attach());
assert!(!receipt.dispatch_available());
assert_eq!(
receipt.unavailable_reasons,
vec![
CodexSubscriptionUnavailableReason::NoRawInferenceRpc,
CodexSubscriptionUnavailableReason::DesktopExternalAttachmentUnsupported,
CodexSubscriptionUnavailableReason::MissingManagedDaemon,
]
);
assert_eq!(
receipt.supported_discovery_quota_rpcs,
SUPPORTED_DISCOVERY_QUOTA_RPCS.into_iter().collect()
);
let encoded = serde_json::to_value(&receipt).expect("serialize capability receipt");
assert_eq!(encoded["openai_transport"], "harness_managed");
assert_eq!(encoded["raw_inference_dispatch"], false);
assert_eq!(encoded["desktop_external_attach"], false);
}
}