Files
zeekay ad468d97af upstream: the fork stands alone; Dev fills its two hooks
hanzoai/codex names nothing outside its tree now (d1d710452): Dev's names,
defaults, model catalog, `/model <name>` match, controller interface and
version live in its `hanzo` crate, and Dev's crates depend on that, never
the reverse. hanzo-loop moves there whole; hanzo-tui keeps only the Hanzo
account; hanzo-config reads API_BASE, DEFAULT_MODEL and is_hanzo from it.

hanzo-dev's main registers what Dev adds before anything starts:
`codex_app_server::register(hanzo_kai::install)` puts Kai on every thread,
and `codex_tui::register(Account { login, logout, show })` puts the Hanzo
CLI's sign-in behind /login, /logout, /auth and /status. launch.rs holds
that /login is offered, which it is only when an account is registered.
make test-upstream runs locked against the fork's own lock.
2026-10-15 10:03:29 -07:00

237 lines
7.9 KiB
Rust

//! Automatic approval review and memory run on models the provider serves.
//!
//! Upstream's own ids for these jobs are OpenAI's, and the bundled catalog keeps
//! OpenAI's entries, unlisted, in every provider's catalog. A reviewer that took
//! "in the catalog" for "served" asked api.hanzo.ai and the LAN engines for a
//! model neither has.
use std::process::Stdio;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use codex_model_provider::create_model_provider;
use codex_model_provider_info::ModelProviderInfo;
use codex_protocol::openai_models::ModelVisibility;
use serde_json::Value;
use serde_json::json;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::Request;
use wiremock::Respond;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
fn provider(table: &str) -> ModelProviderInfo {
toml::from_str(table).expect("a provider table parses")
}
fn hanzo() -> ModelProviderInfo {
let config: toml::Value =
toml::from_str(hanzo_config::DEFAULT_CONFIG).expect("the default config parses");
config["model_providers"]["hanzo"]
.clone()
.try_into()
.expect("the default config's provider parses")
}
#[test]
fn hanzo_names_a_model_it_serves_for_review_and_memory() {
let hanzo = create_model_provider(hanzo(), None);
for model in [
hanzo.approval_review_preferred_model(),
hanzo.memory_extraction_preferred_model(),
hanzo.memory_consolidation_preferred_model(),
] {
assert_eq!(model, hanzo::DEFAULT_MODEL);
let listed = codex_models_manager::bundled_models_response()
.expect("the bundled catalog parses")
.models
.into_iter()
.any(|known| known.slug == model && matches!(known.visibility, ModelVisibility::List));
assert!(
listed,
"{model} is listed in the catalog api.hanzo.ai serves"
);
}
}
#[test]
fn only_hanzo_takes_the_hanzo_choice() {
let openai = create_model_provider(ModelProviderInfo::create_openai_provider(None), None);
let lan = create_model_provider(
provider(
"name = \"LAN\"\nbase_url = \"http://dgx.local:1235/v1\"\nwire_api = \"responses\"",
),
None,
);
for other in [openai, lan] {
assert_ne!(
other.approval_review_preferred_model(),
hanzo::DEFAULT_MODEL
);
assert_ne!(
other.memory_extraction_preferred_model(),
hanzo::DEFAULT_MODEL
);
assert_ne!(
other.memory_consolidation_preferred_model(),
hanzo::DEFAULT_MODEL
);
}
}
fn sse(events: &[Value]) -> String {
events
.iter()
.map(|event| {
format!(
"event: {}\ndata: {event}\n\n",
event["type"].as_str().unwrap_or_default()
)
})
.collect()
}
fn created(id: &str) -> Value {
json!({"type": "response.created", "response": {"id": id}})
}
fn completed(id: &str) -> Value {
json!({
"type": "response.completed",
"response": {
"id": id,
"usage": {"input_tokens": 0, "input_tokens_details": null, "output_tokens": 0,
"output_tokens_details": null, "total_tokens": 0}
}
})
}
fn message(id: &str, text: &str) -> Value {
json!({
"type": "response.output_item.done",
"item": {"type": "message", "role": "assistant", "id": id,
"content": [{"type": "output_text", "text": text}]}
})
}
/// A Responses engine that asks once for an escalated command, allows it when
/// asked to review, and finishes once it sees the command's output.
#[derive(Clone, Default)]
struct Engine {
reviews: Arc<Mutex<Vec<String>>>,
outputs: Arc<Mutex<Vec<String>>>,
}
impl Respond for Engine {
fn respond(&self, request: &Request) -> ResponseTemplate {
let body: Value = serde_json::from_slice(&request.body).unwrap_or_default();
let reviewer = body["client_metadata"]["x-openai-subagent"] == "guardian"
|| request
.headers
.get("x-openai-subagent")
.and_then(|value| value.to_str().ok())
== Some("guardian");
let outputs: Vec<String> = body["input"]
.as_array()
.into_iter()
.flatten()
.filter(|item| item["type"] == "function_call_output")
.map(|item| item["output"].to_string())
.collect();
let events = if reviewer {
let model = body["model"].as_str().unwrap_or_default().to_string();
self.reviews.lock().expect("reviews").push(model);
vec![
created("review"),
message("assessment", r#"{"outcome":"allow"}"#),
completed("review"),
]
} else if !outputs.is_empty() {
self.outputs.lock().expect("outputs").extend(outputs);
vec![created("done"), message("reply", "done"), completed("done")]
} else {
let arguments = json!({
"cmd": "printf reviewed",
"sandbox_permissions": "require_escalated",
"justification": "Run the step the user asked for.",
});
vec![
created("act"),
json!({
"type": "response.output_item.done",
"item": {"type": "function_call", "call_id": "act", "name": "exec_command",
"arguments": arguments.to_string()}
}),
completed("act"),
]
};
ResponseTemplate::new(200)
.insert_header("content-type", "text/event-stream")
.set_body_string(sse(&events))
}
}
/// A provider that is neither OpenAI nor Hanzo and has no catalog of its own (a
/// LAN engine named with `-c model_provider=`) is reviewed by the model it is
/// already running, never by an id it was not asked to serve.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_provider_off_hanzo_reviews_on_the_model_it_runs() {
let server = MockServer::start().await;
let engine = Engine::default();
Mock::given(method("POST"))
.and(path("/v1/responses"))
.respond_with(engine.clone())
.mount(&server)
.await;
let home = tempfile::tempdir().expect("a home");
let work = tempfile::tempdir().expect("a project");
std::fs::write(
home.path().join("config.toml"),
format!(
"model_provider = \"lan\"\n\n[model_providers.lan]\nname = \"LAN\"\nbase_url = \"{}/v1\"\nwire_api = \"responses\"\n",
server.uri()
),
)
.expect("the profile is writable");
let run = tokio::process::Command::new(env!("CARGO_BIN_EXE_dev"))
.args([
"exec",
"--skip-git-repo-check",
"--approve-for-me",
"-m",
"zen6",
"-C",
])
.arg(work.path())
.arg("run the protected step")
.env("DEV_HOME", home.path())
.stdin(Stdio::null())
.output();
let output = tokio::time::timeout(Duration::from_secs(180), run)
.await
.expect("the run finishes")
.expect("dev runs");
let reviews = engine.reviews.lock().expect("reviews").clone();
let outputs = engine.outputs.lock().expect("outputs").clone();
assert!(
output.status.success(),
"dev exec failed\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(!reviews.is_empty(), "the escalated command was reviewed");
assert!(
reviews.iter().all(|model| model == "zen6"),
"reviewed on {reviews:?}"
);
assert!(
outputs.iter().any(|output| output.contains("reviewed")),
"the allowed command ran: {outputs:?}"
);
}