From 47db27d8996f78376486798df3c2a8869386e5d4 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar Date: Thu, 22 Oct 2026 03:15:23 +0530 Subject: [PATCH 1/2] harness: add wally command-code (Command Code via providers.json) --- docs/EDITORS.md | 10 + src/app.rs | 2 + src/cli_formatter.rs | 5 + src/harness/agents.rs | 29 +- src/harness/command_code.rs | 677 ++++++++++++++++++ src/harness/harness.rs | 11 +- src/harness/mod.rs | 4 +- tests/golden/README.md | 4 + tests/golden/cases.json | 17 +- .../alt_help__--no-color__--help.stdout | 1 + tests/golden/expected/alt_help__-h.stdout | 1 + tests/golden/expected/alt_help__help.stdout | 1 + .../expected/alt_help__help__nope.stdout | 1 + tests/golden/expected/help__ROOT.stdout | 1 + tests/golden/expected/help__command-code.code | 1 + .../golden/expected/help__command-code.stderr | 0 .../golden/expected/help__command-code.stdout | 21 + tests/golden/expected/help__help.stdout | 1 + .../expected/help__help__command-code.code | 1 + .../expected/help__help__command-code.stderr | 0 .../expected/help__help__command-code.stdout | 21 + tests/golden/expected/parse__--bogus.stderr | 1 + .../golden/expected/parse__-U__--json.stderr | 1 + tests/golden/expected/parse__-u__-v.stderr | 1 + tests/golden/expected/parse__-z.stderr | 1 + tests/golden/expected/unknown__auth.stderr | 1 + tests/golden/expected/unknown__chat.stderr | 1 + tests/golden/expected/unknown__diarize.stderr | 1 + tests/golden/expected/unknown__embed.stderr | 1 + tests/golden/expected/unknown__foo.stderr | 1 + tests/golden/expected/unknown__image.stderr | 1 + tests/golden/expected/unknown__list.stderr | 1 + tests/golden/expected/unknown__lora.stderr | 1 + tests/golden/expected/unknown__ls.stderr | 1 + tests/golden/expected/unknown__pull.stderr | 1 + tests/golden/expected/unknown__rag.stderr | 1 + tests/golden/expected/unknown__rerank.stderr | 1 + tests/golden/expected/unknown__rm.stderr | 1 + tests/golden/expected/unknown__segment.stderr | 1 + tests/golden/expected/unknown__show.stderr | 1 + tests/golden/expected/unknown__stt.stderr | 1 + tests/golden/expected/unknown__tts.stderr | 1 + tests/golden/expected/unknown__vad.stderr | 1 + tests/golden/expected/unknown__vlm.stderr | 1 + tests/golden/expected/unknown__voice.stderr | 1 + tests/golden/expected/version__bare.stderr | 1 + 46 files changed, 826 insertions(+), 8 deletions(-) create mode 100644 src/harness/command_code.rs create mode 100644 tests/golden/expected/help__command-code.code create mode 100644 tests/golden/expected/help__command-code.stderr create mode 100644 tests/golden/expected/help__command-code.stdout create mode 100644 tests/golden/expected/help__help__command-code.code create mode 100644 tests/golden/expected/help__help__command-code.stderr create mode 100644 tests/golden/expected/help__help__command-code.stdout diff --git a/docs/EDITORS.md b/docs/EDITORS.md index eea8d31e..9fbbc347 100644 --- a/docs/EDITORS.md +++ b/docs/EDITORS.md @@ -30,6 +30,7 @@ tool starts the way you already have it configured, and wally wires nothing. | `openclaw` | your own `openclaw.json` plus one provider, written for the run and named by `OPENCLAW_CONFIG_PATH` | | `deepseek` | a `--patch` overlay on the argv that adds the provider to dsh's `llm-pi-ai` row; nothing enters `$DSH_HOME` | | `prime-agent` | a temporary extension loaded with `-e` that registers the provider for the run, with the key in `RUNANYWHERE_API_KEY`; nothing enters `~/.prime/agent` | +| `command-code` | a `runanywhere-wally` provider merged into `~/.commandcode/providers.json` (it reads providers from nowhere else) and taken out when it exits: the original bytes go back if the file is unchanged, otherwise only that provider comes out. The file names `$WALLY_COMMAND_CODE_API_KEY` rather than holding the key. A local model, or a person with no Command Code sign-in, runs with `CMD_LOCAL_ONLY=1` (no Command Code gateway models, `/share`, `/usage` or server-side web tools); with no sign-in a placeholder `COMMAND_CODE_API_KEY` gets past Command Code's login screen. `--skip-onboarding --no-auto-update` are passed, `-t` is left to you. One run at a time (a lock in `~/.commandcode/`). Its own subcommands (`-- status`, `-- login`, …) run untouched; `-- acp` is wired, and there the editor picks the model | Two flags go with `-m`. `--serve` holds the endpoint open and prints it instead of launching anything, which is how a tool nobody has taught wally about gets @@ -76,6 +77,15 @@ runs at `agent.reasoning_effort` in `~/.hermes/config.yaml` (per model under each model's levels up to its highest and starts at the app's own default. Prime Agent is given no reasoning levels yet: its models run at the endpoint's default, and a level has to be named in Prime Agent itself. +Command Code is given each model's levels from `low` to `max` as +`reasoningEfforts` (`--effort`, or `/model`), and starts at its own default. +The endpoint attributes its traffic as `unknown` until the console learns a +`command_code` harness. +Wally ends when Command Code ends: Ctrl-C reaches both, and wally cleans up +after the tool exits. A signal sent to wally alone leaves it waiting for the +tool; signal the process group to stop both. A `kill -9` leaves the provider in +`providers.json` until the next `wally command-code` drops it, and a bare +`cmd -c` afterwards cannot find that provider for a resumed session. Codex is told the model's context window through the catalog file, so its context meter and auto-compaction work against the real limit. Its hosted web diff --git a/src/app.rs b/src/app.rs index e4fd1a98..64690462 100644 --- a/src/app.rs +++ b/src/app.rs @@ -133,6 +133,7 @@ pub fn configure_app(app: &mut App) { "openclaw", "deepseek", "prime-agent", + "command-code", ], }, Section { @@ -176,6 +177,7 @@ fn is_passthrough_command(token: &str) -> bool { "openclaw", "deepseek", "prime-agent", + "command-code", ]; NAMES.contains(&token) } diff --git a/src/cli_formatter.rs b/src/cli_formatter.rs index b381a525..3eac2828 100644 --- a/src/cli_formatter.rs +++ b/src/cli_formatter.rs @@ -1212,6 +1212,11 @@ mod tests { "Open Prime Agent with a model", "Coding tools", ), + leaf( + "command-code", + "Open Command Code with a model", + "Coding tools", + ), namespace( "account", "Account", diff --git a/src/harness/agents.rs b/src/harness/agents.rs index 612c47a3..b76c974b 100644 --- a/src/harness/agents.rs +++ b/src/harness/agents.rs @@ -27,6 +27,10 @@ pub enum Handoff { /// Prime Agent 0.10 has no `-e` extension flag. The provider is written /// into `models.json` in the agent directory. Extension, + /// Command Code reads custom providers only from + /// `~/.commandcode/providers.json`, and no variable moves that file; see + /// `command_code`. + ProvidersFile, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -72,13 +76,22 @@ pub fn agents() -> &'static [Agent] { handoff: Handoff::Extension, default_args: "", }, + // `command-code` rather than `cmd`: on Windows `cmd` is cmd.exe, and + // the package ships the long name on every platform. + Agent { + id: "command-code", + command: "command-code", + summary: "Open Command Code with a model", + handoff: Handoff::ProvidersFile, + default_args: "", + }, ]; AGENTS } /// An OpenAI client sends an Authorization header whatever is in it, and the /// local server ignores the value. A placeholder keeps both ends happy. -fn key_or_placeholder(api_key: &str) -> &str { +pub(super) fn key_or_placeholder(api_key: &str) -> &str { if api_key.is_empty() { "local" } else { @@ -119,7 +132,7 @@ fn unset_environment(name: &str) { /// The process outlives one launch — tests run several, and the REPL can too /// — so a value left behind would silently configure the next tool that /// named no model. -struct ScopedEnv { +pub(super) struct ScopedEnv { name: String, previous: Option, had_previous: bool, @@ -127,7 +140,7 @@ struct ScopedEnv { } impl ScopedEnv { - fn new(name: &str, value: &str) -> Self { + pub(super) fn new(name: &str, value: &str) -> Self { let previous = std::env::var_os(name); let had_previous = previous.is_some(); let applied = set_environment(name, value); @@ -139,7 +152,7 @@ impl ScopedEnv { } } - fn applied(&self) -> bool { + pub(super) fn applied(&self) -> bool { self.applied } } @@ -485,7 +498,7 @@ fn drop_stale_open_claw_provider(state: &Path, config: &str) { /// The OS home: HOME, falling back to USERPROFILE on Windows the way /// `open_claw_state_directory` does (PowerShell and cmd.exe leave HOME /// unset). -fn os_home() -> Option { +pub(super) fn os_home() -> Option { let home = std::env::var_os("HOME").filter(|home| !home.is_empty()); #[cfg(windows)] let home = home.or_else(|| std::env::var_os("USERPROFILE").filter(|home| !home.is_empty())); @@ -1416,6 +1429,11 @@ pub fn build_deep_seek_patch(llm_config: &str, model: &str) -> String { } pub fn launch_agent(agent: &Agent, model: &str, args: &[String], options: &GlobalOptions) -> i32 { + // Command Code takes its lock and checks for its own subcommands before + // an endpoint is resolved, so it runs its own launch from the start. + if agent.handoff == Handoff::ProvidersFile { + return super::command_code::launch(agent, model, args, options); + } // A launch killed before its own Drop runs (crash, kill -9, a closed // terminal) leaves its config behind; sweep those before this one // possibly writes another, rather than only ever growing the pile. @@ -1656,6 +1674,7 @@ pub fn launch_agent(agent: &Agent, model: &str, args: &[String], options: &Globa options, ) } + Handoff::ProvidersFile => unreachable!("command-code is dispatched before resolve"), }; release(&endpoint); diff --git a/src/harness/command_code.rs b/src/harness/command_code.rs new file mode 100644 index 00000000..eb8ff44d --- /dev/null +++ b/src/harness/command_code.rs @@ -0,0 +1,677 @@ +//! `wally command-code`: Command Code (npm `command-code`) against a local or +//! hosted model. +//! +//! Command Code reads custom providers only from +//! `~/.commandcode/providers.json`. No variable or flag moves that file, and +//! Command Code reloads it live while it runs. So a launch merges a +//! `runanywhere-wally` provider into the person's own file and takes it out +//! again when the tool exits, holding a lock for the whole run so two +//! launches cannot swap providers under each other. + +use std::fs::{File, TryLockError}; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +use serde_json::{json, Value}; + +use crate::bootstrap::GlobalOptions; +use crate::io::json::dump_pretty; +use crate::io::output as out; + +use super::agents::{key_or_placeholder, os_home, Agent, ScopedEnv}; +use super::catalog_models::{catalog_models_for, CatalogModel}; +use super::harness::{launch as launch_tool, release, resolve}; + +/// Our own provider id. Not `runanywhere`: a provider of that name may be the +/// person's own, added through `/connect`, and is never touched. +pub const PROVIDER_ID: &str = "runanywhere-wally"; + +/// The variable `providers.json` names for the key. Command Code refuses a +/// raw secret in that file and resolves `$VAR` from its own environment. +/// Private to wally, so a person's own provider that reads +/// `RUNANYWHERE_API_KEY` keeps its key during the run. +pub const KEY_VARIABLE: &str = "WALLY_COMMAND_CODE_API_KEY"; + +/// What Command Code reads as its own sign-in (`getCommandAuthKey`). +const AUTH_VARIABLE: &str = "COMMAND_CODE_API_KEY"; + +/// Stands in for a Command Code sign-in the person does not have. It only +/// gets past the startup check: `CMD_LOCAL_ONLY` goes with it, so it is never +/// sent anywhere. +const AUTH_PLACEHOLDER: &str = "wally-local"; + +const LOCK_FILE: &str = "wally-command-code.lock"; + +/// The reasoning levels Command Code takes in `reasoningEfforts`; it drops +/// any other name. +const LEVELS: &[&str] = &["low", "medium", "high", "xhigh", "max"]; + +/// Command Code's own subcommands (`cmd --help`, 1.87.0). Run as given, with +/// no provider and no injected flags, so `-- status` reports the person's real +/// sign-in. `acp` is not here: it is an agent session and gets the provider. +const OWN_SUBCOMMANDS: &[&str] = &[ + "info", + "status", + "help", + "whoami", + "update", + "feedback", + "issue", + "taste", + "learn-taste", + "mcp", + "cloud", + "skills", + "mods", + "login", + "logout", +]; + +/// Whether `args` asks for one of Command Code's own subcommands. Only the +/// first token counts: anything else would mean guessing which flags take a +/// value. +pub fn is_own_subcommand(args: &[String]) -> bool { + args.first() + .is_some_and(|first| OWN_SUBCOMMANDS.contains(&first.as_str())) +} + +/// Command Code's home, `/.commandcode`. It reads `HOME` (then +/// `USERPROFILE`) and takes no override. +pub fn command_code_dir() -> Option { + os_home().map(|home| PathBuf::from(home).join(".commandcode")) +} + +/// Whether Command Code would count the person as signed in, decided the +/// way its `getCommandAuthKey` does: a non-empty `COMMAND_CODE_API_KEY`, or +/// else a non-empty top-level `apiKey` in `auth.json`. Only presence is +/// looked at; the value is never kept. +pub fn signed_in(environment_key: Option<&str>, auth_json: Option<&str>) -> bool { + if environment_key.is_some_and(|key| !key.trim().is_empty()) { + return true; + } + auth_json + .and_then(|text| serde_json::from_str::(text).ok()) + .and_then(|auth| { + auth.get("apiKey") + .and_then(Value::as_str) + .map(|key| !key.is_empty()) + }) + .unwrap_or(false) +} + +/// The person's `providers.json`, ready to merge into: the parsed document +/// with any `runanywhere-wally` left by a run that never cleaned up dropped, +/// and whether a `provider` object was there to begin with. +#[derive(Debug, PartialEq)] +pub struct ExistingProviders { + pub document: Value, + pub had_provider: bool, + pub dropped_leftover: bool, +} + +pub fn parse_existing(existing: Option<&str>) -> Result { + let mut document: Value = match existing { + Some(text) if !text.trim().is_empty() => serde_json::from_str(text) + .map_err(|err| format!("command-code providers.json: {err}"))?, + _ => json!({}), + }; + if !document.is_object() { + return Err("command-code providers.json is not an object".to_string()); + } + let had_provider = match document.get("provider") { + None => false, + Some(provider) if provider.is_object() => true, + Some(_) => { + return Err("command-code providers.json: \"provider\" is not an object".to_string()) + } + }; + let dropped_leftover = document + .get_mut("provider") + .and_then(Value::as_object_mut) + .is_some_and(|providers| providers.remove(PROVIDER_ID).is_some()); + Ok(ExistingProviders { + document, + had_provider, + dropped_leftover, + }) +} + +/// One catalog model as a Command Code model entry. +fn model_entry(model: &CatalogModel) -> Value { + let mut entry = json!({ "name": model.id }); + if model.context_window > 0 { + entry["contextWindow"] = json!(model.context_window); + } + if model.max_output > 0 { + entry["maxOutput"] = json!(model.max_output); + } + // No levels Command Code can name means no reasoning field at all: + // `reasoning: true` would offer low / medium / high, which the endpoint + // refuses for a model that lacks them. + if let Some(reasoning) = &model.reasoning { + let levels: Vec<&str> = reasoning + .levels + .iter() + .copied() + .filter(|level| LEVELS.contains(level)) + .collect(); + if !levels.is_empty() { + entry["reasoningEfforts"] = json!(levels); + } + } + if model.input_per_mtok > 0 || model.output_per_mtok > 0 { + // Divided, not multiplied by 1e-6: 400000 * 1e-6 is 0.39999999999999997. + let per_million = |micro: i64| micro as f64 / 1_000_000.0; + entry["cost"] = json!({ + "input": per_million(model.input_per_mtok), + "output": per_million(model.output_per_mtok), + "cacheRead": per_million(model.cached_input_per_mtok), + }); + } + entry +} + +/// `document` with our provider added. +pub fn with_our_provider(mut document: Value, base_url: &str, models: &[CatalogModel]) -> Value { + let entries: serde_json::Map = models + .iter() + .map(|model| (model.id.clone(), model_entry(model))) + .collect(); + if document.get("provider").is_none() { + document["provider"] = json!({}); + } + document["provider"][PROVIDER_ID] = json!({ + "name": "RunAnywhere", + "api": "openai-completions", + "baseURL": base_url, + "apiKey": format!("${KEY_VARIABLE}"), + "models": entries, + }); + document +} + +/// What the file looked like before the run, and what the run wrote. +#[derive(Debug, Clone)] +pub struct Snapshot { + /// None when there was no file. + pub original: Option>, + pub had_provider: bool, + pub written: Vec, +} + +#[derive(Debug, PartialEq)] +pub enum Cleanup { + Write(Vec), + Remove, + Nothing, + /// The file no longer parses; it is the person's to fix. + Unparseable, +} + +/// How to put the file back, given what is in it now (None: gone). +/// +/// Untouched since the run wrote it: the original bytes go back verbatim, or +/// the file goes when there was none. Edited during the run (Command Code's +/// `/connect` writes it): only our provider comes out, and the `provider` +/// object too if we created it and it is now empty, so their edits survive. +pub fn plan_cleanup(snapshot: &Snapshot, current: Option<&[u8]>) -> Cleanup { + let Some(current) = current else { + return Cleanup::Nothing; + }; + if current == snapshot.written.as_slice() { + return match &snapshot.original { + Some(original) => Cleanup::Write(original.clone()), + None => Cleanup::Remove, + }; + } + let Ok(mut document) = serde_json::from_slice::(current) else { + return Cleanup::Unparseable; + }; + let Some(providers) = document.get_mut("provider").and_then(Value::as_object_mut) else { + return Cleanup::Nothing; + }; + if providers.remove(PROVIDER_ID).is_none() { + return Cleanup::Nothing; + } + let provider_now_empty = providers.is_empty(); + if provider_now_empty && !snapshot.had_provider { + if let Some(root) = document.as_object_mut() { + root.remove("provider"); + } + } + if snapshot.original.is_none() && document.as_object().is_some_and(|root| root.is_empty()) { + return Cleanup::Remove; + } + Cleanup::Write(dump_pretty(&document, 2).into_bytes()) +} + +/// Puts `providers.json` back when the launch ends, however it ends short of +/// the process being killed outright. +struct ProvidersGuard { + path: PathBuf, + snapshot: Snapshot, +} + +impl Drop for ProvidersGuard { + fn drop(&mut self) { + let current = match std::fs::read(&self.path) { + Ok(bytes) => Some(bytes), + Err(err) if err.kind() == ErrorKind::NotFound => None, + Err(_) => { + report_manual_cleanup(&self.path); + return; + } + }; + let done = match plan_cleanup(&self.snapshot, current.as_deref()) { + Cleanup::Write(bytes) => std::fs::write(&self.path, bytes).is_ok(), + Cleanup::Remove => std::fs::remove_file(&self.path).is_ok(), + Cleanup::Nothing => true, + Cleanup::Unparseable => false, + }; + if !done { + report_manual_cleanup(&self.path); + } + } +} + +fn report_manual_cleanup(path: &Path) { + out::error_line(&format!( + "could not take the \"{PROVIDER_ID}\" provider out of {}; remove it by hand", + path.display() + )); +} + +/// Holds `~/.commandcode/wally-command-code.lock` for the run. Command Code +/// never reads that file, so even Windows' mandatory lock on it blocks +/// nothing; the OS drops the lock when the process ends, however it ends. +fn take_run_lock(dir: &Path) -> Result { + let file = File::options() + .create(true) + .truncate(false) + .write(true) + .open(dir.join(LOCK_FILE)) + .map_err(|err| format!("could not open the command-code run lock: {err}"))?; + match file.try_lock() { + Ok(()) => Ok(file), + Err(TryLockError::WouldBlock) => { + Err("another `wally command-code` is running; one at a time".to_string()) + } + Err(TryLockError::Error(err)) => { + Err(format!("could not lock the command-code run lock: {err}")) + } + } +} + +/// `--skip-onboarding --no-auto-update --model runanywhere-wally/`, +/// then their own arguments. No auto-update, so the binary is not swapped +/// out from under a run. +pub fn command_code_argv(model: &str, child_args: &[String]) -> Vec { + let mut argv = vec![ + "--skip-onboarding".to_string(), + "--no-auto-update".to_string(), + "--model".to_string(), + format!("{PROVIDER_ID}/{model}"), + ]; + argv.extend(child_args.iter().cloned()); + argv +} + +pub fn launch(agent: &Agent, model: &str, args: &[String], options: &GlobalOptions) -> i32 { + if model.is_empty() || is_own_subcommand(args) { + return launch_tool(agent.command, "", args, options); + } + let Some(dir) = command_code_dir() else { + out::error_line("could not find a home directory for command-code"); + return 1; + }; + if let Err(err) = std::fs::create_dir_all(&dir) { + out::error_line(&format!("could not create {}: {err}", dir.display())); + return 1; + } + // Before resolving, so a refused run never loads a model. + let _lock = match take_run_lock(&dir) { + Ok(lock) => lock, + Err(failure) => { + out::error_line(&failure); + return 1; + } + }; + + let Some(endpoint) = resolve(model, options, agent.id) else { + return 1; + }; + let status = launch_with_endpoint(agent, model, args, options, &dir, &endpoint); + release(&endpoint); + status +} + +fn launch_with_endpoint( + agent: &Agent, + model: &str, + args: &[String], + options: &GlobalOptions, + dir: &Path, + endpoint: &super::harness::Endpoint, +) -> i32 { + // After resolve: a model pull there installs and then clears its own + // interrupt action, which would take an earlier one with it. The action + // only notes the interrupt. The child gets the same signal from the + // terminal and wally waits for it to exit, then cleans up. + let interrupted = Arc::new(AtomicBool::new(false)); + let noted = Arc::clone(&interrupted); + let _interrupt = crate::util::interrupt::on_interrupt(move || { + noted.store(true, Ordering::SeqCst); + }); + + let catalog = catalog_models_for(endpoint, model); + if catalog[0].context_window > 0 { + out::status_line(&format!( + "context window: {} tokens", + catalog[0].context_window + )); + } + + let path = dir.join("providers.json"); + let raw = match std::fs::read(&path) { + Ok(bytes) => Some(bytes), + Err(err) if err.kind() == ErrorKind::NotFound => None, + Err(err) => { + out::error_line(&format!("could not read {}: {err}", path.display())); + return 1; + } + }; + let text = match raw.as_deref().map(std::str::from_utf8).transpose() { + Ok(text) => text, + Err(_) => { + out::error_line(&format!("{} is not UTF-8", path.display())); + return 1; + } + }; + let existing = match parse_existing(text) { + Ok(existing) => existing, + Err(failure) => { + out::error_line(&failure); + return 1; + } + }; + // A leftover from a run that never cleaned up is not put back. + let original = match (&raw, existing.dropped_leftover) { + (Some(_), true) => Some(dump_pretty(&existing.document, 2).into_bytes()), + _ => raw.clone(), + }; + let written = dump_pretty( + &with_our_provider(existing.document, &endpoint.base_url, &catalog), + 2, + ) + .into_bytes(); + // Before the write, so a write that fails halfway is still undone. + let _restore = ProvidersGuard { + path: path.clone(), + snapshot: Snapshot { + original, + had_provider: existing.had_provider, + written: written.clone(), + }, + }; + if let Err(err) = std::fs::write(&path, &written) { + out::error_line(&format!("could not write {}: {err}", path.display())); + return 1; + } + + let key = ScopedEnv::new(KEY_VARIABLE, key_or_placeholder(&endpoint.api_key)); + let auth_json = std::fs::read_to_string(dir.join("auth.json")).ok(); + let environment_key = std::env::var(AUTH_VARIABLE).ok(); + let is_signed_in = signed_in(environment_key.as_deref(), auth_json.as_deref()); + // Local-only for a model on this machine, and whenever the placeholder + // sign-in is in play, so it never reaches Command Code's backend. + let placeholder = (!is_signed_in).then(|| ScopedEnv::new(AUTH_VARIABLE, AUTH_PLACEHOLDER)); + let local_only = + (endpoint.serving || !is_signed_in).then(|| ScopedEnv::new("CMD_LOCAL_ONLY", "1")); + let all_applied = key.applied() + && placeholder.as_ref().is_none_or(ScopedEnv::applied) + && local_only.as_ref().is_none_or(ScopedEnv::applied); + if !all_applied { + out::error_line(&format!("could not set the endpoint for {}", agent.id)); + return 1; + } + + if interrupted.load(Ordering::SeqCst) { + return 130; + } + out::status_line(&format!( + "{} will talk to {model} through {}", + agent.id, endpoint.base_url + )); + launch_tool(agent.command, "", &command_code_argv(model, args), options) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::account::ReasoningSupport; + + fn priced(id: &str) -> CatalogModel { + CatalogModel { + id: id.to_string(), + context_window: 200_000, + max_output: 32_000, + input_per_mtok: 400_000, + output_per_mtok: 1_600_000, + cached_input_per_mtok: 40_000, + ..Default::default() + } + } + + fn reasoning(id: &str, levels: &[&'static str]) -> CatalogModel { + CatalogModel { + id: id.to_string(), + reasoning: Some(ReasoningSupport { + levels: levels.to_vec(), + default_level: levels.first().copied(), + thinking_off: true, + }), + ..Default::default() + } + } + + fn snapshot(original: Option<&str>, had_provider: bool, written: &str) -> Snapshot { + Snapshot { + original: original.map(|text| text.as_bytes().to_vec()), + had_provider, + written: written.as_bytes().to_vec(), + } + } + + #[test] + fn our_provider_names_the_key_variable_and_keeps_theirs() { + let existing = parse_existing(Some( + r#"{"provider":{"runanywhere":{"baseURL":"http://mine/v1","apiKey":"$RUNANYWHERE_API_KEY","models":{}}}}"#, + )) + .unwrap(); + let built = with_our_provider( + existing.document, + "https://api.example/v1", + &[priced("glm-5.3-flash")], + ); + assert_eq!( + built["provider"]["runanywhere"]["baseURL"], + "http://mine/v1" + ); + let ours = &built["provider"][PROVIDER_ID]; + assert_eq!(ours["api"], "openai-completions"); + assert_eq!(ours["baseURL"], "https://api.example/v1"); + assert_eq!(ours["apiKey"], "$WALLY_COMMAND_CODE_API_KEY"); + } + + #[test] + fn model_entries_carry_exact_limits_and_prices() { + let built = with_our_provider(json!({}), "http://x/v1", &[priced("glm-5.3-flash")]); + let model = &built["provider"][PROVIDER_ID]["models"]["glm-5.3-flash"]; + assert_eq!(model["contextWindow"], 200_000); + assert_eq!(model["maxOutput"], 32_000); + assert_eq!(model["cost"]["input"], json!(0.4)); + assert_eq!(model["cost"]["output"], json!(1.6)); + assert_eq!(model["cost"]["cacheRead"], json!(0.04)); + } + + #[test] + fn only_levels_command_code_names_are_offered() { + let built = with_our_provider( + json!({}), + "http://x/v1", + &[ + reasoning("mimo", &["minimal", "low", "high", "max"]), + reasoning("minimal-only", &["minimal"]), + reasoning("switch", &[]), + ], + ); + let models = &built["provider"][PROVIDER_ID]["models"]; + assert_eq!( + models["mimo"]["reasoningEfforts"], + json!(["low", "high", "max"]) + ); + for id in ["minimal-only", "switch"] { + assert!(models[id].get("reasoningEfforts").is_none(), "{id}"); + assert!(models[id].get("reasoning").is_none(), "{id}"); + } + } + + #[test] + fn a_leftover_from_an_earlier_run_is_dropped() { + let existing = parse_existing(Some( + r#"{"provider":{"runanywhere-wally":{"baseURL":"http://old/v1","models":{}},"ollama":{"baseURL":"http://localhost:11434/v1","models":{}}}}"#, + )) + .unwrap(); + assert!(existing.dropped_leftover); + assert!(existing.document["provider"].get(PROVIDER_ID).is_none()); + assert!(existing.document["provider"].get("ollama").is_some()); + } + + #[test] + fn documents_that_cannot_be_merged_into_are_refused() { + for text in ["[]", r#"{"provider":[]}"#, "{not json"] { + assert!(parse_existing(Some(text)).is_err(), "{text}"); + } + } + + #[test] + fn a_missing_or_empty_file_merges_into_an_empty_document() { + for text in [None, Some(""), Some(" \n")] { + let existing = parse_existing(text).unwrap(); + assert_eq!(existing.document, json!({})); + assert!(!existing.had_provider); + } + } + + #[test] + fn an_untouched_file_gets_its_original_bytes_back() { + let snap = snapshot(Some("{ \"theirs\" : 1 }\n"), false, "WRITTEN"); + assert_eq!( + plan_cleanup(&snap, Some(b"WRITTEN")), + Cleanup::Write(b"{ \"theirs\" : 1 }\n".to_vec()) + ); + } + + #[test] + fn an_untouched_file_that_was_absent_is_removed() { + let snap = snapshot(None, false, "WRITTEN"); + assert_eq!(plan_cleanup(&snap, Some(b"WRITTEN")), Cleanup::Remove); + } + + #[test] + fn an_edited_file_loses_only_our_provider() { + let snap = snapshot(Some("{}"), false, "WRITTEN"); + let edited = br#"{"provider":{"runanywhere-wally":{},"groq":{"baseURL":"https://api.groq.com/openai/v1"}}}"#; + let Cleanup::Write(bytes) = plan_cleanup(&snap, Some(edited)) else { + panic!("expected a write"); + }; + let after: Value = serde_json::from_slice(&bytes).unwrap(); + assert!(after["provider"].get(PROVIDER_ID).is_none()); + assert!(after["provider"].get("groq").is_some()); + } + + #[test] + fn an_edited_file_drops_a_provider_object_we_created_once_empty() { + let snap = snapshot(Some(r#"{"theme":"dark"}"#), false, "WRITTEN"); + let edited = br#"{"theme":"light","provider":{"runanywhere-wally":{}}}"#; + let Cleanup::Write(bytes) = plan_cleanup(&snap, Some(edited)) else { + panic!("expected a write"); + }; + let after: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(after, json!({"theme": "light"})); + } + + #[test] + fn an_edited_file_that_was_absent_and_ends_empty_is_removed() { + let snap = snapshot(None, false, "WRITTEN"); + assert_eq!( + plan_cleanup(&snap, Some(br#"{"provider":{"runanywhere-wally":{}}}"#)), + Cleanup::Remove + ); + } + + #[test] + fn a_file_that_no_longer_parses_is_left_alone() { + let snap = snapshot(Some("{}"), false, "WRITTEN"); + assert_eq!(plan_cleanup(&snap, Some(b"{broken")), Cleanup::Unparseable); + } + + #[test] + fn a_file_deleted_during_the_run_stays_deleted() { + let snap = snapshot(Some("{}"), false, "WRITTEN"); + assert_eq!(plan_cleanup(&snap, None), Cleanup::Nothing); + } + + #[test] + fn a_second_run_lock_is_refused_while_the_first_is_held() { + let dir = std::env::temp_dir().join(format!("wally-cc-lock-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let first = take_run_lock(&dir).unwrap(); + let second = take_run_lock(&dir).unwrap_err(); + assert!(second.contains("one at a time"), "{second}"); + drop(first); + assert!(take_run_lock(&dir).is_ok()); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn signed_in_matches_command_codes_own_check() { + assert!(signed_in(Some("ck_live"), None)); + assert!(!signed_in(Some(" "), None)); + assert!(signed_in( + None, + Some(r#"{"apiKey":"ck_live","userName":"x"}"#) + )); + assert!(!signed_in(None, Some(r#"{"apiKey":""}"#))); + assert!(!signed_in(None, Some(r#"{"openrouter":{"apiKey":"sk"}}"#))); + assert!(!signed_in(None, Some("not json"))); + assert!(!signed_in(None, None)); + } + + #[test] + fn own_subcommands_run_untouched_and_acp_does_not() { + let args = |tokens: &[&str]| tokens.iter().map(|t| t.to_string()).collect::>(); + assert!(is_own_subcommand(&args(&["status"]))); + assert!(is_own_subcommand(&args(&["login", "openai"]))); + assert!(!is_own_subcommand(&args(&["acp"]))); + assert!(!is_own_subcommand(&args(&["-p", "status"]))); + assert!(!is_own_subcommand(&args(&[]))); + } + + #[test] + fn argv_skips_setup_and_names_the_model_before_theirs() { + assert_eq!( + command_code_argv("qwen3-4b", &["-p".to_string(), "hi".to_string()]), + [ + "--skip-onboarding", + "--no-auto-update", + "--model", + "runanywhere-wally/qwen3-4b", + "-p", + "hi" + ] + ); + } +} diff --git a/src/harness/harness.rs b/src/harness/harness.rs index fd6d57b2..1f410764 100644 --- a/src/harness/harness.rs +++ b/src/harness/harness.rs @@ -754,6 +754,7 @@ fn installer(tool: &str) -> Option { "openclaw" => ("npm i -g openclaw@latest", "npm.cmd i -g openclaw@latest", true), "dsh" => ("npm i -g @deepseek-ai/dsh", "npm.cmd i -g @deepseek-ai/dsh", true), "codex" => ("npm i -g @openai/codex", "npm.cmd i -g @openai/codex", true), + "command-code" => ("npm i -g command-code", "npm.cmd i -g command-code", true), "claude" => ( "curl -fsSL https://claude.ai/install.sh | bash", "irm https://claude.ai/install.ps1 | iex", @@ -1407,12 +1408,20 @@ mod tests { assert!(opencode.needs_npm); } + #[test] + #[cfg(not(windows))] + fn command_code_installs_with_npm() { + let command_code = installer("command-code").unwrap(); + assert_eq!(command_code.command, "npm i -g command-code"); + assert!(command_code.needs_npm); + } + // PowerShell's default execution policy blocks npm's own npm.ps1 shim, // so the offered command must be the npm.cmd shim instead. #[cfg(windows)] #[test] fn npm_tools_install_with_npm_cmd_on_windows() { - for tool in ["opencode", "openclaw", "dsh"] { + for tool in ["opencode", "openclaw", "dsh", "command-code"] { let installer = installer(tool).unwrap(); assert!( installer.command.starts_with("npm.cmd "), diff --git a/src/harness/mod.rs b/src/harness/mod.rs index 29540cee..2e5407cf 100644 --- a/src/harness/mod.rs +++ b/src/harness/mod.rs @@ -1,4 +1,5 @@ -//! Launching coding tools (Claude Code, Codex, opencode, Hermes, OpenClaw, DeepSeek) +//! Launching coding tools (Claude Code, Codex, opencode, Hermes, OpenClaw, DeepSeek, +//! Prime Agent, Command Code) //! against a hosted or local model. One namespace, as C++ `wally::harness` was. pub mod agents; @@ -6,6 +7,7 @@ pub mod catalog_models; pub mod chatgpt; pub mod claude_effort; pub mod codex; +pub mod command_code; pub mod declared_harness; #[allow(clippy::module_inception)] pub mod harness; diff --git a/tests/golden/README.md b/tests/golden/README.md index 65cfe315..8b311790 100644 --- a/tests/golden/README.md +++ b/tests/golden/README.md @@ -108,3 +108,7 @@ The kit pin moved to 0.20.41 (IDL 1.6.1), so the three `version__*` cases were updated; the version and schema lines are the only difference. The `pplx-decider-4b-gguf` catalog row added one row to `local__models__list__--all` and its `--json` twin. + +`wally command-code` added a line to every command list, so the same 31 cases +were re-captured from the Rust binary; that line is the only difference in +each. `help__command-code` and `help__help__command-code` are new cases. diff --git a/tests/golden/cases.json b/tests/golden/cases.json index abc245ab..d54035c4 100644 --- a/tests/golden/cases.json +++ b/tests/golden/cases.json @@ -292,6 +292,13 @@ "prime-agent" ] }, + { + "name": "help__command-code", + "args": [ + "help", + "command-code" + ] + }, { "name": "help__codex", "args": [ @@ -467,6 +474,14 @@ "--help" ] }, + { + "name": "help__help__command-code", + "args": [ + "help", + "command-code", + "--help" + ] + }, { "name": "help__help__codex", "args": [ @@ -3004,4 +3019,4 @@ "no-such-model" ] } -] +] \ No newline at end of file diff --git a/tests/golden/expected/alt_help__--no-color__--help.stdout b/tests/golden/expected/alt_help__--no-color__--help.stdout index cbc1b452..f08642f5 100644 --- a/tests/golden/expected/alt_help__--no-color__--help.stdout +++ b/tests/golden/expected/alt_help__--no-color__--help.stdout @@ -24,6 +24,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/alt_help__-h.stdout b/tests/golden/expected/alt_help__-h.stdout index cbc1b452..f08642f5 100644 --- a/tests/golden/expected/alt_help__-h.stdout +++ b/tests/golden/expected/alt_help__-h.stdout @@ -24,6 +24,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/alt_help__help.stdout b/tests/golden/expected/alt_help__help.stdout index cbc1b452..f08642f5 100644 --- a/tests/golden/expected/alt_help__help.stdout +++ b/tests/golden/expected/alt_help__help.stdout @@ -24,6 +24,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/alt_help__help__nope.stdout b/tests/golden/expected/alt_help__help__nope.stdout index cbc1b452..f08642f5 100644 --- a/tests/golden/expected/alt_help__help__nope.stdout +++ b/tests/golden/expected/alt_help__help__nope.stdout @@ -24,6 +24,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/help__ROOT.stdout b/tests/golden/expected/help__ROOT.stdout index cbc1b452..f08642f5 100644 --- a/tests/golden/expected/help__ROOT.stdout +++ b/tests/golden/expected/help__ROOT.stdout @@ -24,6 +24,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/help__command-code.code b/tests/golden/expected/help__command-code.code new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/tests/golden/expected/help__command-code.code @@ -0,0 +1 @@ +0 diff --git a/tests/golden/expected/help__command-code.stderr b/tests/golden/expected/help__command-code.stderr new file mode 100644 index 00000000..e69de29b diff --git a/tests/golden/expected/help__command-code.stdout b/tests/golden/expected/help__command-code.stdout new file mode 100644 index 00000000..5c862ad8 --- /dev/null +++ b/tests/golden/expected/help__command-code.stdout @@ -0,0 +1,21 @@ +Open Command Code with a model + +Usage: wally command-code [OPTIONS] [args...] + +Arguments: + args Passed through to the tool + +Options: + -h, --help Show help + -m, --model TEXT A model on this machine, or a hosted one from your + account + +Examples: + # Download the certified local model once + wally models pull qwen3-4b-instruct-2507 + + # Start the tool with the certified local model + wally command-code -m qwen3-4b-instruct-2507 + + # Use a cloud model after wally account login + wally command-code -m glm-5.3-flash diff --git a/tests/golden/expected/help__help.stdout b/tests/golden/expected/help__help.stdout index cbc1b452..f08642f5 100644 --- a/tests/golden/expected/help__help.stdout +++ b/tests/golden/expected/help__help.stdout @@ -24,6 +24,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/help__help__command-code.code b/tests/golden/expected/help__help__command-code.code new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/tests/golden/expected/help__help__command-code.code @@ -0,0 +1 @@ +0 diff --git a/tests/golden/expected/help__help__command-code.stderr b/tests/golden/expected/help__help__command-code.stderr new file mode 100644 index 00000000..e69de29b diff --git a/tests/golden/expected/help__help__command-code.stdout b/tests/golden/expected/help__help__command-code.stdout new file mode 100644 index 00000000..5c862ad8 --- /dev/null +++ b/tests/golden/expected/help__help__command-code.stdout @@ -0,0 +1,21 @@ +Open Command Code with a model + +Usage: wally command-code [OPTIONS] [args...] + +Arguments: + args Passed through to the tool + +Options: + -h, --help Show help + -m, --model TEXT A model on this machine, or a hosted one from your + account + +Examples: + # Download the certified local model once + wally models pull qwen3-4b-instruct-2507 + + # Start the tool with the certified local model + wally command-code -m qwen3-4b-instruct-2507 + + # Use a cloud model after wally account login + wally command-code -m glm-5.3-flash diff --git a/tests/golden/expected/parse__--bogus.stderr b/tests/golden/expected/parse__--bogus.stderr index 6c4decbb..bd36b3aa 100644 --- a/tests/golden/expected/parse__--bogus.stderr +++ b/tests/golden/expected/parse__--bogus.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/parse__-U__--json.stderr b/tests/golden/expected/parse__-U__--json.stderr index da99d047..380bcab7 100644 --- a/tests/golden/expected/parse__-U__--json.stderr +++ b/tests/golden/expected/parse__-U__--json.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/parse__-u__-v.stderr b/tests/golden/expected/parse__-u__-v.stderr index fae3370c..48c7db57 100644 --- a/tests/golden/expected/parse__-u__-v.stderr +++ b/tests/golden/expected/parse__-u__-v.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/parse__-z.stderr b/tests/golden/expected/parse__-z.stderr index 83e3fbcd..99255651 100644 --- a/tests/golden/expected/parse__-z.stderr +++ b/tests/golden/expected/parse__-z.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__auth.stderr b/tests/golden/expected/unknown__auth.stderr index b9f93bc9..18d23552 100644 --- a/tests/golden/expected/unknown__auth.stderr +++ b/tests/golden/expected/unknown__auth.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__chat.stderr b/tests/golden/expected/unknown__chat.stderr index c9be307c..f6abe390 100644 --- a/tests/golden/expected/unknown__chat.stderr +++ b/tests/golden/expected/unknown__chat.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__diarize.stderr b/tests/golden/expected/unknown__diarize.stderr index c3c4deac..748a2ba6 100644 --- a/tests/golden/expected/unknown__diarize.stderr +++ b/tests/golden/expected/unknown__diarize.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__embed.stderr b/tests/golden/expected/unknown__embed.stderr index 9c6c5216..ea90af69 100644 --- a/tests/golden/expected/unknown__embed.stderr +++ b/tests/golden/expected/unknown__embed.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__foo.stderr b/tests/golden/expected/unknown__foo.stderr index 60ddc9c2..24134d51 100644 --- a/tests/golden/expected/unknown__foo.stderr +++ b/tests/golden/expected/unknown__foo.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__image.stderr b/tests/golden/expected/unknown__image.stderr index 518a9582..704271ab 100644 --- a/tests/golden/expected/unknown__image.stderr +++ b/tests/golden/expected/unknown__image.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__list.stderr b/tests/golden/expected/unknown__list.stderr index f45a4887..ec3d005b 100644 --- a/tests/golden/expected/unknown__list.stderr +++ b/tests/golden/expected/unknown__list.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__lora.stderr b/tests/golden/expected/unknown__lora.stderr index 96f57ad8..4731b50a 100644 --- a/tests/golden/expected/unknown__lora.stderr +++ b/tests/golden/expected/unknown__lora.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__ls.stderr b/tests/golden/expected/unknown__ls.stderr index 869f4fad..28414645 100644 --- a/tests/golden/expected/unknown__ls.stderr +++ b/tests/golden/expected/unknown__ls.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__pull.stderr b/tests/golden/expected/unknown__pull.stderr index 270097ec..538436b1 100644 --- a/tests/golden/expected/unknown__pull.stderr +++ b/tests/golden/expected/unknown__pull.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__rag.stderr b/tests/golden/expected/unknown__rag.stderr index e9229638..08dcdd3c 100644 --- a/tests/golden/expected/unknown__rag.stderr +++ b/tests/golden/expected/unknown__rag.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__rerank.stderr b/tests/golden/expected/unknown__rerank.stderr index 3943b0c9..fe1ae10b 100644 --- a/tests/golden/expected/unknown__rerank.stderr +++ b/tests/golden/expected/unknown__rerank.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__rm.stderr b/tests/golden/expected/unknown__rm.stderr index d9c6996e..9d4dcf52 100644 --- a/tests/golden/expected/unknown__rm.stderr +++ b/tests/golden/expected/unknown__rm.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__segment.stderr b/tests/golden/expected/unknown__segment.stderr index dd69fe91..948ade8a 100644 --- a/tests/golden/expected/unknown__segment.stderr +++ b/tests/golden/expected/unknown__segment.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__show.stderr b/tests/golden/expected/unknown__show.stderr index 9af2a270..cdc4c2cf 100644 --- a/tests/golden/expected/unknown__show.stderr +++ b/tests/golden/expected/unknown__show.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__stt.stderr b/tests/golden/expected/unknown__stt.stderr index a1d52ec7..2dafe23c 100644 --- a/tests/golden/expected/unknown__stt.stderr +++ b/tests/golden/expected/unknown__stt.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__tts.stderr b/tests/golden/expected/unknown__tts.stderr index 83a48211..382785c4 100644 --- a/tests/golden/expected/unknown__tts.stderr +++ b/tests/golden/expected/unknown__tts.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__vad.stderr b/tests/golden/expected/unknown__vad.stderr index ac715da2..d4cc89da 100644 --- a/tests/golden/expected/unknown__vad.stderr +++ b/tests/golden/expected/unknown__vad.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__vlm.stderr b/tests/golden/expected/unknown__vlm.stderr index 0bc6e520..9770b825 100644 --- a/tests/golden/expected/unknown__vlm.stderr +++ b/tests/golden/expected/unknown__vlm.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/unknown__voice.stderr b/tests/golden/expected/unknown__voice.stderr index 38679b7a..4e4b7164 100644 --- a/tests/golden/expected/unknown__voice.stderr +++ b/tests/golden/expected/unknown__voice.stderr @@ -25,6 +25,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser diff --git a/tests/golden/expected/version__bare.stderr b/tests/golden/expected/version__bare.stderr index b1dcc40f..3f063721 100644 --- a/tests/golden/expected/version__bare.stderr +++ b/tests/golden/expected/version__bare.stderr @@ -24,6 +24,7 @@ Coding tools: openclaw Open OpenClaw with a model deepseek Open DeepSeek Harness with a model prime-agent Open Prime Agent with a model + command-code Open Command Code with a model Account: account login Sign in through the browser From 0c2999a4588af516ccee84b7f6339e59834e5e66 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar Date: Thu, 22 Oct 2026 03:30:20 +0530 Subject: [PATCH 2/2] harness: write command-code providers.json atomically, only over unchanged bytes --- .commandcode/taste/taste.md | 0 src/harness/command_code.rs | 318 ++++++++++++++++++++++++++++++------ 2 files changed, 264 insertions(+), 54 deletions(-) create mode 100644 .commandcode/taste/taste.md diff --git a/.commandcode/taste/taste.md b/.commandcode/taste/taste.md new file mode 100644 index 00000000..e69de29b diff --git a/src/harness/command_code.rs b/src/harness/command_code.rs index eb8ff44d..2fbafc04 100644 --- a/src/harness/command_code.rs +++ b/src/harness/command_code.rs @@ -256,24 +256,142 @@ struct ProvidersGuard { impl Drop for ProvidersGuard { fn drop(&mut self) { - let current = match std::fs::read(&self.path) { - Ok(bytes) => Some(bytes), - Err(err) if err.kind() == ErrorKind::NotFound => None, - Err(_) => { - report_manual_cleanup(&self.path); - return; + for _ in 0..REPLACE_ATTEMPTS { + let Ok(current) = read_optional(&self.path) else { + break; + }; + let replacement = match plan_cleanup(&self.snapshot, current.as_deref()) { + Cleanup::Write(bytes) => Some(bytes), + Cleanup::Remove => None, + Cleanup::Nothing => return, + Cleanup::Unparseable => break, + }; + match replace_if_unchanged(&self.path, current.as_deref(), replacement.as_deref()) { + Ok(Replaced::Done) => return, + // Changed between our read and our write: plan again from + // what is there now rather than overwrite it. + Ok(Replaced::Conflict) => continue, + Err(_) => break, } + } + report_manual_cleanup(&self.path); + } +} + +/// How many times a read-plan-replace of `providers.json` is retried when +/// someone else changes the file in between. +const REPLACE_ATTEMPTS: usize = 3; + +fn read_optional(path: &Path) -> std::io::Result>> { + match std::fs::read(path) { + Ok(bytes) => Ok(Some(bytes)), + Err(err) if err.kind() == ErrorKind::NotFound => Ok(None), + Err(err) => Err(err), + } +} + +#[derive(Debug, PartialEq)] +pub enum Replaced { + Done, + /// The file no longer held `expected`; nothing was changed. + Conflict, +} + +/// Replaces `path` with `replacement` (None: removes it), but only while it +/// still holds `expected` (None: absent). +/// +/// The new bytes go to a temporary file beside it and are renamed into place, +/// so Command Code, which reloads the file live, never reads half of one. +/// Command Code takes no lock of its own, so the compare and the rename are +/// two steps: a change landing between them is still lost, but the window is +/// the length of one rename rather than of a whole merge. +pub fn replace_if_unchanged( + path: &Path, + expected: Option<&[u8]>, + replacement: Option<&[u8]>, +) -> std::io::Result { + let staged = match replacement { + Some(bytes) => { + let mut name = path.file_name().unwrap_or_default().to_os_string(); + name.push(format!(".wally-{}.tmp", std::process::id())); + let staged = path.with_file_name(name); + if let Err(err) = std::fs::write(&staged, bytes) { + let _ = std::fs::remove_file(&staged); + return Err(err); + } + Some(staged) + } + None => None, + }; + let unchanged = match read_optional(path) { + Ok(current) => current.as_deref() == expected, + Err(err) => { + if let Some(staged) = &staged { + let _ = std::fs::remove_file(staged); + } + return Err(err); + } + }; + if !unchanged { + if let Some(staged) = &staged { + let _ = std::fs::remove_file(staged); + } + return Ok(Replaced::Conflict); + } + let committed = match &staged { + Some(staged) => std::fs::rename(staged, path), + None if expected.is_some() => std::fs::remove_file(path), + None => Ok(()), + }; + if let Err(err) = committed { + if let Some(staged) = &staged { + let _ = std::fs::remove_file(staged); + } + return Err(err); + } + Ok(Replaced::Done) +} + +/// Merges our provider into `path` and returns what the guard needs to take +/// it out again. A file that changes while it is being merged is read again +/// rather than overwritten with a stale merge. +fn install_provider( + path: &Path, + base_url: &str, + catalog: &[CatalogModel], +) -> Result { + for _ in 0..REPLACE_ATTEMPTS { + let raw = read_optional(path) + .map_err(|err| format!("could not read {}: {err}", path.display()))?; + let text = raw + .as_deref() + .map(std::str::from_utf8) + .transpose() + .map_err(|_| format!("{} is not UTF-8", path.display()))?; + let existing = parse_existing(text)?; + // A leftover from a run that never cleaned up is not put back. + let original = match (&raw, existing.dropped_leftover) { + (Some(_), true) => Some(dump_pretty(&existing.document, 2).into_bytes()), + _ => raw.clone(), }; - let done = match plan_cleanup(&self.snapshot, current.as_deref()) { - Cleanup::Write(bytes) => std::fs::write(&self.path, bytes).is_ok(), - Cleanup::Remove => std::fs::remove_file(&self.path).is_ok(), - Cleanup::Nothing => true, - Cleanup::Unparseable => false, - }; - if !done { - report_manual_cleanup(&self.path); + let written = + dump_pretty(&with_our_provider(existing.document, base_url, catalog), 2).into_bytes(); + match replace_if_unchanged(path, raw.as_deref(), Some(&written)) { + Ok(Replaced::Done) => { + return Ok(Snapshot { + original, + had_provider: existing.had_provider, + written, + }) + } + Ok(Replaced::Conflict) => continue, + Err(err) => return Err(format!("could not write {}: {err}", path.display())), } } + Err(format!( + "{} kept changing while wally was adding its provider; try again", + path.display() + )) } fn report_manual_cleanup(path: &Path) { @@ -374,51 +492,15 @@ fn launch_with_endpoint( } let path = dir.join("providers.json"); - let raw = match std::fs::read(&path) { - Ok(bytes) => Some(bytes), - Err(err) if err.kind() == ErrorKind::NotFound => None, - Err(err) => { - out::error_line(&format!("could not read {}: {err}", path.display())); - return 1; - } - }; - let text = match raw.as_deref().map(std::str::from_utf8).transpose() { - Ok(text) => text, - Err(_) => { - out::error_line(&format!("{} is not UTF-8", path.display())); - return 1; - } - }; - let existing = match parse_existing(text) { - Ok(existing) => existing, + // Only once our provider is in: a run that never wrote anything, or lost + // a conflict, has nothing to take back out. + let _restore = match install_provider(&path, &endpoint.base_url, &catalog) { + Ok(snapshot) => ProvidersGuard { path, snapshot }, Err(failure) => { out::error_line(&failure); return 1; } }; - // A leftover from a run that never cleaned up is not put back. - let original = match (&raw, existing.dropped_leftover) { - (Some(_), true) => Some(dump_pretty(&existing.document, 2).into_bytes()), - _ => raw.clone(), - }; - let written = dump_pretty( - &with_our_provider(existing.document, &endpoint.base_url, &catalog), - 2, - ) - .into_bytes(); - // Before the write, so a write that fails halfway is still undone. - let _restore = ProvidersGuard { - path: path.clone(), - snapshot: Snapshot { - original, - had_provider: existing.had_provider, - written: written.clone(), - }, - }; - if let Err(err) = std::fs::write(&path, &written) { - out::error_line(&format!("could not write {}: {err}", path.display())); - return 1; - } let key = ScopedEnv::new(KEY_VARIABLE, key_or_placeholder(&endpoint.api_key)); let auth_json = std::fs::read_to_string(dir.join("auth.json")).ok(); @@ -624,6 +706,134 @@ mod tests { assert_eq!(plan_cleanup(&snap, None), Cleanup::Nothing); } + fn scratch(name: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!("wally-cc-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + dir + } + + fn leftovers(dir: &Path) -> Vec { + std::fs::read_dir(dir) + .unwrap() + .map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned()) + .filter(|name| name.ends_with(".tmp")) + .collect() + } + + #[test] + fn replace_commits_when_the_file_is_as_expected() { + let dir = scratch("replace-ok"); + let path = dir.join("providers.json"); + std::fs::write(&path, b"old").unwrap(); + assert_eq!( + replace_if_unchanged(&path, Some(b"old"), Some(b"new")).unwrap(), + Replaced::Done + ); + assert_eq!(std::fs::read(&path).unwrap(), b"new"); + assert!(leftovers(&dir).is_empty()); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn replace_leaves_a_file_that_changed_since_it_was_read() { + let dir = scratch("replace-conflict"); + let path = dir.join("providers.json"); + std::fs::write(&path, b"theirs").unwrap(); + assert_eq!( + replace_if_unchanged(&path, Some(b"old"), Some(b"new")).unwrap(), + Replaced::Conflict + ); + assert_eq!(std::fs::read(&path).unwrap(), b"theirs"); + assert!(leftovers(&dir).is_empty()); + assert_eq!( + replace_if_unchanged(&path, Some(b"old"), None).unwrap(), + Replaced::Conflict + ); + assert!(path.exists()); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn replace_removes_and_creates_by_expectation() { + let dir = scratch("replace-absent"); + let path = dir.join("providers.json"); + assert_eq!( + replace_if_unchanged(&path, None, Some(b"new")).unwrap(), + Replaced::Done + ); + assert_eq!( + replace_if_unchanged(&path, None, Some(b"again")).unwrap(), + Replaced::Conflict + ); + assert_eq!( + replace_if_unchanged(&path, Some(b"new"), None).unwrap(), + Replaced::Done + ); + assert!(!path.exists()); + let _ = std::fs::remove_dir_all(&dir); + } + + /// Makes the staged write fail: a directory already sits where the + /// temporary file would go. + fn block_staging(path: &Path) { + let mut name = path.file_name().unwrap().to_os_string(); + name.push(format!(".wally-{}.tmp", std::process::id())); + std::fs::create_dir_all(path.with_file_name(name)).unwrap(); + } + + #[test] + fn a_failed_setup_write_keeps_the_original_intact() { + let dir = scratch("install-fails"); + let path = dir.join("providers.json"); + let theirs = br#"{"provider":{"mine":{"baseURL":"http://x/v1","models":{}}}}"#; + std::fs::write(&path, theirs).unwrap(); + block_staging(&path); + let failure = install_provider(&path, "http://ours/v1", &[priced("m")]).unwrap_err(); + assert!(failure.starts_with("could not write"), "{failure}"); + assert_eq!(std::fs::read(&path).unwrap(), theirs); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn a_failed_cleanup_write_leaves_a_whole_file_not_a_torn_one() { + let dir = scratch("cleanup-fails"); + let path = dir.join("providers.json"); + std::fs::write(&path, b"{}").unwrap(); + let snapshot = install_provider(&path, "http://ours/v1", &[priced("m")]).unwrap(); + let installed = std::fs::read(&path).unwrap(); + block_staging(&path); + drop(ProvidersGuard { + path: path.clone(), + snapshot, + }); + // Cleanup could not stage its write, so the file is exactly what + // setup wrote: still valid JSON, never half of one. + assert_eq!(std::fs::read(&path).unwrap(), installed); + assert!(serde_json::from_slice::(&installed).is_ok()); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn install_then_guard_puts_the_file_back_byte_for_byte() { + let dir = scratch("install-restore"); + let path = dir.join("providers.json"); + let theirs = + b"{ \"provider\" : { \"mine\": {\"baseURL\":\"http://x/v1\",\"models\":{}} } }\n"; + std::fs::write(&path, theirs).unwrap(); + let snapshot = + install_provider(&path, "http://ours/v1", &[priced("glm-5.3-flash")]).unwrap(); + let installed: Value = serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap(); + assert!(installed["provider"].get(PROVIDER_ID).is_some()); + drop(ProvidersGuard { + path: path.clone(), + snapshot, + }); + assert_eq!(std::fs::read(&path).unwrap(), theirs); + assert!(leftovers(&dir).is_empty()); + let _ = std::fs::remove_dir_all(&dir); + } + #[test] fn a_second_run_lock_is_refused_while_the_first_is_held() { let dir = std::env::temp_dir().join(format!("wally-cc-lock-{}", std::process::id()));