diff --git a/epiphany-openai-codex-spine/Cargo.toml b/epiphany-openai-codex-spine/Cargo.toml index 5c4b24bd..95de66e0 100644 --- a/epiphany-openai-codex-spine/Cargo.toml +++ b/epiphany-openai-codex-spine/Cargo.toml @@ -19,3 +19,17 @@ codex-model-provider-info = { path = "../vendor/codex/codex-rs/model-provider-in codex-protocol = { path = "../vendor/codex/codex-rs/protocol" } epiphany-openai-adapter = { path = "../epiphany-openai-adapter" } futures = "0.3" +serde = "1" +serde_json = "1" +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } + +[[bin]] +name = "epiphany-openai-spine" +path = "src/bin/epiphany-openai-spine.rs" + +[patch.crates-io] +tokio-tungstenite = { git = "https://github.com/openai-oss-forks/tokio-tungstenite", rev = "132f5b39c862e3a970f731d709608b3e6276d5f6" } +tungstenite = { git = "https://github.com/openai-oss-forks/tungstenite-rs", rev = "9200079d3b54a1ff51072e24d81fd354f085156f" } + +[patch."ssh://git@github.com/openai-oss-forks/tungstenite-rs.git"] +tungstenite = { git = "https://github.com/openai-oss-forks/tungstenite-rs", rev = "9200079d3b54a1ff51072e24d81fd354f085156f" } diff --git a/epiphany-openai-codex-spine/src/bin/epiphany-openai-spine.rs b/epiphany-openai-codex-spine/src/bin/epiphany-openai-spine.rs new file mode 100644 index 00000000..3776bb18 --- /dev/null +++ b/epiphany-openai-codex-spine/src/bin/epiphany-openai-spine.rs @@ -0,0 +1,154 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Context; +use anyhow::Result; +use anyhow::anyhow; +use codex_login::AuthCredentialsStoreMode; +use codex_login::AuthManager; +use codex_login::CodexAuth; +use epiphany_openai_adapter::EpiphanyOpenAiInputItem; +use epiphany_openai_adapter::EpiphanyOpenAiModelRequest; +use epiphany_openai_codex_spine::EpiphanyCodexOpenAiTransport; +use epiphany_openai_codex_spine::responses_request_from_epiphany; +use epiphany_openai_codex_spine::status_from_auth_manager; +use epiphany_openai_codex_spine::status_from_codex_auth; +use serde_json::json; + +#[tokio::main] +async fn main() -> Result<()> { + let mut args = env::args().skip(1); + let command = args.next().unwrap_or_else(|| "usage".to_string()); + match command.as_str() { + "status" => { + let options = parse_common_options(args.collect())?; + let auth_manager = auth_manager(options.codex_home); + let status = status_from_auth_manager( + &auth_manager, + options.default_model, + /*supports_websockets*/ true, + ) + .await; + print_json(&status)?; + } + "model-turn" => { + let options = parse_model_turn_options(args.collect())?; + let request_text = fs::read_to_string(&options.request_path) + .with_context(|| format!("failed to read {}", options.request_path.display()))?; + let request: EpiphanyOpenAiModelRequest = serde_json::from_str(&request_text) + .with_context(|| format!("failed to parse {}", options.request_path.display()))?; + let transport = EpiphanyCodexOpenAiTransport::openai(auth_manager(options.codex_home)); + let events = transport.collect_model_events(request).await?; + print_json(&events)?; + } + "smoke" => { + let auth = CodexAuth::from_api_key("test-key"); + let status = status_from_codex_auth(Some(&auth), Some("gpt-5.4".to_string()), true); + let mut request = EpiphanyOpenAiModelRequest::new( + "smoke-request", + "smoke-conversation", + "gpt-5.4", + "Answer with one sentence.", + ); + request.input.push(EpiphanyOpenAiInputItem::UserText { + text: "smoke".to_string(), + }); + let mapped = responses_request_from_epiphany(request)?; + print_json(&json!({ + "status": status, + "mappedModel": mapped.model, + "mappedInputItems": mapped.input.len(), + "transport": "not-opened" + }))?; + } + _ => { + return Err(anyhow!( + "usage: epiphany-openai-spine [--codex-home path] [--default-model model] [--request path]" + )); + } + } + Ok(()) +} + +struct CommonOptions { + codex_home: PathBuf, + default_model: Option, +} + +struct ModelTurnOptions { + codex_home: PathBuf, + request_path: PathBuf, +} + +fn parse_common_options(args: Vec) -> Result { + let mut codex_home = default_codex_home()?; + let mut default_model = None; + let mut iter = args.into_iter(); + while let Some(arg) = iter.next() { + match arg.as_str() { + "--codex-home" => { + codex_home = PathBuf::from(next_value(&mut iter, "--codex-home")?); + } + "--default-model" => { + default_model = Some(next_value(&mut iter, "--default-model")?); + } + other => return Err(anyhow!("unknown status argument: {other}")), + } + } + Ok(CommonOptions { + codex_home, + default_model, + }) +} + +fn parse_model_turn_options(args: Vec) -> Result { + let mut codex_home = default_codex_home()?; + let mut request_path = None; + let mut iter = args.into_iter(); + while let Some(arg) = iter.next() { + match arg.as_str() { + "--codex-home" => { + codex_home = PathBuf::from(next_value(&mut iter, "--codex-home")?); + } + "--request" => { + request_path = Some(PathBuf::from(next_value(&mut iter, "--request")?)); + } + other => return Err(anyhow!("unknown model-turn argument: {other}")), + } + } + Ok(ModelTurnOptions { + codex_home, + request_path: request_path.context("model-turn requires --request")?, + }) +} + +fn next_value(iter: &mut impl Iterator, name: &str) -> Result { + iter.next() + .ok_or_else(|| anyhow!("{name} requires a value")) +} + +fn default_codex_home() -> Result { + if let Ok(path) = env::var("CODEX_HOME") { + return Ok(PathBuf::from(path)); + } + let home = env::var("USERPROFILE") + .or_else(|_| env::var("HOME")) + .context("CODEX_HOME is unset and no home directory environment variable exists")?; + Ok(PathBuf::from(home).join(".codex")) +} + +fn auth_manager(codex_home: PathBuf) -> Arc { + AuthManager::shared( + codex_home, + /*enable_codex_api_key_env*/ true, + AuthCredentialsStoreMode::File, + /*chatgpt_base_url*/ None, + ) +} + +fn print_json(value: &T) -> Result<()> { + println!("{}", serde_json::to_string_pretty(value)?); + Ok(()) +} diff --git a/notes/codex-auth-spine-inventory.md b/notes/codex-auth-spine-inventory.md index de6338ed..7c7f9a87 100644 --- a/notes/codex-auth-spine-inventory.md +++ b/notes/codex-auth-spine-inventory.md @@ -141,5 +141,20 @@ contracts in its hello/schema catalog. This is the contract bridge the native runtime should consume next. Do not add another JSON-RPC model endpoint and call it progress. +The first native operator/debug edge now exists as the +`epiphany-openai-spine` binary in `epiphany-openai-codex-spine`. It can report +typed adapter status and consume a serialized `EpiphanyOpenAiModelRequest` +document for a model turn without going through Codex app-server JSON-RPC. This +is not the final CultNet daemon. It is a buildable extracted edge that proves +the spine can be called from Epiphany-owned code and gives the next cut a place +to route through while the old `thread/epiphany/*` model path is starved. + +The bad news is useful: the current spine still drags in `codex-api` and its +large transport dependency stack. That is tolerable as a sealed compatibility +reliquary, but not as the final shape. The next purification should shrink the +surviving Codex dependency surface to credential loading/refresh, provider +auth, model selection, and the smallest OpenAI Responses HTTP call that keeps +Codex subscription compatibility. + The point is ownership: Epiphany calls a model adapter; it does not live inside the Codex host brain. diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index ee1221e3..2da9f7cd 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -567,6 +567,16 @@ plus the coordinator-owned model request contract. The native runtime should consume that contract next; do not make a new JSON-RPC model endpoint and pretend the whale got lighter. +The `epiphany-openai-spine` binary is now the first native edge for that +wrapper. It can print typed adapter status and consume a serialized +`EpiphanyOpenAiModelRequest` document for a model turn outside Codex +app-server. Treat its JSON as CLI/file-edge serialization of typed documents, +not internal data cargo. The current wrapper still pulls `codex-api` and a +large dependency stack, so the next real cut is not celebration; it is routing +native runtime/CultNet model calls through this spine and then shrinking the +surviving Codex dependency surface to auth, provider/model routing, and the +smallest OpenAI Responses call needed for subscription compatibility. + Also: MCP itself is allowed to be JSON. The target is not "replace MCP JSON"; the target is an Epiphany-owned boundary that speaks typed Epiphany intent/result/receipt documents internally and normal MCP JSON-RPC externally. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index be1f785a..b472b069 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index e5e39963..8090ddee 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -279,5 +279,5 @@ rejected_paths: reason: making Epiphany cleaner inside Codex still preserves the wrong organ; Codex must be starved down to OpenAI subscription auth/model transport, while Epiphany data and subsystem communication move to typed CultCache/CultNet. current_status: - summary: The Codex starvation pass has five typing receipts, seventeen processor cuts, a real physical extraction in `epiphany-codex-bridge`, a source-grounded Codex auth spine inventory, the pure native `epiphany-openai-adapter` document boundary, and a workspace-verified Codex auth/model wrapper in `epiphany-openai-codex-spine`. The bridge owns Codex-facing Epiphany adapters for scene, jobs, context/planning/graph query, retrieval, result projection, runtime-spine result adaptation, launch/prompt doctrine, mutation document mechanics, live-state projection, pressure/pre-compaction rendering, freshness/reorientation projection, CRRC/coordinator/role-board projection, and acceptance/evidence signal helpers. Vendored app-server now retains only `epiphany_invalidation.rs` as a root Epiphany module because watcher lifecycle is still app-server machinery; `codex_message_processor.rs` still contains route dispatch and mutation orchestration. The processor is about 10,339 lines, `epiphany_mutation_routes.rs` is about 1,071 lines, and `epiphany_state_helpers.rs` is down to a 19-line rollout-state loader. `epiphany-openai-adapter` defines typed request/event/receipt/auth-status documents with no Codex app-server or generic JSON dependency. `epiphany-openai-codex-spine` projects `AuthManager` / `CodexAuth` into typed `EpiphanyOpenAiAdapterStatus`, maps typed `EpiphanyOpenAiModelRequest` into Codex API `ResponsesApiRequest`, resolves auth/provider through keeper Codex crates, opens HTTP Responses streams through `codex-api`, and maps deltas/completion into typed stream events and receipts. `epiphany-runtime-spine` now advertises OpenAI adapter status/model-request/stream-event/receipt CultNet schemas and mutation contracts, so the next caller can be native instead of JSON-RPC. MCP itself is correctly JSON at its protocol edge; the target is typed Epiphany intent/result/receipt wrappers around MCP, not replacing MCP JSON. - next_action: Continue Codex starvation by making an Epiphany-native runtime surface consume the advertised OpenAI adapter CultNet contract and call the typed spine directly, then starve the old model-turn path out of `thread/epiphany/*` JSON-RPC and `codex_message_processor.rs`, without adding Codex app-server, apps, skills, marketplace, or Epiphany JSON-RPC dependencies. Do not resume app/skill/marketplace expansion. + summary: The Codex starvation pass has five typing receipts, seventeen processor cuts, a real physical extraction in `epiphany-codex-bridge`, a source-grounded Codex auth spine inventory, the pure native `epiphany-openai-adapter` document boundary, and a buildable Codex auth/model wrapper in `epiphany-openai-codex-spine`. The bridge owns Codex-facing Epiphany adapters for scene, jobs, context/planning/graph query, retrieval, result projection, runtime-spine result adaptation, launch/prompt doctrine, mutation document mechanics, live-state projection, pressure/pre-compaction rendering, freshness/reorientation projection, CRRC/coordinator/role-board projection, and acceptance/evidence signal helpers. Vendored app-server now retains only `epiphany_invalidation.rs` as a root Epiphany module because watcher lifecycle is still app-server machinery; `codex_message_processor.rs` still contains route dispatch and mutation orchestration. The processor is about 10,339 lines, `epiphany_mutation_routes.rs` is about 1,071 lines, and `epiphany_state_helpers.rs` is down to a 19-line rollout-state loader. `epiphany-openai-adapter` defines typed request/event/receipt/auth-status documents with no Codex app-server or generic JSON dependency. `epiphany-openai-codex-spine` projects `AuthManager` / `CodexAuth` into typed `EpiphanyOpenAiAdapterStatus`, maps typed `EpiphanyOpenAiModelRequest` into Codex API `ResponsesApiRequest`, resolves auth/provider through keeper Codex crates, opens HTTP Responses streams through `codex-api`, and maps deltas/completion into typed stream events and receipts. The `epiphany-openai-spine` binary is the first native edge: it prints typed status and can consume serialized typed request documents outside Codex app-server JSON-RPC. Treat that JSON as CLI/file-edge serialization, not internal authority. `epiphany-runtime-spine` advertises OpenAI adapter status/model-request/stream-event/receipt CultNet schemas and mutation contracts, so the next caller can be native instead of JSON-RPC. MCP itself is correctly JSON at its protocol edge; the target is typed Epiphany intent/result/receipt wrappers around MCP, not replacing MCP JSON. The current OpenAI spine still pulls `codex-api` and a large transport dependency stack, so its next cut is dependency starvation after native routing exists. + next_action: Continue Codex starvation by routing an Epiphany-native runtime/CultNet model-call surface through the typed OpenAI spine, then starve the old model-turn path out of `thread/epiphany/*` JSON-RPC and `codex_message_processor.rs`. After routing is real, shrink the surviving Codex dependency surface to credential refresh, provider/model routing, and the smallest OpenAI Responses call required for Codex subscription compatibility. Do not add Codex app-server, apps, skills, marketplace, or new Epiphany JSON-RPC dependencies. diff --git a/vendor/codex/codex-rs/Cargo.lock b/vendor/codex/codex-rs/Cargo.lock index 5fb3130a..59113857 100644 --- a/vendor/codex/codex-rs/Cargo.lock +++ b/vendor/codex/codex-rs/Cargo.lock @@ -5275,6 +5275,9 @@ dependencies = [ "codex-protocol", "epiphany-openai-adapter", "futures", + "serde", + "serde_json", + "tokio", ] [[package]]