diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 221bab821c..30d567b26b 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -4574,6 +4574,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "codex-runtime-agent-instance" +version = "0.0.0" +dependencies = [ + "codex-utils-redacted-string", + "pretty_assertions", + "serde", + "serde_json", + "uuid", +] + [[package]] name = "codex-sandboxing" version = "0.0.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index e588cd6e59..5e1b303315 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -102,6 +102,7 @@ members = [ "rollout-trace", "rmcp-client", "responses-api-proxy", + "runtime-agent-instance", "response-debug-context", "sandboxing", "stdio-to-uds", diff --git a/codex-rs/runtime-agent-instance/BUILD.bazel b/codex-rs/runtime-agent-instance/BUILD.bazel new file mode 100644 index 0000000000..c1f2245ebe --- /dev/null +++ b/codex-rs/runtime-agent-instance/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "runtime-agent-instance", + crate_name = "codex_runtime_agent_instance", +) diff --git a/codex-rs/runtime-agent-instance/Cargo.toml b/codex-rs/runtime-agent-instance/Cargo.toml new file mode 100644 index 0000000000..1398925d35 --- /dev/null +++ b/codex-rs/runtime-agent-instance/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "codex-runtime-agent-instance" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +doctest = false + +[lints] +workspace = true + +[dependencies] +codex-utils-redacted-string = { workspace = true } +serde = { workspace = true, features = ["derive"] } +uuid = { workspace = true, features = ["serde"] } + +[dev-dependencies] +pretty_assertions = { workspace = true } +serde_json = { workspace = true } diff --git a/codex-rs/runtime-agent-instance/src/bootstrap.rs b/codex-rs/runtime-agent-instance/src/bootstrap.rs new file mode 100644 index 0000000000..1e2804e262 --- /dev/null +++ b/codex-rs/runtime-agent-instance/src/bootstrap.rs @@ -0,0 +1,71 @@ +//! Wire types for one-call runtime registration. Credentials are inputs, not identity claims. + +use codex_utils_redacted_string::RedactedString; +use serde::Deserialize; +use serde::Serialize; +use uuid::Uuid; + +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum AuthenticatedActorAuthorization { + Oauth { credential: RedactedString }, + PersonalAccessToken { credential: RedactedString }, + PlatformApiKey { credential: RedactedString }, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum AgentHostAuthorization { + // An installation ID is tracking metadata, not proof of device ownership. + InstallationId { installation_id: Uuid }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeRole { + Orchestrator, + Executor, + OrchestratorAndExecutor, +} + +#[derive(Clone, Debug, Serialize)] +pub struct RuntimeRegistrationMetadata { + #[serde(skip_serializing_if = "Option::is_none")] + pub harness: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub harness_version: Option, + pub role: RuntimeRole, +} + +#[derive(Clone, Debug, Serialize)] +pub struct RuntimeRegistrationRequest { + pub actor_auth: AuthenticatedActorAuthorization, + pub agent_host_auth: AgentHostAuthorization, + pub public_key: String, + pub metadata: RuntimeRegistrationMetadata, +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +pub struct RuntimeRegistrationResponse { + pub runtime_agent_instance_id: String, + pub agent_host_id: String, + pub actor: ActorInfo, +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum ActorInfo { + Chatgpt { + user_id: String, + account_id: String, + }, + Platform { + user_id: String, + organization_id: String, + project_id: Option, + }, +} + +#[cfg(test)] +#[path = "bootstrap_tests.rs"] +mod tests; diff --git a/codex-rs/runtime-agent-instance/src/bootstrap_tests.rs b/codex-rs/runtime-agent-instance/src/bootstrap_tests.rs new file mode 100644 index 0000000000..8a4635a7fc --- /dev/null +++ b/codex-rs/runtime-agent-instance/src/bootstrap_tests.rs @@ -0,0 +1,97 @@ +//! Registration wire compatibility for each supported actor entry point. + +use super::*; +use pretty_assertions::assert_eq; +use serde_json::json; + +#[test] +fn registration_serializes_the_supported_actor_authorizations() { + let installation_id = "00000000-0000-4000-8000-000000000001"; + for (authorization, expected) in [ + ( + AuthenticatedActorAuthorization::Oauth { + credential: "secret".into(), + }, + json!({"type": "oauth", "credential": "secret"}), + ), + ( + AuthenticatedActorAuthorization::PersonalAccessToken { + credential: "secret".into(), + }, + json!({"type": "personal_access_token", "credential": "secret"}), + ), + ( + AuthenticatedActorAuthorization::PlatformApiKey { + credential: "secret".into(), + }, + json!({"type": "platform_api_key", "credential": "secret"}), + ), + ] { + let request = RuntimeRegistrationRequest { + actor_auth: authorization, + agent_host_auth: AgentHostAuthorization::InstallationId { + installation_id: installation_id.parse().unwrap(), + }, + public_key: "ssh-ed25519 public-key".into(), + metadata: RuntimeRegistrationMetadata { + harness: Some("codex".into()), + harness_version: Some("0.0.0".into()), + role: RuntimeRole::OrchestratorAndExecutor, + }, + }; + assert!(!format!("{request:?}").contains("secret")); + assert_eq!( + serde_json::to_value(request).unwrap(), + json!({ + "actor_auth": expected, + "agent_host_auth": {"type": "installation_id", "installation_id": installation_id}, + "public_key": "ssh-ed25519 public-key", + "metadata": {"harness": "codex", "harness_version": "0.0.0", "role": "orchestrator_and_executor"}, + }) + ); + } +} + +#[test] +fn registration_decodes_composite_actor_contexts() { + for (actor, expected) in [ + ( + json!({"kind": "chatgpt", "user_id": "user", "account_id": "account"}), + ActorInfo::Chatgpt { + user_id: "user".into(), + account_id: "account".into(), + }, + ), + ( + json!({"kind": "platform", "user_id": "user", "organization_id": "org", "project_id": "project"}), + ActorInfo::Platform { + user_id: "user".into(), + organization_id: "org".into(), + project_id: Some("project".into()), + }, + ), + ] { + let response: RuntimeRegistrationResponse = serde_json::from_value(json!({ + "runtime_agent_instance_id": "runtime", "agent_host_id": "host", "actor": actor, + })) + .unwrap(); + assert_eq!( + response, + RuntimeRegistrationResponse { + runtime_agent_instance_id: "runtime".into(), + agent_host_id: "host".into(), + actor: expected, + } + ); + } + assert!( + serde_json::from_value::( + json!({"runtime_agent_instance_id": "runtime"}) + ) + .is_err() + ); + assert!( + serde_json::from_value::(json!({"kind": "platform", "user_id": "user"})) + .is_err() + ); +} diff --git a/codex-rs/runtime-agent-instance/src/lib.rs b/codex-rs/runtime-agent-instance/src/lib.rs new file mode 100644 index 0000000000..3fceaf5c10 --- /dev/null +++ b/codex-rs/runtime-agent-instance/src/lib.rs @@ -0,0 +1,11 @@ +//! OpenAI runtime registration types, independent of credential storage and model providers. + +mod bootstrap; + +pub use bootstrap::ActorInfo; +pub use bootstrap::AgentHostAuthorization; +pub use bootstrap::AuthenticatedActorAuthorization; +pub use bootstrap::RuntimeRegistrationMetadata; +pub use bootstrap::RuntimeRegistrationRequest; +pub use bootstrap::RuntimeRegistrationResponse; +pub use bootstrap::RuntimeRole;