diff --git a/.agents/skills/rcli-architecture/SKILL.md b/.agents/skills/wally-architecture/SKILL.md similarity index 74% rename from .agents/skills/rcli-architecture/SKILL.md rename to .agents/skills/wally-architecture/SKILL.md index 66f9f64..ba95062 100644 --- a/.agents/skills/rcli-architecture/SKILL.md +++ b/.agents/skills/wally-architecture/SKILL.md @@ -1,16 +1,16 @@ --- -name: rcli-architecture -description: Where RCLI logic belongs — command layering, proto as SOT, kit vs CLI ownership, Apple MLX host vs rcli-cxx. Use when adding a command, moving inference logic, or deciding whether a bug is SDK or CLI. +name: wally-architecture +description: Where Wally logic belongs — command layering, proto as SOT, kit vs CLI ownership, Apple MLX host vs wally-cxx. Use when adding a command, moving inference logic, or deciding whether a bug is SDK or CLI. --- -# RCLI architecture +# Wally architecture -Repo: `RunanywhereAI/RCLI`. Product CLI named `rcli`. It consumes a **packaged +Repo: `RunanywhereAI/wally`. Product CLI named `wally`. It consumes a **packaged C++ desktop kit** via `find_package(RunAnywhere)`. It does not `add_subdirectory` or FetchContent the SDK, and it does not compile llama.cpp / Sherpa / ONNX / MLX from source. -Product version (`project(rcli VERSION …)` in `CMakeLists.txt`) is independent +Product version (`project(wally VERSION …)` in `CMakeLists.txt`) is independent of the SDK kit pin in `cmake/sdk-pin.cmake`. ## Ownership @@ -27,7 +27,7 @@ argv / flags / env The kit owns truth: models, backends, proto contracts, download, inference. The CLI renders and interacts. If a command is composing a multi-step bootstrap, hardcoding an engine name, or post-processing model output, that is a bug in the -SDK — fix it there, then consume a new kit (**rcli-kit-pin**). +SDK — fix it there, then consume a new kit (**wally-kit-pin**). ## Layering rules @@ -56,14 +56,14 @@ a retired MetalRT / hardcoded catalog. ## Apple MLX host -On Apple Silicon, `cmake --build` produces `build/rcli` (Swift host wrapping -`rcli_run_main`). Users never run `rcli-cxx`; that name exists only so CMake -cannot overwrite the product binary. Independent clones set `RCLI_SDK_SWIFT_PATH` -to a runanywhere-sdks checkout (CI does this). Nested `EXTERNAL/RCLI` finds -`../../Package.swift` automatically. Disable with `-DRCLI_APPLE_MLX_HOST=OFF` +On Apple Silicon, `cmake --build` produces `build/wally` (Swift host wrapping +`wally_run_main`). Users never run `wally-cxx`; that name exists only so CMake +cannot overwrite the product binary. Independent clones set `WALLY_SDK_SWIFT_PATH` +to a runanywhere-sdks checkout (CI does this). Nested `EXTERNAL/Wally` finds +`../../Package.swift` automatically. Disable with `-DWALLY_APPLE_MLX_HOST=OFF` only for a C++-only compile loop. -NeuRT image gen is `#if RCLI_HAS_NEURT` in `src/commands/cmd_image.cpp`, which +NeuRT image gen is `#if WALLY_HAS_NEURT` in `src/commands/cmd_image.cpp`, which is true only when the NeuRT overlay is applied. Public bottles stay OSS. `--engine qhexrt` / `qnn` / `npu` / `hexagon` map to `INFERENCE_FRAMEWORK_QHEXRT`. Local HNPU trees are inferred from `v75`/`v79`/ diff --git a/.claude/skills/rcli-device-e2e/SKILL.md b/.agents/skills/wally-device-e2e/SKILL.md similarity index 60% rename from .claude/skills/rcli-device-e2e/SKILL.md rename to .agents/skills/wally-device-e2e/SKILL.md index d969391..134b664 100644 --- a/.claude/skills/rcli-device-e2e/SKILL.md +++ b/.agents/skills/wally-device-e2e/SKILL.md @@ -1,36 +1,36 @@ --- -name: rcli-device-e2e -description: Run engine-agnostic rcli modality e2e on Apple Neural Engine (NeuRT) and Snapdragon Hexagon NPU (QHexRT) devices. Use when adding overlay backends, proving LLM/STT/TTS/VLM/embed/diffusion on device, or when a PC only has one modality's bundles on disk. +name: wally-device-e2e +description: Run engine-agnostic wally modality e2e on Apple Neural Engine (NeuRT) and Snapdragon Hexagon NPU (QHexRT) devices. Use when adding overlay backends, proving LLM/STT/TTS/VLM/embed/diffusion on device, or when a PC only has one modality's bundles on disk. --- -# RCLI device modality e2e +# Wally device modality e2e -Do not write per-engine tests. The harness is `scripts/e2e-modalities.sh`, -called from `scripts/e2e.sh`. Keys are **primitives** (`llm`, `stt`, `tts`, -`vlm`, `embed`, `image`, `vad`, `rerank`, `segment`). rcli picks the engine +Do not write per-engine tests. The harness is `scripts/test/e2e-modalities.sh`, +called from `scripts/test/e2e.sh`. Keys are **primitives** (`llm`, `stt`, `tts`, +`vlm`, `embed`, `image`, `vad`, `rerank`, `segment`). wally picks the engine from catalog framework, local path, or plugin priority. `--engine` is an -override (`RCLI_E2E_ENGINE`), never a required test input. +override (`WALLY_E2E_ENGINE`), never a required test input. ## Run ```bash # Public CI (modelless): skip every modality -bash scripts/e2e.sh /path/to/rcli +bash scripts/test/e2e.sh /path/to/wally # Device: discover whatever is already on disk, then run each primitive export RUNANYWHERE_HOME=/path/to/home # already-pulled OSS models -export RCLI_E2E_MODEL_ROOTS=/path/to/hnpu:/path/to/coreml -bash scripts/e2e-modalities.sh /path/to/rcli +export WALLY_E2E_MODEL_ROOTS=/path/to/hnpu:/path/to/coreml +bash scripts/test/e2e-modalities.sh /path/to/wally # Or pin one primitive (path or catalog id) -RCLI_E2E_LLM=/path/to/lfm2_5_230m_HNPU \ -RCLI_E2E_STT=/path/to/whisper_base_HNPU \ -RCLI_E2E_TTS=/path/to/kitten_micro_0_8_HNPU \ -RCLI_E2E_EMBED=/path/to/embeddinggemma_300m_HNPU \ - bash scripts/e2e-modalities.sh /path/to/rcli +WALLY_E2E_LLM=/path/to/lfm2_5_230m_HNPU \ +WALLY_E2E_STT=/path/to/whisper_base_HNPU \ +WALLY_E2E_TTS=/path/to/kitten_micro_0_8_HNPU \ +WALLY_E2E_EMBED=/path/to/embeddinggemma_300m_HNPU \ + bash scripts/test/e2e-modalities.sh /path/to/wally ``` -`RCLI_E2E_AUTO=1` pulls small OSS catalog defaults the **registered** backends +`WALLY_E2E_AUTO=1` pulls small OSS catalog defaults the **registered** backends can run (`smollm2`, `whisper-tiny`, `piper`, `minilm`, `silero`, `mlx-qwen3`, …). Never enable AUTO in public CI. @@ -49,7 +49,7 @@ copied for LLM smoke — not because the engine is LLM-only. Catalog ids: | VLM | `internvl3_5_1b` (~10 GB) | — | | Image | `cosmos3_edge_diffusion` | `sd15` | -`rcli pull` of a Hugging Face **repo page** is HTML. Pass the expanded +`wally pull` of a Hugging Face **repo page** is HTML. Pass the expanded directory to `-m`. Download `v81/*` only on Hexagon v81. Skip with a clear "no bundle" when the tree is missing. Fail only when a @@ -57,12 +57,12 @@ model was selected and the command failed. ## Overlay gotchas -- Public bottles never list `neurt` / `qhexrt`. Rebuild product `rcli` against - an overlay kit (`RCLI_SDK_KIT` pointing at that prefix). +- Public bottles never list `neurt` / `qhexrt`. Rebuild product `wally` against + an overlay kit (`WALLY_SDK_KIT` pointing at that prefix). - **QHexRT:** QAIRT **2.48** on Snapdragon X2 Elite / Hexagon v81. `ADSP_LIBRARY_PATH` must be the fully expanded `...\lib\hexagon-v81\unsigned` path. Nested `%QNN_SDK_ROOT%` in `cmd /c set` - does not expand. Copy `QnnHtp*.dll` next to `rcli.exe`. FastRPC ~90s then + does not expand. Copy `QnnHtp*.dll` next to `wally.exe`. FastRPC ~90s then user-driver fallback is normal. Use a `.bat`, not nested `cmd /c`. - **NeuRT image:** `--prompt` and `--out` required; `--steps 4` for smoke. Compiled zip, not the HF repo HTML. Tree needs `TextEncoder.mlmodelc` / @@ -71,8 +71,8 @@ model was selected and the command failed. SDK inserts `mtmd_default_marker()`. An extra `` makes `mtmd_tokenize` see 0 media markers. A tiny PNG can `bad_alloc` in SmolVLM2 after the 512×512 warmup; skip or pass a real photo via - `RCLI_E2E_VLM`. + `WALLY_E2E_VLM`. - **segment:** binary P6 PPM, not PNG. - STT has no `--engine` flag; put `-m` before the wav. -See `rcli-e2e` for bottle/backends assertions and Apple MLX host link flags. +See `wally-e2e` for bottle/backends assertions and Apple MLX host link flags. diff --git a/.claude/skills/rcli-e2e/SKILL.md b/.agents/skills/wally-e2e/SKILL.md similarity index 72% rename from .claude/skills/rcli-e2e/SKILL.md rename to .agents/skills/wally-e2e/SKILL.md index 099f7ac..7e9a819 100644 --- a/.claude/skills/rcli-e2e/SKILL.md +++ b/.agents/skills/wally-e2e/SKILL.md @@ -1,55 +1,55 @@ --- -name: rcli-e2e -description: Verify a built rcli binary against a pinned C++ desktop kit on macOS and Windows. Use when CI smoke/e2e is red, backends are missing, DLLs fail to load, or the Apple MLX host fails to link. +name: wally-e2e +description: Verify a built wally binary against a pinned C++ desktop kit on macOS and Windows. Use when CI smoke/e2e is red, backends are missing, DLLs fail to load, or the Apple MLX host fails to link. --- -# RCLI e2e +# Wally e2e -Entry: `scripts/e2e.sh `. Always runs `scripts/smoke.sh`, then -`scripts/e2e-modalities.sh` (engine-agnostic primitives). Public CI leaves +Entry: `scripts/test/e2e.sh `. Always runs `scripts/test/smoke.sh`, then +`scripts/test/e2e-modalities.sh` (engine-agnostic primitives). Public CI leaves modality knobs unset so every round-trip **skips**. Device runs set -`RCLI_E2E_` / `RCLI_E2E_MODEL_ROOTS` / `RCLI_E2E_AUTO=1`. See -`rcli-device-e2e` for ANE/NPU. +`WALLY_E2E_` / `WALLY_E2E_MODEL_ROOTS` / `WALLY_E2E_AUTO=1`. See +`wally-device-e2e` for ANE/NPU. | Env | Primitive | Example | |---|---|---| -| `RCLI_E2E_LLM` / `RCLI_E2E_MODEL` | llm | `mlx-qwen3` or a `*_HNPU` dir | -| `RCLI_E2E_STT` | stt | `whisper-tiny` or `whisper_base_HNPU` | -| `RCLI_E2E_TTS` | tts | `piper` or `kitten_micro_0_8_HNPU` | -| `RCLI_E2E_VLM` | vlm | `smolvlm2` (SDK inserts the media marker) | -| `RCLI_E2E_EMBED` | embed | `minilm` or `embeddinggemma_300m_HNPU` | -| `RCLI_E2E_IMAGE` | image | compiled SD1.5 tree / `sd15` | -| `RCLI_E2E_NEURT_MODEL` | classified by path | `sd15`, a Parakeet ANE tree, or `lfm2-230m-ane` | -| `RCLI_E2E_VAD` | vad | `silero` | -| `RCLI_E2E_RERANK` | rerank | `bge-reranker` | -| `RCLI_E2E_SEGMENT` | segment | `segformer` (P6 PPM) | -| `RCLI_E2E_ENGINE` | override only | `qhexrt` / `neurt` / `mlx` | - -Legacy `RCLI_E2E_MLX_MODEL` / `RCLI_E2E_NEURT_MODEL` / `RCLI_E2E_QHEXRT_MODEL` +| `WALLY_E2E_LLM` / `WALLY_E2E_MODEL` | llm | `mlx-qwen3` or a `*_HNPU` dir | +| `WALLY_E2E_STT` | stt | `whisper-tiny` or `whisper_base_HNPU` | +| `WALLY_E2E_TTS` | tts | `piper` or `kitten_micro_0_8_HNPU` | +| `WALLY_E2E_VLM` | vlm | `smolvlm2` (SDK inserts the media marker) | +| `WALLY_E2E_EMBED` | embed | `minilm` or `embeddinggemma_300m_HNPU` | +| `WALLY_E2E_IMAGE` | image | compiled SD1.5 tree / `sd15` | +| `WALLY_E2E_NEURT_MODEL` | classified by path | `sd15`, a Parakeet ANE tree, or `lfm2-230m-ane` | +| `WALLY_E2E_VAD` | vad | `silero` | +| `WALLY_E2E_RERANK` | rerank | `bge-reranker` | +| `WALLY_E2E_SEGMENT` | segment | `segformer` (P6 PPM) | +| `WALLY_E2E_ENGINE` | override only | `qhexrt` / `neurt` / `mlx` | + +Legacy `WALLY_E2E_MLX_MODEL` / `WALLY_E2E_NEURT_MODEL` / `WALLY_E2E_QHEXRT_MODEL` are classified by path/id into a primitive (not always image). Do not add new engine-named knobs. -`scripts/assert-binary-backends.sh` greps `nm`/`llvm-nm`/`dumpbin`/`strings` +`scripts/test/assert-binary-backends.sh` greps `nm`/`llvm-nm`/`dumpbin`/`strings` for registrar symbols (`raMLXRegisterRuntime`, `rac_plugin_entry_neurt`, `rac_plugin_entry_qhexrt`, …) so a backends() listing cannot pass without the engine actually being linked into the bottle. -Pass `RCLI_SDK_KIT` so overlay backends (`neurt` / `qhexrt`) are required +Pass `WALLY_SDK_KIT` so overlay backends (`neurt` / `qhexrt`) are required when those libs are in the kit. `CMAKE_PREFIX_PATH` is only used for `HAS_*` flags and Windows DLL staging — an ambient overlay prefix must not make a public OSS bottle fail for missing NeuRT. ## What "green" means -`scripts/assert-backends.sh` requires every engine the kit actually ships: +`scripts/test/assert-backends.sh` requires every engine the kit actually ships: -| Condition | Required `rcli --json backends` name | +| Condition | Required `wally --json backends` name | |---|---| | no kit Config (public OSS bottle) | `llamacpp` + `onnx` + `sherpa` | | kit `RunAnywhere_HAS_LLAMACPP` TRUE | `llamacpp` | | kit `RunAnywhere_HAS_ONNX` TRUE | `onnx` | | kit `RunAnywhere_HAS_SHERPA` TRUE | `sherpa` | -| Darwin arm64 product binary `rcli` (not `rcli-cxx`) | `mlx` | +| Darwin arm64 product binary `wally` (not `wally-cxx`) | `mlx` | | overlay `lib/librac_backend_neurt.a` / `rac_backend_neurt.lib` | `neurt` | | overlay `lib/librac_backend_qhexrt.a` / `rac_backend_qhexrt.lib` | `qhexrt` | @@ -69,7 +69,7 @@ the plugin was registered. Do not reintroduce a primitive allow-list. ## Windows DLLs Win32 `LoadLibrary` searches the exe directory, then PATH. `e2e.sh` copies -`third_party` / `bin` / `lib` `*.dll` next to `rcli.exe` and prepends those +`third_party` / `bin` / `lib` `*.dll` next to `wally.exe` and prepends those dirs to PATH **before** smoke. Skipping that produces "llamacpp only" even when the kit contains `rac_backend_onnx.lib` + `onnxruntime.dll`. @@ -81,19 +81,19 @@ GitHub Windows: `GITHUB_WORKSPACE` is `D:\a\...`; msys `tar -C` needs ## Apple MLX host link (Ninja) -`scripts/bundle-core.sh` merges everything `rcli` links into `librcli_bundle.a` +`scripts/build/bundle-core.sh` merges everything `wally` links into `libwally_bundle.a` for SwiftPM. -- Ninja **never** writes `CMakeFiles/rcli.dir/link.txt` (Makefiles only). -- Harvest with `ninja -t commands rcli | tail -1`. The **last** command is the - link. Grepping for `rcli` hits compile lines (`CMakeFiles/rcli.dir/…`). +- Ninja **never** writes `CMakeFiles/wally.dir/link.txt` (Makefiles only). +- Harvest with `ninja -t commands wally | tail -1`. The **last** command is the + link. Grepping for `wally` hits compile lines (`CMakeFiles/wally.dir/…`). - Apple ld emits one token `-Wl,-force_load,/abs/path/lib.a`. That ends in `.a` but is not a path. Prefixing `BUILD/` produces `build/-Wl,-force_load,…`. Strip `-Wl,-force_load,` first. - Ninja lists archives twice; `libtool -static` then fails on duplicate members unless you dedupe. -`scripts/build-mlx.sh` must dump the xcodebuild log on failure (`Undefined +`scripts/build/build-mlx.sh` must dump the xcodebuild log on failure (`Undefined symbols` does not contain `error:`). Do not grep bare `error:` — every CompileC line contains `-Werror=`. Observed CI `32786359915`: grep `error:|Metal|BUILD` left only `clang: error: linker command failed`. @@ -104,23 +104,23 @@ run (empty `xcodebuild-mlx.log`, status taken from a later assignment). Link flags that must survive the Swift host: -- `-Wl,-force_load,$BUILD/librcli_plugins.a` then `-L$BUILD -lrcli_bundle`. +- `-Wl,-force_load,$BUILD/libwally_plugins.a` then `-L$BUILD -lwally_bundle`. Force-load **only** the plugin backends (static registrars). Do **not** force-load llama-common: that pulls `download.cpp.o`, which references cpp-httplib `Client::Get` methods the kit never emitted as objects - (`rcli-cxx` never needed that TU). Observed locally after revealing the + (`wally-cxx` never needed that TU). Observed locally after revealing the real `Ld` log. - `-L$KIT/third_party -lonnxruntime` and `-Wl,-rpath,$KIT/third_party` — `bundle-core.sh` rewrites the kit dylib to `-l` and must keep `-L` plus the - rpath `rcli-cxx` already had, or the Swift host abort-traps at launch + rpath `wally-cxx` already had, or the Swift host abort-traps at launch (`Library not loaded: @rpath/libonnxruntime.dylib`). Harvest `-l*` as well as dylib conversions (`-ldl`, `-lbz2`). -- Canonicalize `RCLI_SDK_SWIFT_PATH` with `cd && pwd`. SwiftPM's local package - identity is the **directory name**, so `…/EXTERNAL/RCLI/../..` registers as +- Canonicalize `WALLY_SDK_SWIFT_PATH` with `cd && pwd`. SwiftPM's local package + identity is the **directory name**, so `…/EXTERNAL/Wally/../..` registers as `..`. Nested checkouts named `sdks1` must use that name in `.product(..., package:)`. -Do not point `RCLI_SDK_SWIFT_PATH` at an unreleased `Package.swift` whose +Do not point `WALLY_SDK_SWIFT_PATH` at an unreleased `Package.swift` whose `sdkVersion` zips 404 (`v0.20.28` before publish). CI checks out the **tagged** SDK tree (`ref: v$SDK`) whose binaryTargets already exist. @@ -134,12 +134,12 @@ tools version 6.2.0 but the installed version is 6.1.0`. macos-14 is Swift ## Linux Linux bottles are not a v1 merge blocker. Windows x64 and macOS arm64 are. -`scripts/e2e-linux.sh` exists for later. +`scripts/test/e2e-linux.sh` exists for later. ## Private engines NeuRT / QHexRT only appear in `backends` when the overlay was applied. -`scripts/e2e.sh` requires `neurt` / `qhexrt` when +`scripts/test/e2e.sh` requires `neurt` / `qhexrt` when `lib/librac_backend_neurt.a` or `lib/rac_backend_qhexrt.lib` exists — not by grepping packaged `HAS_NEURT FALSE` (that stays false; find_package flips it when the archive is present). Public CI must pass without overlays. Image gen @@ -148,30 +148,30 @@ when the archive is present). Public CI must pass without overlays. Image gen ## Device / overlay gotchas (0.5.1 + kit 0.20.28) Public bottles never list `neurt` or `qhexrt`. That is the product, not a -test gap. Overlay-rebuild the product binary (`RCLI_APPLE_MLX_HOST=ON` on +test gap. Overlay-rebuild the product binary (`WALLY_APPLE_MLX_HOST=ON` on Mac; ARM64 MSVC + QHexRT overlay on Snapdragon). -- **`CMAKE_PREFIX_PATH` is not an overlay opt-in.** Only `RCLI_SDK_KIT` +- **`CMAKE_PREFIX_PATH` is not an overlay opt-in.** Only `WALLY_SDK_KIT` makes e2e require `neurt`/`qhexrt`. An ambient overlay prefix from a previous rebuild will otherwise fail a public-bottle run. - **Binary assert:** never `nm | grep -q` under `pipefail` (SIGPIPE → false FAIL). Stream `strings -a` / `nm -a`. Darwin MLX proof is - `mlx-swift_Cmlx.bundle` next to product `rcli` (`nm -gU` misses Swift + `mlx-swift_Cmlx.bundle` next to product `wally` (`nm -gU` misses Swift host symbols). First C++ `rac_plugin_register(mlx)` logs `-811`; Swift callbacks then register MLX — noisy, not a miss. - **Windows ARM64 public/overlay kits have `HAS_LLAMACPP FALSE`.** Do not - require `llamacpp` in e2e. Overlay `rcli.exe` listing **only** `qhexrt` + require `llamacpp` in e2e. Overlay `wally.exe` listing **only** `qhexrt` (priority 150) is correct. On-disk GGUF (`qwen3.5-2b`) cannot run there. - **QHexRT generate needs QAIRT matching the device skel, not the overlay DLL set.** Snapdragon X2 Elite / Hexagon v81: `QNN_SDK_ROOT` + `ADSP_LIBRARY_PATH=%QNN_SDK_ROOT%\lib\hexagon-v81\unsigned`, copy `aarch64-windows-msvc` `QnnHtp.dll` / `QnnHtpPrepare.dll` / `QnnHtpV81Stub.dll` / `QnnHtpV81CalculatorStub.dll` / `QnnSystem.dll` - next to `rcli.exe`. Overlay 2.47 DLLs vs device 2.41 skels fail; QAIRT + next to `wally.exe`. Overlay 2.47 DLLs vs device 2.41 skels fail; QAIRT **2.48** worked. Pass the `*_HNPU` directory (`--engine qhexrt`), not a GGUF. FastRPC `openSession` timeouts (~90s) then user-driver fallback are normal; a second generate while DSP is wedged fails with - `Skel failed to process context binary` / `0x3ea` — `taskkill rcli.exe` + `Skel failed to process context binary` / `0x3ea` — `taskkill wally.exe` and use a `.bat` with **fully expanded** `ADSP_LIBRARY_PATH` (nested `%QNN_SDK_ROOT%` in `cmd /c "set A=…&& set B=%A%\…"` does not expand). - **VS on the ARM64 box may be 2026 / 18 Community**, not 2022: @@ -179,17 +179,17 @@ Mac; ARM64 MSVC + QHexRT overlay on Snapdragon). CMake/Ninja live under VS CMake extensions; they are not on default PATH. - **v0.20.28 Windows ARM64 public kit omits `libcurl.lib`.** Copy from `arm64-windows-static` into the kit `lib/` before linking (fixed in the - SDK packager for the *next* kit; do not retag 0.20.28). RCLI already + SDK packager for the *next* kit; do not retag 0.20.28). Wally already links kit `libcurl.lib` when present. -- **`rcli image generate` needs `--prompt` and `--out`**, not a positional +- **`wally image generate` needs `--prompt` and `--out`**, not a positional prompt. `--steps 4` is enough for a smoke PNG. Help exists on the public - bottle; real generate is compiled only with `RCLI_HAS_NEURT`. + bottle; real generate is compiled only with `WALLY_HAS_NEURT`. - **`sd15` catalog URL must be the compiled zip**, not the HF repo page (HTML ~160 KB). Unzip to a tree with `TextEncoder.mlmodelc` / `Unet.mlmodelc` / `VAEDecoder.mlmodelc` and pass that directory. COREML / QHEXRT catalog rows register `ModelInfo` (folder), not the single-file - download factory — `rcli pull sd15` is not a substitute for the zip. -- Published product bottles: macOS `rcli-$V-macos-arm64.tar.gz`, Windows + download factory — `wally pull sd15` is not a substitute for the zip. +- Published product bottles: macOS `wally-$V-macos-arm64.tar.gz`, Windows **x64** zip. There is no public Windows ARM64 bottle; NPU is overlay-only. - **The private QHexRT overlay tarball used to ship zero skel files** (only `.dll`/`.lib`, no `.so`/`.cat`) — `rac-cli`'s own overlay build could not @@ -197,12 +197,12 @@ Mac; ARM64 MSVC + QHexRT overlay on Snapdragon). box; validating it required hand-copying `librun_main_on_hexagon_skel.so` + `.cat` in from the `electron-qhexrt` npm package as a workaround. Fixed in `runanywhere-sdks`' `scripts/build/package-private-engine-overlay.sh` - (widened the copy filter and added a pass for `dsp/win-arm64/`). **RCLI + (widened the copy filter and added a pass for `dsp/win-arm64/`). **Wally itself never had the `ADSP_LIBRARY_PATH` bug the Electron binding had** — - `fastrpc_win.cpp`'s `exe_dir()` fallback naturally resolves for `rcli.exe` + `fastrpc_win.cpp`'s `exe_dir()` fallback naturally resolves for `wally.exe` because dependent DLLs/skels are staged flat beside the executable by this repo's own packaging convention — but that protection is a property of the - *packaging layout*, not of RCLI's code, so it is not something to assume + *packaging layout*, not of Wally's code, so it is not something to assume going forward. **Always build a fresh overlay from the actual release script and run the ternary model against it after any SDK kit-pin bump** that touches QHexRT — do not assume last time's manually-patched overlay @@ -211,19 +211,19 @@ Mac; ARM64 MSVC + QHexRT overlay on Snapdragon). into the same directory tree (`overlay/bin/*` → `kit/bin/`, `overlay/lib/*` → `kit/lib/`, `overlay/include/*` → `kit/include/`, `overlay/share/...` → `kit/share/...`) — never kept as a separate sibling `overlay/` directory - fed to CMake via a second `CMAKE_PREFIX_PATH` entry.** `rcli_stage_windows_runtime_dlls()` + fed to CMake via a second `CMAKE_PREFIX_PATH` entry.** `wally_stage_windows_runtime_dlls()` (`cmake/RunAnywhereSDK.cmake`) only ever copies from `${RunAnywhere_LIBRARY_DIR}/../bin` — i.e. `kit/bin` — so a same-named `overlay/bin` sitting next to `kit/` is silently never consulted. Worse, - this fails **completely silently**: the build succeeds, `rcli.exe` links, - and `rcli backends --json` returns `{"backends":[]}` with no error naming + this fails **completely silently**: the build succeeds, `wally.exe` links, + and `wally backends --json` returns `{"backends":[]}` with no error naming QHexRT at all (`find_library`-style detection in `RunAnywhereSDK.cmake` just doesn't find `kit/lib/rac_backend_qhexrt.lib` because it was never copied there). If a fresh overlay build reports zero backends, check this BEFORE suspecting the overlay tarball's contents. - **`qwen3.8-27b-1bit-npu`'s `HostOpFailed` had THREE compounding causes, found and fixed one at a time — a kit-pin bump to v0.20.31 alone was NOT - enough; RCLI needed its own additional fix (below) even with a perfectly + enough; Wally needed its own additional fix (below) even with a perfectly merged overlay.** 1. The overlay-skel-files-never-shipped bug (above), fixed upstream in `runanywhere-sdks`' overlay packaging script. @@ -236,10 +236,10 @@ Mac; ARM64 MSVC + QHexRT overlay on Snapdragon). in `neurun` v0.20.31 (`Backend::profile()` no longer shares `ensure_device()` with `device()`) — see that repo's `qhexrt-profile-must-not-create-live-device` KB finding. - 3. **RCLI-specific, and NOT fixed by the kit-pin bump alone**: + 3. **Wally-specific, and NOT fixed by the kit-pin bump alone**: `copy-overlay-dlls.cmake` globbed `*.dll` only, so even a correctly merged overlay (per the bullet above) left the Bonsai skel's `.so`/ - `.cat` sitting in `kit/bin/` and NEVER staged next to `rcli.exe` — the + `.cat` sitting in `kit/bin/` and NEVER staged next to `wally.exe` — the one place `fastrpc_win.cpp`'s `ADSP_LIBRARY_PATH ∪ exe_dir()` search actually looks. Fixed by widening the glob to `*.dll *.so *.cat`. **`fastrpc_win.cpp`'s `SET_PATH`/`GET_PATH` both returning a non-zero rc @@ -253,6 +253,6 @@ Mac; ARM64 MSVC + QHexRT overlay on Snapdragon). missing skel, missing/wrong/stale `.cat`, or — as this entry adds — the pair never being in the searched directory at all). Confirmed fixed end to end on a Snapdragon X2 Elite with all three fixes - in place: `rcli run --engine qhexrt` against `qwen3.8-27b-1bit-npu` opens + in place: `wally run --engine qhexrt` against `qwen3.8-27b-1bit-npu` opens the cDSP session and generates correctly ("The capital of France is **Paris**.", 0.105 tok/s, 12425 DSP linears). diff --git a/.claude/skills/rcli-kit-pin/SKILL.md b/.agents/skills/wally-kit-pin/SKILL.md similarity index 65% rename from .claude/skills/rcli-kit-pin/SKILL.md rename to .agents/skills/wally-kit-pin/SKILL.md index f85b77c..5d0dc51 100644 --- a/.claude/skills/rcli-kit-pin/SKILL.md +++ b/.agents/skills/wally-kit-pin/SKILL.md @@ -1,12 +1,12 @@ --- -name: rcli-kit-pin +name: wally-kit-pin description: Bump cmake/sdk-pin.cmake to a new published SDK C++ desktop kit (version + SHA-256 + IDL lock). Use after an SDK GitHub Release is published (not draft), when fetch-kit.sh 404s, or when SCHEMA_LOCK mismatches. --- -# RCLI kit pin +# Wally kit pin -Pin file: `cmake/sdk-pin.cmake`. Fetcher: `scripts/fetch-kit.sh`. Overlay: -`scripts/fetch-private-pack.sh`. CMake wrapper: `cmake/RunAnywhereSDK.cmake`. +Pin file: `cmake/sdk-pin.cmake`. Fetcher: `scripts/build/fetch-kit.sh`. Overlay: +`scripts/build/fetch-private-pack.sh`. CMake wrapper: `cmake/RunAnywhereSDK.cmake`. Companion: **cpp-desktop-kit** in runanywhere-sdks (what the tarball contains). @@ -17,8 +17,8 @@ Companion: **cpp-desktop-kit** in runanywhere-sdks (what the tarball contains). 2. Public assets exist: `RunAnywhere-cpp-desktop-{macos-arm64,windows-x64}-v$SDK.tar.gz` (+ windows-arm64 once that kit is on the release). -3. Do **not** point `RCLI_SDK_DIR` at SDK **source** — configure error. - Prefix is `-DCMAKE_PREFIX_PATH=` or `-DRCLI_SDK_KIT=`. +3. Do **not** point `WALLY_SDK_DIR` at SDK **source** — configure error. + Prefix is `-DCMAKE_PREFIX_PATH=` or `-DWALLY_SDK_KIT=`. ## Bump together (never version-only) @@ -31,26 +31,26 @@ shasum -a 256 /tmp/kits/RunAnywhere-cpp-desktop-*.tar.gz Update in `cmake/sdk-pin.cmake`: -- `RCLI_PINNED_SDK_VERSION` -- `RCLI_PINNED_KIT_SHA256_MACOS_ARM64` -- `RCLI_PINNED_KIT_SHA256_WINDOWS_X64` -- `RCLI_PINNED_KIT_SHA256_WINDOWS_ARM64` (uncomment once the asset exists) +- `WALLY_PINNED_SDK_VERSION` +- `WALLY_PINNED_KIT_SHA256_MACOS_ARM64` +- `WALLY_PINNED_KIT_SHA256_WINDOWS_X64` +- `WALLY_PINNED_KIT_SHA256_WINDOWS_ARM64` (uncomment once the asset exists) - IDL triple copied from the kit's `share/runanywhere/SCHEMA_LOCK`: - `RCLI_PINNED_IDL_VERSION`, `RCLI_PINNED_IDL_SCHEMA_SHA256`, - `RCLI_PINNED_IDL_PROTOC_VERSION` + `WALLY_PINNED_IDL_VERSION`, `WALLY_PINNED_IDL_SCHEMA_SHA256`, + `WALLY_PINNED_IDL_PROTOC_VERSION` -`fetch-kit.sh` refuses `SDK_VERSION != RCLI_PINNED_SDK_VERSION`. Do not override +`fetch-kit.sh` refuses `SDK_VERSION != WALLY_PINNED_SDK_VERSION`. Do not override only an env var to "try" a newer kit — checksums are keyed to the pin. Configure fails if the extracted kit's `SCHEMA_LOCK` does not match the IDL pin. When the schema changes, consume a new kit and bump the pin — never -regenerate headers locally. RCLI never runs `protoc`. +regenerate headers locally. Wally never runs `protoc`. ## CI Swift tree `.github/workflows/ci.yml` and `release.yml` check out `RunanywhereAI/runanywhere-sdks` at `ref: v$SDK` into `.deps/runanywhere-sdks` -for the Apple MLX host (`RCLI_SDK_SWIFT_PATH`). Bump that ref in the **same +for the Apple MLX host (`WALLY_SDK_SWIFT_PATH`). Bump that ref in the **same commit** as the pin. macos-26 (Xcode 26 / Swift 6.2 — matches SDK `swift-tools-version: 6.2`); macos-15 is Swift 6.1; macos-14 is Swift 5.10. @@ -58,29 +58,29 @@ commit** as the pin. macos-26 (Xcode 26 / Swift 6.2 — matches SDK Not part of the pin. Applied after extract: -1. `RCLI_PRIVATE_OVERLAY` (local tarball) +1. `WALLY_PRIVATE_OVERLAY` (local tarball) 2. tarball next to the kit prefix named `RunAnywhere-cpp-desktop-{macos-arm64-neurt,windows-arm64-qhexrt}-private-v*.tar.gz` -3. skip (OSS) unless `RCLI_REQUIRE_PRIVATE=1` +3. skip (OSS) unless `WALLY_REQUIRE_PRIVATE=1` QHexRT is Windows ARM64 only. Public bottles must stay OSS — never bake overlay bytes into a Homebrew bottle or a public GitHub Release asset. Windows ARM64 **public** kit is commons-only (no llama.cpp). Overlay apply -then rebuild product `rcli.exe`. `v0.20.28` ARM64 kits are missing +then rebuild product `wally.exe`. `v0.20.28` ARM64 kits are missing `lib/libcurl.lib` (packager only globbed x64-windows-static) — copy from -vcpkg `arm64-windows-static` until the next SDK kit train. RCLI +vcpkg `arm64-windows-static` until the next SDK kit train. Wally `cmake/RunAnywhereSDK.cmake` links that import lib when present. NeuRT: public mac bottle has MLX, not NeuRT. Overlay tarball + -`RCLI_APPLE_MLX_HOST=ON` rebuild. See **rcli-e2e** for QAIRT / SD15 / DSP +`WALLY_APPLE_MLX_HOST=ON` rebuild. See **wally-e2e** for QAIRT / SD15 / DSP gotchas; do not weaken public CI to require overlays. ## Verify ```bash -bash scripts/fetch-kit.sh macos-arm64 /tmp/kit +bash scripts/build/fetch-kit.sh macos-arm64 /tmp/kit cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=/tmp/kit cmake --build build -j "$(sysctl -n hw.logicalcpu)" -RCLI_SDK_KIT=/tmp/kit bash scripts/e2e.sh ./build/rcli +WALLY_SDK_KIT=/tmp/kit bash scripts/test/e2e.sh ./build/wally ``` diff --git a/.claude/skills/rcli-release/SKILL.md b/.agents/skills/wally-release/SKILL.md similarity index 64% rename from .claude/skills/rcli-release/SKILL.md rename to .agents/skills/wally-release/SKILL.md index e05d6b0..f5919db 100644 --- a/.claude/skills/rcli-release/SKILL.md +++ b/.agents/skills/wally-release/SKILL.md @@ -1,16 +1,16 @@ --- -name: rcli-release -description: Cut an RCLI product release (independent of SDK version) — version bump, release:patch label, merge, auto-tag, bottles. Use when shipping rcli after a published SDK kit, or when Homebrew / notarization / private overlays must not leak into public bottles. +name: wally-release +description: Cut an Wally product release (independent of SDK version) — version bump, release:patch label, merge, auto-tag, bottles. Use when shipping wally after a published SDK kit, or when Homebrew / notarization / private overlays must not leak into public bottles. --- -# RCLI release +# Wally release -Repo: `RunanywhereAI/RCLI`. Product version is `project(rcli VERSION x.y.z)` in -`CMakeLists.txt` (also stamped into `Formula/rcli.rb`). Independent of the SDK +Repo: `RunanywhereAI/wally`. Product version is `project(wally VERSION x.y.z)` in +`CMakeLists.txt` (also stamped into `Formula/wally.rb`). Independent of the SDK kit pin. Do this **after** the SDK GitHub Release this pin targets is **published** -(not draft). Companion: **rcli-kit-pin**, then this skill. +(not draft). Companion: **wally-kit-pin**, then this skill. ## 0. Facts @@ -24,7 +24,7 @@ Do this **after** the SDK GitHub Release this pin targets is **published** **macos runner must be macos-26** (Xcode 26 / Swift 6.2). macos-15 is Swift 6.1 and cannot resolve the SDK package; macos-14 is Swift 5.10. - Public bottle / zip is OSS only. NeuRT / QHexRT overlays are workflow - artifacts or `RCLI_PRIVATE_OVERLAY`, never public release assets, never + artifacts or `WALLY_PRIVATE_OVERLAY`, never public release assets, never Homebrew bottles. - Linux bottles are not a v1 merge blocker. @@ -32,12 +32,12 @@ Do this **after** the SDK GitHub Release this pin targets is **published** If this train needs new engines (sherpa routable, windows-arm64 kit, NeuRT image gen): bump `cmake/sdk-pin.cmake` + CI `ref: v$SDK` in the **same PR** -(**rcli-kit-pin**). CI must be green on that pin before you bump the product +(**wally-kit-pin**). CI must be green on that pin before you bump the product version. ## 2. Product version + label -Next patch after `0.5.0` is `0.5.1`. Bump `CMakeLists.txt` `project(rcli +Next patch after `0.5.0` is `0.5.1`. Bump `CMakeLists.txt` `project(wally VERSION …)` and any Formula version stamp in the same commit. Open/update the PR and apply **exactly one** `release:patch` (or minor/major). @@ -46,7 +46,7 @@ verify the label landed (`gh pr view --json labels`). ## 3. CI bar before merge -Required: macOS product e2e (`scripts/e2e.sh ./build/rcli`) and Windows e2e +Required: macOS product e2e (`scripts/test/e2e.sh ./build/wally`) and Windows e2e against the pinned kit. `agents-sync.yml` must pass (`scripts/ci/check-agents-sync.sh`). @@ -54,7 +54,7 @@ Re-check immediately before merge (a rollup of SUCCESS can hide checks that have not started): ```bash -gh pr checks --repo RunanywhereAI/RCLI --json name,state,bucket \ +gh pr checks --repo RunanywhereAI/wally --json name,state,bucket \ --jq '[.[] | select(.state == null or .state == "")] | length' # must be 0 ``` @@ -64,7 +64,7 @@ the SDK (`bypass_pull_request_allowances`) before `--admin`. ## 4. Merge triggers the release ```bash -gh pr merge --repo RunanywhereAI/RCLI --squash --admin # only if allow-listed +gh pr merge --repo RunanywhereAI/wally --squash --admin # only if allow-listed ``` `auto-tag.yml` tags `v$PRODUCT` and dispatches `release.yml`. Unlike the SDK @@ -75,17 +75,17 @@ link + e2e). ## 5. Verify the GitHub Release ```bash -gh release view v$PRODUCT --repo RunanywhereAI/RCLI --json isDraft,assets \ +gh release view v$PRODUCT --repo RunanywhereAI/wally --json isDraft,assets \ --jq '{isDraft, names: [.assets[].name]}' ``` -Expect `rcli-$PRODUCT-macos-arm64.tar.gz` (+ `.sha256`) and -`rcli-$PRODUCT-windows-x64.zip` (+ `.sha256`). **Must be empty** for +Expect `wally-$PRODUCT-macos-arm64.tar.gz` (+ `.sha256`) and +`wally-$PRODUCT-windows-x64.zip` (+ `.sha256`). **Must be empty** for `*neurt*`, `*qhexrt*`, `*private*`. If `release.yml` still creates a non-draft release, that is the live product cut — confirm asset names before anyone bottles from it. Stamp Formula from -the macOS sidecar (`scripts/stamp-formula.py`) when that path is wired. +the macOS sidecar (`scripts/release/stamp-formula.py`) when that path is wired. ## 6. Overlays after the public cut @@ -93,21 +93,21 @@ Private packs stay off the public release. For a machine that should load NeuRT / QHexRT: ```bash -export RCLI_PRIVATE_OVERLAY=/path/to/RunAnywhere-cpp-desktop-macos-arm64-neurt-private-v$SDK.tar.gz +export WALLY_PRIVATE_OVERLAY=/path/to/RunAnywhere-cpp-desktop-macos-arm64-neurt-private-v$SDK.tar.gz # or windows-arm64-qhexrt ``` -`RCLI_REQUIRE_PRIVATE=1` fails closed when the overlay is missing. Default CI +`WALLY_REQUIRE_PRIVATE=1` fails closed when the overlay is missing. Default CI must pass without it. -Device proof is not CI: rebuild overlay `rcli` on a Mac (NeuRT image gen) and +Device proof is not CI: rebuild overlay `wally` on a Mac (NeuRT image gen) and on Snapdragon ARM64 Windows (QHexRT). Match QAIRT to the device Hexagon skel -(`ADSP_LIBRARY_PATH`); see **rcli-e2e**. Public `v0.5.1` bottles are OSS — -NeuRT/QHexRT will never appear in `rcli backends` on those artifacts. +(`ADSP_LIBRARY_PATH`); see **wally-e2e**. Public `v0.5.1` bottles are OSS — +NeuRT/QHexRT will never appear in `wally backends` on those artifacts. ## Do not -- Attach `rcli-*` assets onto an SDK GitHub Release. +- Attach `wally-*` assets onto an SDK GitHub Release. - Pin `fetch-kit.sh` at a draft SDK tag. - Ship NeuRT / QHexRT inside the public bottle "just this once". - Use macos-14 or macos-15 for the Apple job (need macos-26 / Swift 6.2). diff --git a/.claude/skills/rcli-architecture/SKILL.md b/.claude/skills/wally-architecture/SKILL.md similarity index 74% rename from .claude/skills/rcli-architecture/SKILL.md rename to .claude/skills/wally-architecture/SKILL.md index 66f9f64..ba95062 100644 --- a/.claude/skills/rcli-architecture/SKILL.md +++ b/.claude/skills/wally-architecture/SKILL.md @@ -1,16 +1,16 @@ --- -name: rcli-architecture -description: Where RCLI logic belongs — command layering, proto as SOT, kit vs CLI ownership, Apple MLX host vs rcli-cxx. Use when adding a command, moving inference logic, or deciding whether a bug is SDK or CLI. +name: wally-architecture +description: Where Wally logic belongs — command layering, proto as SOT, kit vs CLI ownership, Apple MLX host vs wally-cxx. Use when adding a command, moving inference logic, or deciding whether a bug is SDK or CLI. --- -# RCLI architecture +# Wally architecture -Repo: `RunanywhereAI/RCLI`. Product CLI named `rcli`. It consumes a **packaged +Repo: `RunanywhereAI/wally`. Product CLI named `wally`. It consumes a **packaged C++ desktop kit** via `find_package(RunAnywhere)`. It does not `add_subdirectory` or FetchContent the SDK, and it does not compile llama.cpp / Sherpa / ONNX / MLX from source. -Product version (`project(rcli VERSION …)` in `CMakeLists.txt`) is independent +Product version (`project(wally VERSION …)` in `CMakeLists.txt`) is independent of the SDK kit pin in `cmake/sdk-pin.cmake`. ## Ownership @@ -27,7 +27,7 @@ argv / flags / env The kit owns truth: models, backends, proto contracts, download, inference. The CLI renders and interacts. If a command is composing a multi-step bootstrap, hardcoding an engine name, or post-processing model output, that is a bug in the -SDK — fix it there, then consume a new kit (**rcli-kit-pin**). +SDK — fix it there, then consume a new kit (**wally-kit-pin**). ## Layering rules @@ -56,14 +56,14 @@ a retired MetalRT / hardcoded catalog. ## Apple MLX host -On Apple Silicon, `cmake --build` produces `build/rcli` (Swift host wrapping -`rcli_run_main`). Users never run `rcli-cxx`; that name exists only so CMake -cannot overwrite the product binary. Independent clones set `RCLI_SDK_SWIFT_PATH` -to a runanywhere-sdks checkout (CI does this). Nested `EXTERNAL/RCLI` finds -`../../Package.swift` automatically. Disable with `-DRCLI_APPLE_MLX_HOST=OFF` +On Apple Silicon, `cmake --build` produces `build/wally` (Swift host wrapping +`wally_run_main`). Users never run `wally-cxx`; that name exists only so CMake +cannot overwrite the product binary. Independent clones set `WALLY_SDK_SWIFT_PATH` +to a runanywhere-sdks checkout (CI does this). Nested `EXTERNAL/Wally` finds +`../../Package.swift` automatically. Disable with `-DWALLY_APPLE_MLX_HOST=OFF` only for a C++-only compile loop. -NeuRT image gen is `#if RCLI_HAS_NEURT` in `src/commands/cmd_image.cpp`, which +NeuRT image gen is `#if WALLY_HAS_NEURT` in `src/commands/cmd_image.cpp`, which is true only when the NeuRT overlay is applied. Public bottles stay OSS. `--engine qhexrt` / `qnn` / `npu` / `hexagon` map to `INFERENCE_FRAMEWORK_QHEXRT`. Local HNPU trees are inferred from `v75`/`v79`/ diff --git a/.agents/skills/rcli-device-e2e/SKILL.md b/.claude/skills/wally-device-e2e/SKILL.md similarity index 60% rename from .agents/skills/rcli-device-e2e/SKILL.md rename to .claude/skills/wally-device-e2e/SKILL.md index d969391..134b664 100644 --- a/.agents/skills/rcli-device-e2e/SKILL.md +++ b/.claude/skills/wally-device-e2e/SKILL.md @@ -1,36 +1,36 @@ --- -name: rcli-device-e2e -description: Run engine-agnostic rcli modality e2e on Apple Neural Engine (NeuRT) and Snapdragon Hexagon NPU (QHexRT) devices. Use when adding overlay backends, proving LLM/STT/TTS/VLM/embed/diffusion on device, or when a PC only has one modality's bundles on disk. +name: wally-device-e2e +description: Run engine-agnostic wally modality e2e on Apple Neural Engine (NeuRT) and Snapdragon Hexagon NPU (QHexRT) devices. Use when adding overlay backends, proving LLM/STT/TTS/VLM/embed/diffusion on device, or when a PC only has one modality's bundles on disk. --- -# RCLI device modality e2e +# Wally device modality e2e -Do not write per-engine tests. The harness is `scripts/e2e-modalities.sh`, -called from `scripts/e2e.sh`. Keys are **primitives** (`llm`, `stt`, `tts`, -`vlm`, `embed`, `image`, `vad`, `rerank`, `segment`). rcli picks the engine +Do not write per-engine tests. The harness is `scripts/test/e2e-modalities.sh`, +called from `scripts/test/e2e.sh`. Keys are **primitives** (`llm`, `stt`, `tts`, +`vlm`, `embed`, `image`, `vad`, `rerank`, `segment`). wally picks the engine from catalog framework, local path, or plugin priority. `--engine` is an -override (`RCLI_E2E_ENGINE`), never a required test input. +override (`WALLY_E2E_ENGINE`), never a required test input. ## Run ```bash # Public CI (modelless): skip every modality -bash scripts/e2e.sh /path/to/rcli +bash scripts/test/e2e.sh /path/to/wally # Device: discover whatever is already on disk, then run each primitive export RUNANYWHERE_HOME=/path/to/home # already-pulled OSS models -export RCLI_E2E_MODEL_ROOTS=/path/to/hnpu:/path/to/coreml -bash scripts/e2e-modalities.sh /path/to/rcli +export WALLY_E2E_MODEL_ROOTS=/path/to/hnpu:/path/to/coreml +bash scripts/test/e2e-modalities.sh /path/to/wally # Or pin one primitive (path or catalog id) -RCLI_E2E_LLM=/path/to/lfm2_5_230m_HNPU \ -RCLI_E2E_STT=/path/to/whisper_base_HNPU \ -RCLI_E2E_TTS=/path/to/kitten_micro_0_8_HNPU \ -RCLI_E2E_EMBED=/path/to/embeddinggemma_300m_HNPU \ - bash scripts/e2e-modalities.sh /path/to/rcli +WALLY_E2E_LLM=/path/to/lfm2_5_230m_HNPU \ +WALLY_E2E_STT=/path/to/whisper_base_HNPU \ +WALLY_E2E_TTS=/path/to/kitten_micro_0_8_HNPU \ +WALLY_E2E_EMBED=/path/to/embeddinggemma_300m_HNPU \ + bash scripts/test/e2e-modalities.sh /path/to/wally ``` -`RCLI_E2E_AUTO=1` pulls small OSS catalog defaults the **registered** backends +`WALLY_E2E_AUTO=1` pulls small OSS catalog defaults the **registered** backends can run (`smollm2`, `whisper-tiny`, `piper`, `minilm`, `silero`, `mlx-qwen3`, …). Never enable AUTO in public CI. @@ -49,7 +49,7 @@ copied for LLM smoke — not because the engine is LLM-only. Catalog ids: | VLM | `internvl3_5_1b` (~10 GB) | — | | Image | `cosmos3_edge_diffusion` | `sd15` | -`rcli pull` of a Hugging Face **repo page** is HTML. Pass the expanded +`wally pull` of a Hugging Face **repo page** is HTML. Pass the expanded directory to `-m`. Download `v81/*` only on Hexagon v81. Skip with a clear "no bundle" when the tree is missing. Fail only when a @@ -57,12 +57,12 @@ model was selected and the command failed. ## Overlay gotchas -- Public bottles never list `neurt` / `qhexrt`. Rebuild product `rcli` against - an overlay kit (`RCLI_SDK_KIT` pointing at that prefix). +- Public bottles never list `neurt` / `qhexrt`. Rebuild product `wally` against + an overlay kit (`WALLY_SDK_KIT` pointing at that prefix). - **QHexRT:** QAIRT **2.48** on Snapdragon X2 Elite / Hexagon v81. `ADSP_LIBRARY_PATH` must be the fully expanded `...\lib\hexagon-v81\unsigned` path. Nested `%QNN_SDK_ROOT%` in `cmd /c set` - does not expand. Copy `QnnHtp*.dll` next to `rcli.exe`. FastRPC ~90s then + does not expand. Copy `QnnHtp*.dll` next to `wally.exe`. FastRPC ~90s then user-driver fallback is normal. Use a `.bat`, not nested `cmd /c`. - **NeuRT image:** `--prompt` and `--out` required; `--steps 4` for smoke. Compiled zip, not the HF repo HTML. Tree needs `TextEncoder.mlmodelc` / @@ -71,8 +71,8 @@ model was selected and the command failed. SDK inserts `mtmd_default_marker()`. An extra `` makes `mtmd_tokenize` see 0 media markers. A tiny PNG can `bad_alloc` in SmolVLM2 after the 512×512 warmup; skip or pass a real photo via - `RCLI_E2E_VLM`. + `WALLY_E2E_VLM`. - **segment:** binary P6 PPM, not PNG. - STT has no `--engine` flag; put `-m` before the wav. -See `rcli-e2e` for bottle/backends assertions and Apple MLX host link flags. +See `wally-e2e` for bottle/backends assertions and Apple MLX host link flags. diff --git a/.agents/skills/rcli-e2e/SKILL.md b/.claude/skills/wally-e2e/SKILL.md similarity index 72% rename from .agents/skills/rcli-e2e/SKILL.md rename to .claude/skills/wally-e2e/SKILL.md index 099f7ac..7e9a819 100644 --- a/.agents/skills/rcli-e2e/SKILL.md +++ b/.claude/skills/wally-e2e/SKILL.md @@ -1,55 +1,55 @@ --- -name: rcli-e2e -description: Verify a built rcli binary against a pinned C++ desktop kit on macOS and Windows. Use when CI smoke/e2e is red, backends are missing, DLLs fail to load, or the Apple MLX host fails to link. +name: wally-e2e +description: Verify a built wally binary against a pinned C++ desktop kit on macOS and Windows. Use when CI smoke/e2e is red, backends are missing, DLLs fail to load, or the Apple MLX host fails to link. --- -# RCLI e2e +# Wally e2e -Entry: `scripts/e2e.sh `. Always runs `scripts/smoke.sh`, then -`scripts/e2e-modalities.sh` (engine-agnostic primitives). Public CI leaves +Entry: `scripts/test/e2e.sh `. Always runs `scripts/test/smoke.sh`, then +`scripts/test/e2e-modalities.sh` (engine-agnostic primitives). Public CI leaves modality knobs unset so every round-trip **skips**. Device runs set -`RCLI_E2E_` / `RCLI_E2E_MODEL_ROOTS` / `RCLI_E2E_AUTO=1`. See -`rcli-device-e2e` for ANE/NPU. +`WALLY_E2E_` / `WALLY_E2E_MODEL_ROOTS` / `WALLY_E2E_AUTO=1`. See +`wally-device-e2e` for ANE/NPU. | Env | Primitive | Example | |---|---|---| -| `RCLI_E2E_LLM` / `RCLI_E2E_MODEL` | llm | `mlx-qwen3` or a `*_HNPU` dir | -| `RCLI_E2E_STT` | stt | `whisper-tiny` or `whisper_base_HNPU` | -| `RCLI_E2E_TTS` | tts | `piper` or `kitten_micro_0_8_HNPU` | -| `RCLI_E2E_VLM` | vlm | `smolvlm2` (SDK inserts the media marker) | -| `RCLI_E2E_EMBED` | embed | `minilm` or `embeddinggemma_300m_HNPU` | -| `RCLI_E2E_IMAGE` | image | compiled SD1.5 tree / `sd15` | -| `RCLI_E2E_NEURT_MODEL` | classified by path | `sd15`, a Parakeet ANE tree, or `lfm2-230m-ane` | -| `RCLI_E2E_VAD` | vad | `silero` | -| `RCLI_E2E_RERANK` | rerank | `bge-reranker` | -| `RCLI_E2E_SEGMENT` | segment | `segformer` (P6 PPM) | -| `RCLI_E2E_ENGINE` | override only | `qhexrt` / `neurt` / `mlx` | - -Legacy `RCLI_E2E_MLX_MODEL` / `RCLI_E2E_NEURT_MODEL` / `RCLI_E2E_QHEXRT_MODEL` +| `WALLY_E2E_LLM` / `WALLY_E2E_MODEL` | llm | `mlx-qwen3` or a `*_HNPU` dir | +| `WALLY_E2E_STT` | stt | `whisper-tiny` or `whisper_base_HNPU` | +| `WALLY_E2E_TTS` | tts | `piper` or `kitten_micro_0_8_HNPU` | +| `WALLY_E2E_VLM` | vlm | `smolvlm2` (SDK inserts the media marker) | +| `WALLY_E2E_EMBED` | embed | `minilm` or `embeddinggemma_300m_HNPU` | +| `WALLY_E2E_IMAGE` | image | compiled SD1.5 tree / `sd15` | +| `WALLY_E2E_NEURT_MODEL` | classified by path | `sd15`, a Parakeet ANE tree, or `lfm2-230m-ane` | +| `WALLY_E2E_VAD` | vad | `silero` | +| `WALLY_E2E_RERANK` | rerank | `bge-reranker` | +| `WALLY_E2E_SEGMENT` | segment | `segformer` (P6 PPM) | +| `WALLY_E2E_ENGINE` | override only | `qhexrt` / `neurt` / `mlx` | + +Legacy `WALLY_E2E_MLX_MODEL` / `WALLY_E2E_NEURT_MODEL` / `WALLY_E2E_QHEXRT_MODEL` are classified by path/id into a primitive (not always image). Do not add new engine-named knobs. -`scripts/assert-binary-backends.sh` greps `nm`/`llvm-nm`/`dumpbin`/`strings` +`scripts/test/assert-binary-backends.sh` greps `nm`/`llvm-nm`/`dumpbin`/`strings` for registrar symbols (`raMLXRegisterRuntime`, `rac_plugin_entry_neurt`, `rac_plugin_entry_qhexrt`, …) so a backends() listing cannot pass without the engine actually being linked into the bottle. -Pass `RCLI_SDK_KIT` so overlay backends (`neurt` / `qhexrt`) are required +Pass `WALLY_SDK_KIT` so overlay backends (`neurt` / `qhexrt`) are required when those libs are in the kit. `CMAKE_PREFIX_PATH` is only used for `HAS_*` flags and Windows DLL staging — an ambient overlay prefix must not make a public OSS bottle fail for missing NeuRT. ## What "green" means -`scripts/assert-backends.sh` requires every engine the kit actually ships: +`scripts/test/assert-backends.sh` requires every engine the kit actually ships: -| Condition | Required `rcli --json backends` name | +| Condition | Required `wally --json backends` name | |---|---| | no kit Config (public OSS bottle) | `llamacpp` + `onnx` + `sherpa` | | kit `RunAnywhere_HAS_LLAMACPP` TRUE | `llamacpp` | | kit `RunAnywhere_HAS_ONNX` TRUE | `onnx` | | kit `RunAnywhere_HAS_SHERPA` TRUE | `sherpa` | -| Darwin arm64 product binary `rcli` (not `rcli-cxx`) | `mlx` | +| Darwin arm64 product binary `wally` (not `wally-cxx`) | `mlx` | | overlay `lib/librac_backend_neurt.a` / `rac_backend_neurt.lib` | `neurt` | | overlay `lib/librac_backend_qhexrt.a` / `rac_backend_qhexrt.lib` | `qhexrt` | @@ -69,7 +69,7 @@ the plugin was registered. Do not reintroduce a primitive allow-list. ## Windows DLLs Win32 `LoadLibrary` searches the exe directory, then PATH. `e2e.sh` copies -`third_party` / `bin` / `lib` `*.dll` next to `rcli.exe` and prepends those +`third_party` / `bin` / `lib` `*.dll` next to `wally.exe` and prepends those dirs to PATH **before** smoke. Skipping that produces "llamacpp only" even when the kit contains `rac_backend_onnx.lib` + `onnxruntime.dll`. @@ -81,19 +81,19 @@ GitHub Windows: `GITHUB_WORKSPACE` is `D:\a\...`; msys `tar -C` needs ## Apple MLX host link (Ninja) -`scripts/bundle-core.sh` merges everything `rcli` links into `librcli_bundle.a` +`scripts/build/bundle-core.sh` merges everything `wally` links into `libwally_bundle.a` for SwiftPM. -- Ninja **never** writes `CMakeFiles/rcli.dir/link.txt` (Makefiles only). -- Harvest with `ninja -t commands rcli | tail -1`. The **last** command is the - link. Grepping for `rcli` hits compile lines (`CMakeFiles/rcli.dir/…`). +- Ninja **never** writes `CMakeFiles/wally.dir/link.txt` (Makefiles only). +- Harvest with `ninja -t commands wally | tail -1`. The **last** command is the + link. Grepping for `wally` hits compile lines (`CMakeFiles/wally.dir/…`). - Apple ld emits one token `-Wl,-force_load,/abs/path/lib.a`. That ends in `.a` but is not a path. Prefixing `BUILD/` produces `build/-Wl,-force_load,…`. Strip `-Wl,-force_load,` first. - Ninja lists archives twice; `libtool -static` then fails on duplicate members unless you dedupe. -`scripts/build-mlx.sh` must dump the xcodebuild log on failure (`Undefined +`scripts/build/build-mlx.sh` must dump the xcodebuild log on failure (`Undefined symbols` does not contain `error:`). Do not grep bare `error:` — every CompileC line contains `-Werror=`. Observed CI `32786359915`: grep `error:|Metal|BUILD` left only `clang: error: linker command failed`. @@ -104,23 +104,23 @@ run (empty `xcodebuild-mlx.log`, status taken from a later assignment). Link flags that must survive the Swift host: -- `-Wl,-force_load,$BUILD/librcli_plugins.a` then `-L$BUILD -lrcli_bundle`. +- `-Wl,-force_load,$BUILD/libwally_plugins.a` then `-L$BUILD -lwally_bundle`. Force-load **only** the plugin backends (static registrars). Do **not** force-load llama-common: that pulls `download.cpp.o`, which references cpp-httplib `Client::Get` methods the kit never emitted as objects - (`rcli-cxx` never needed that TU). Observed locally after revealing the + (`wally-cxx` never needed that TU). Observed locally after revealing the real `Ld` log. - `-L$KIT/third_party -lonnxruntime` and `-Wl,-rpath,$KIT/third_party` — `bundle-core.sh` rewrites the kit dylib to `-l` and must keep `-L` plus the - rpath `rcli-cxx` already had, or the Swift host abort-traps at launch + rpath `wally-cxx` already had, or the Swift host abort-traps at launch (`Library not loaded: @rpath/libonnxruntime.dylib`). Harvest `-l*` as well as dylib conversions (`-ldl`, `-lbz2`). -- Canonicalize `RCLI_SDK_SWIFT_PATH` with `cd && pwd`. SwiftPM's local package - identity is the **directory name**, so `…/EXTERNAL/RCLI/../..` registers as +- Canonicalize `WALLY_SDK_SWIFT_PATH` with `cd && pwd`. SwiftPM's local package + identity is the **directory name**, so `…/EXTERNAL/Wally/../..` registers as `..`. Nested checkouts named `sdks1` must use that name in `.product(..., package:)`. -Do not point `RCLI_SDK_SWIFT_PATH` at an unreleased `Package.swift` whose +Do not point `WALLY_SDK_SWIFT_PATH` at an unreleased `Package.swift` whose `sdkVersion` zips 404 (`v0.20.28` before publish). CI checks out the **tagged** SDK tree (`ref: v$SDK`) whose binaryTargets already exist. @@ -134,12 +134,12 @@ tools version 6.2.0 but the installed version is 6.1.0`. macos-14 is Swift ## Linux Linux bottles are not a v1 merge blocker. Windows x64 and macOS arm64 are. -`scripts/e2e-linux.sh` exists for later. +`scripts/test/e2e-linux.sh` exists for later. ## Private engines NeuRT / QHexRT only appear in `backends` when the overlay was applied. -`scripts/e2e.sh` requires `neurt` / `qhexrt` when +`scripts/test/e2e.sh` requires `neurt` / `qhexrt` when `lib/librac_backend_neurt.a` or `lib/rac_backend_qhexrt.lib` exists — not by grepping packaged `HAS_NEURT FALSE` (that stays false; find_package flips it when the archive is present). Public CI must pass without overlays. Image gen @@ -148,30 +148,30 @@ when the archive is present). Public CI must pass without overlays. Image gen ## Device / overlay gotchas (0.5.1 + kit 0.20.28) Public bottles never list `neurt` or `qhexrt`. That is the product, not a -test gap. Overlay-rebuild the product binary (`RCLI_APPLE_MLX_HOST=ON` on +test gap. Overlay-rebuild the product binary (`WALLY_APPLE_MLX_HOST=ON` on Mac; ARM64 MSVC + QHexRT overlay on Snapdragon). -- **`CMAKE_PREFIX_PATH` is not an overlay opt-in.** Only `RCLI_SDK_KIT` +- **`CMAKE_PREFIX_PATH` is not an overlay opt-in.** Only `WALLY_SDK_KIT` makes e2e require `neurt`/`qhexrt`. An ambient overlay prefix from a previous rebuild will otherwise fail a public-bottle run. - **Binary assert:** never `nm | grep -q` under `pipefail` (SIGPIPE → false FAIL). Stream `strings -a` / `nm -a`. Darwin MLX proof is - `mlx-swift_Cmlx.bundle` next to product `rcli` (`nm -gU` misses Swift + `mlx-swift_Cmlx.bundle` next to product `wally` (`nm -gU` misses Swift host symbols). First C++ `rac_plugin_register(mlx)` logs `-811`; Swift callbacks then register MLX — noisy, not a miss. - **Windows ARM64 public/overlay kits have `HAS_LLAMACPP FALSE`.** Do not - require `llamacpp` in e2e. Overlay `rcli.exe` listing **only** `qhexrt` + require `llamacpp` in e2e. Overlay `wally.exe` listing **only** `qhexrt` (priority 150) is correct. On-disk GGUF (`qwen3.5-2b`) cannot run there. - **QHexRT generate needs QAIRT matching the device skel, not the overlay DLL set.** Snapdragon X2 Elite / Hexagon v81: `QNN_SDK_ROOT` + `ADSP_LIBRARY_PATH=%QNN_SDK_ROOT%\lib\hexagon-v81\unsigned`, copy `aarch64-windows-msvc` `QnnHtp.dll` / `QnnHtpPrepare.dll` / `QnnHtpV81Stub.dll` / `QnnHtpV81CalculatorStub.dll` / `QnnSystem.dll` - next to `rcli.exe`. Overlay 2.47 DLLs vs device 2.41 skels fail; QAIRT + next to `wally.exe`. Overlay 2.47 DLLs vs device 2.41 skels fail; QAIRT **2.48** worked. Pass the `*_HNPU` directory (`--engine qhexrt`), not a GGUF. FastRPC `openSession` timeouts (~90s) then user-driver fallback are normal; a second generate while DSP is wedged fails with - `Skel failed to process context binary` / `0x3ea` — `taskkill rcli.exe` + `Skel failed to process context binary` / `0x3ea` — `taskkill wally.exe` and use a `.bat` with **fully expanded** `ADSP_LIBRARY_PATH` (nested `%QNN_SDK_ROOT%` in `cmd /c "set A=…&& set B=%A%\…"` does not expand). - **VS on the ARM64 box may be 2026 / 18 Community**, not 2022: @@ -179,17 +179,17 @@ Mac; ARM64 MSVC + QHexRT overlay on Snapdragon). CMake/Ninja live under VS CMake extensions; they are not on default PATH. - **v0.20.28 Windows ARM64 public kit omits `libcurl.lib`.** Copy from `arm64-windows-static` into the kit `lib/` before linking (fixed in the - SDK packager for the *next* kit; do not retag 0.20.28). RCLI already + SDK packager for the *next* kit; do not retag 0.20.28). Wally already links kit `libcurl.lib` when present. -- **`rcli image generate` needs `--prompt` and `--out`**, not a positional +- **`wally image generate` needs `--prompt` and `--out`**, not a positional prompt. `--steps 4` is enough for a smoke PNG. Help exists on the public - bottle; real generate is compiled only with `RCLI_HAS_NEURT`. + bottle; real generate is compiled only with `WALLY_HAS_NEURT`. - **`sd15` catalog URL must be the compiled zip**, not the HF repo page (HTML ~160 KB). Unzip to a tree with `TextEncoder.mlmodelc` / `Unet.mlmodelc` / `VAEDecoder.mlmodelc` and pass that directory. COREML / QHEXRT catalog rows register `ModelInfo` (folder), not the single-file - download factory — `rcli pull sd15` is not a substitute for the zip. -- Published product bottles: macOS `rcli-$V-macos-arm64.tar.gz`, Windows + download factory — `wally pull sd15` is not a substitute for the zip. +- Published product bottles: macOS `wally-$V-macos-arm64.tar.gz`, Windows **x64** zip. There is no public Windows ARM64 bottle; NPU is overlay-only. - **The private QHexRT overlay tarball used to ship zero skel files** (only `.dll`/`.lib`, no `.so`/`.cat`) — `rac-cli`'s own overlay build could not @@ -197,12 +197,12 @@ Mac; ARM64 MSVC + QHexRT overlay on Snapdragon). box; validating it required hand-copying `librun_main_on_hexagon_skel.so` + `.cat` in from the `electron-qhexrt` npm package as a workaround. Fixed in `runanywhere-sdks`' `scripts/build/package-private-engine-overlay.sh` - (widened the copy filter and added a pass for `dsp/win-arm64/`). **RCLI + (widened the copy filter and added a pass for `dsp/win-arm64/`). **Wally itself never had the `ADSP_LIBRARY_PATH` bug the Electron binding had** — - `fastrpc_win.cpp`'s `exe_dir()` fallback naturally resolves for `rcli.exe` + `fastrpc_win.cpp`'s `exe_dir()` fallback naturally resolves for `wally.exe` because dependent DLLs/skels are staged flat beside the executable by this repo's own packaging convention — but that protection is a property of the - *packaging layout*, not of RCLI's code, so it is not something to assume + *packaging layout*, not of Wally's code, so it is not something to assume going forward. **Always build a fresh overlay from the actual release script and run the ternary model against it after any SDK kit-pin bump** that touches QHexRT — do not assume last time's manually-patched overlay @@ -211,19 +211,19 @@ Mac; ARM64 MSVC + QHexRT overlay on Snapdragon). into the same directory tree (`overlay/bin/*` → `kit/bin/`, `overlay/lib/*` → `kit/lib/`, `overlay/include/*` → `kit/include/`, `overlay/share/...` → `kit/share/...`) — never kept as a separate sibling `overlay/` directory - fed to CMake via a second `CMAKE_PREFIX_PATH` entry.** `rcli_stage_windows_runtime_dlls()` + fed to CMake via a second `CMAKE_PREFIX_PATH` entry.** `wally_stage_windows_runtime_dlls()` (`cmake/RunAnywhereSDK.cmake`) only ever copies from `${RunAnywhere_LIBRARY_DIR}/../bin` — i.e. `kit/bin` — so a same-named `overlay/bin` sitting next to `kit/` is silently never consulted. Worse, - this fails **completely silently**: the build succeeds, `rcli.exe` links, - and `rcli backends --json` returns `{"backends":[]}` with no error naming + this fails **completely silently**: the build succeeds, `wally.exe` links, + and `wally backends --json` returns `{"backends":[]}` with no error naming QHexRT at all (`find_library`-style detection in `RunAnywhereSDK.cmake` just doesn't find `kit/lib/rac_backend_qhexrt.lib` because it was never copied there). If a fresh overlay build reports zero backends, check this BEFORE suspecting the overlay tarball's contents. - **`qwen3.8-27b-1bit-npu`'s `HostOpFailed` had THREE compounding causes, found and fixed one at a time — a kit-pin bump to v0.20.31 alone was NOT - enough; RCLI needed its own additional fix (below) even with a perfectly + enough; Wally needed its own additional fix (below) even with a perfectly merged overlay.** 1. The overlay-skel-files-never-shipped bug (above), fixed upstream in `runanywhere-sdks`' overlay packaging script. @@ -236,10 +236,10 @@ Mac; ARM64 MSVC + QHexRT overlay on Snapdragon). in `neurun` v0.20.31 (`Backend::profile()` no longer shares `ensure_device()` with `device()`) — see that repo's `qhexrt-profile-must-not-create-live-device` KB finding. - 3. **RCLI-specific, and NOT fixed by the kit-pin bump alone**: + 3. **Wally-specific, and NOT fixed by the kit-pin bump alone**: `copy-overlay-dlls.cmake` globbed `*.dll` only, so even a correctly merged overlay (per the bullet above) left the Bonsai skel's `.so`/ - `.cat` sitting in `kit/bin/` and NEVER staged next to `rcli.exe` — the + `.cat` sitting in `kit/bin/` and NEVER staged next to `wally.exe` — the one place `fastrpc_win.cpp`'s `ADSP_LIBRARY_PATH ∪ exe_dir()` search actually looks. Fixed by widening the glob to `*.dll *.so *.cat`. **`fastrpc_win.cpp`'s `SET_PATH`/`GET_PATH` both returning a non-zero rc @@ -253,6 +253,6 @@ Mac; ARM64 MSVC + QHexRT overlay on Snapdragon). missing skel, missing/wrong/stale `.cat`, or — as this entry adds — the pair never being in the searched directory at all). Confirmed fixed end to end on a Snapdragon X2 Elite with all three fixes - in place: `rcli run --engine qhexrt` against `qwen3.8-27b-1bit-npu` opens + in place: `wally run --engine qhexrt` against `qwen3.8-27b-1bit-npu` opens the cDSP session and generates correctly ("The capital of France is **Paris**.", 0.105 tok/s, 12425 DSP linears). diff --git a/.agents/skills/rcli-kit-pin/SKILL.md b/.claude/skills/wally-kit-pin/SKILL.md similarity index 65% rename from .agents/skills/rcli-kit-pin/SKILL.md rename to .claude/skills/wally-kit-pin/SKILL.md index f85b77c..5d0dc51 100644 --- a/.agents/skills/rcli-kit-pin/SKILL.md +++ b/.claude/skills/wally-kit-pin/SKILL.md @@ -1,12 +1,12 @@ --- -name: rcli-kit-pin +name: wally-kit-pin description: Bump cmake/sdk-pin.cmake to a new published SDK C++ desktop kit (version + SHA-256 + IDL lock). Use after an SDK GitHub Release is published (not draft), when fetch-kit.sh 404s, or when SCHEMA_LOCK mismatches. --- -# RCLI kit pin +# Wally kit pin -Pin file: `cmake/sdk-pin.cmake`. Fetcher: `scripts/fetch-kit.sh`. Overlay: -`scripts/fetch-private-pack.sh`. CMake wrapper: `cmake/RunAnywhereSDK.cmake`. +Pin file: `cmake/sdk-pin.cmake`. Fetcher: `scripts/build/fetch-kit.sh`. Overlay: +`scripts/build/fetch-private-pack.sh`. CMake wrapper: `cmake/RunAnywhereSDK.cmake`. Companion: **cpp-desktop-kit** in runanywhere-sdks (what the tarball contains). @@ -17,8 +17,8 @@ Companion: **cpp-desktop-kit** in runanywhere-sdks (what the tarball contains). 2. Public assets exist: `RunAnywhere-cpp-desktop-{macos-arm64,windows-x64}-v$SDK.tar.gz` (+ windows-arm64 once that kit is on the release). -3. Do **not** point `RCLI_SDK_DIR` at SDK **source** — configure error. - Prefix is `-DCMAKE_PREFIX_PATH=` or `-DRCLI_SDK_KIT=`. +3. Do **not** point `WALLY_SDK_DIR` at SDK **source** — configure error. + Prefix is `-DCMAKE_PREFIX_PATH=` or `-DWALLY_SDK_KIT=`. ## Bump together (never version-only) @@ -31,26 +31,26 @@ shasum -a 256 /tmp/kits/RunAnywhere-cpp-desktop-*.tar.gz Update in `cmake/sdk-pin.cmake`: -- `RCLI_PINNED_SDK_VERSION` -- `RCLI_PINNED_KIT_SHA256_MACOS_ARM64` -- `RCLI_PINNED_KIT_SHA256_WINDOWS_X64` -- `RCLI_PINNED_KIT_SHA256_WINDOWS_ARM64` (uncomment once the asset exists) +- `WALLY_PINNED_SDK_VERSION` +- `WALLY_PINNED_KIT_SHA256_MACOS_ARM64` +- `WALLY_PINNED_KIT_SHA256_WINDOWS_X64` +- `WALLY_PINNED_KIT_SHA256_WINDOWS_ARM64` (uncomment once the asset exists) - IDL triple copied from the kit's `share/runanywhere/SCHEMA_LOCK`: - `RCLI_PINNED_IDL_VERSION`, `RCLI_PINNED_IDL_SCHEMA_SHA256`, - `RCLI_PINNED_IDL_PROTOC_VERSION` + `WALLY_PINNED_IDL_VERSION`, `WALLY_PINNED_IDL_SCHEMA_SHA256`, + `WALLY_PINNED_IDL_PROTOC_VERSION` -`fetch-kit.sh` refuses `SDK_VERSION != RCLI_PINNED_SDK_VERSION`. Do not override +`fetch-kit.sh` refuses `SDK_VERSION != WALLY_PINNED_SDK_VERSION`. Do not override only an env var to "try" a newer kit — checksums are keyed to the pin. Configure fails if the extracted kit's `SCHEMA_LOCK` does not match the IDL pin. When the schema changes, consume a new kit and bump the pin — never -regenerate headers locally. RCLI never runs `protoc`. +regenerate headers locally. Wally never runs `protoc`. ## CI Swift tree `.github/workflows/ci.yml` and `release.yml` check out `RunanywhereAI/runanywhere-sdks` at `ref: v$SDK` into `.deps/runanywhere-sdks` -for the Apple MLX host (`RCLI_SDK_SWIFT_PATH`). Bump that ref in the **same +for the Apple MLX host (`WALLY_SDK_SWIFT_PATH`). Bump that ref in the **same commit** as the pin. macos-26 (Xcode 26 / Swift 6.2 — matches SDK `swift-tools-version: 6.2`); macos-15 is Swift 6.1; macos-14 is Swift 5.10. @@ -58,29 +58,29 @@ commit** as the pin. macos-26 (Xcode 26 / Swift 6.2 — matches SDK Not part of the pin. Applied after extract: -1. `RCLI_PRIVATE_OVERLAY` (local tarball) +1. `WALLY_PRIVATE_OVERLAY` (local tarball) 2. tarball next to the kit prefix named `RunAnywhere-cpp-desktop-{macos-arm64-neurt,windows-arm64-qhexrt}-private-v*.tar.gz` -3. skip (OSS) unless `RCLI_REQUIRE_PRIVATE=1` +3. skip (OSS) unless `WALLY_REQUIRE_PRIVATE=1` QHexRT is Windows ARM64 only. Public bottles must stay OSS — never bake overlay bytes into a Homebrew bottle or a public GitHub Release asset. Windows ARM64 **public** kit is commons-only (no llama.cpp). Overlay apply -then rebuild product `rcli.exe`. `v0.20.28` ARM64 kits are missing +then rebuild product `wally.exe`. `v0.20.28` ARM64 kits are missing `lib/libcurl.lib` (packager only globbed x64-windows-static) — copy from -vcpkg `arm64-windows-static` until the next SDK kit train. RCLI +vcpkg `arm64-windows-static` until the next SDK kit train. Wally `cmake/RunAnywhereSDK.cmake` links that import lib when present. NeuRT: public mac bottle has MLX, not NeuRT. Overlay tarball + -`RCLI_APPLE_MLX_HOST=ON` rebuild. See **rcli-e2e** for QAIRT / SD15 / DSP +`WALLY_APPLE_MLX_HOST=ON` rebuild. See **wally-e2e** for QAIRT / SD15 / DSP gotchas; do not weaken public CI to require overlays. ## Verify ```bash -bash scripts/fetch-kit.sh macos-arm64 /tmp/kit +bash scripts/build/fetch-kit.sh macos-arm64 /tmp/kit cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=/tmp/kit cmake --build build -j "$(sysctl -n hw.logicalcpu)" -RCLI_SDK_KIT=/tmp/kit bash scripts/e2e.sh ./build/rcli +WALLY_SDK_KIT=/tmp/kit bash scripts/test/e2e.sh ./build/wally ``` diff --git a/.agents/skills/rcli-release/SKILL.md b/.claude/skills/wally-release/SKILL.md similarity index 64% rename from .agents/skills/rcli-release/SKILL.md rename to .claude/skills/wally-release/SKILL.md index e05d6b0..f5919db 100644 --- a/.agents/skills/rcli-release/SKILL.md +++ b/.claude/skills/wally-release/SKILL.md @@ -1,16 +1,16 @@ --- -name: rcli-release -description: Cut an RCLI product release (independent of SDK version) — version bump, release:patch label, merge, auto-tag, bottles. Use when shipping rcli after a published SDK kit, or when Homebrew / notarization / private overlays must not leak into public bottles. +name: wally-release +description: Cut an Wally product release (independent of SDK version) — version bump, release:patch label, merge, auto-tag, bottles. Use when shipping wally after a published SDK kit, or when Homebrew / notarization / private overlays must not leak into public bottles. --- -# RCLI release +# Wally release -Repo: `RunanywhereAI/RCLI`. Product version is `project(rcli VERSION x.y.z)` in -`CMakeLists.txt` (also stamped into `Formula/rcli.rb`). Independent of the SDK +Repo: `RunanywhereAI/wally`. Product version is `project(wally VERSION x.y.z)` in +`CMakeLists.txt` (also stamped into `Formula/wally.rb`). Independent of the SDK kit pin. Do this **after** the SDK GitHub Release this pin targets is **published** -(not draft). Companion: **rcli-kit-pin**, then this skill. +(not draft). Companion: **wally-kit-pin**, then this skill. ## 0. Facts @@ -24,7 +24,7 @@ Do this **after** the SDK GitHub Release this pin targets is **published** **macos runner must be macos-26** (Xcode 26 / Swift 6.2). macos-15 is Swift 6.1 and cannot resolve the SDK package; macos-14 is Swift 5.10. - Public bottle / zip is OSS only. NeuRT / QHexRT overlays are workflow - artifacts or `RCLI_PRIVATE_OVERLAY`, never public release assets, never + artifacts or `WALLY_PRIVATE_OVERLAY`, never public release assets, never Homebrew bottles. - Linux bottles are not a v1 merge blocker. @@ -32,12 +32,12 @@ Do this **after** the SDK GitHub Release this pin targets is **published** If this train needs new engines (sherpa routable, windows-arm64 kit, NeuRT image gen): bump `cmake/sdk-pin.cmake` + CI `ref: v$SDK` in the **same PR** -(**rcli-kit-pin**). CI must be green on that pin before you bump the product +(**wally-kit-pin**). CI must be green on that pin before you bump the product version. ## 2. Product version + label -Next patch after `0.5.0` is `0.5.1`. Bump `CMakeLists.txt` `project(rcli +Next patch after `0.5.0` is `0.5.1`. Bump `CMakeLists.txt` `project(wally VERSION …)` and any Formula version stamp in the same commit. Open/update the PR and apply **exactly one** `release:patch` (or minor/major). @@ -46,7 +46,7 @@ verify the label landed (`gh pr view --json labels`). ## 3. CI bar before merge -Required: macOS product e2e (`scripts/e2e.sh ./build/rcli`) and Windows e2e +Required: macOS product e2e (`scripts/test/e2e.sh ./build/wally`) and Windows e2e against the pinned kit. `agents-sync.yml` must pass (`scripts/ci/check-agents-sync.sh`). @@ -54,7 +54,7 @@ Re-check immediately before merge (a rollup of SUCCESS can hide checks that have not started): ```bash -gh pr checks --repo RunanywhereAI/RCLI --json name,state,bucket \ +gh pr checks --repo RunanywhereAI/wally --json name,state,bucket \ --jq '[.[] | select(.state == null or .state == "")] | length' # must be 0 ``` @@ -64,7 +64,7 @@ the SDK (`bypass_pull_request_allowances`) before `--admin`. ## 4. Merge triggers the release ```bash -gh pr merge --repo RunanywhereAI/RCLI --squash --admin # only if allow-listed +gh pr merge --repo RunanywhereAI/wally --squash --admin # only if allow-listed ``` `auto-tag.yml` tags `v$PRODUCT` and dispatches `release.yml`. Unlike the SDK @@ -75,17 +75,17 @@ link + e2e). ## 5. Verify the GitHub Release ```bash -gh release view v$PRODUCT --repo RunanywhereAI/RCLI --json isDraft,assets \ +gh release view v$PRODUCT --repo RunanywhereAI/wally --json isDraft,assets \ --jq '{isDraft, names: [.assets[].name]}' ``` -Expect `rcli-$PRODUCT-macos-arm64.tar.gz` (+ `.sha256`) and -`rcli-$PRODUCT-windows-x64.zip` (+ `.sha256`). **Must be empty** for +Expect `wally-$PRODUCT-macos-arm64.tar.gz` (+ `.sha256`) and +`wally-$PRODUCT-windows-x64.zip` (+ `.sha256`). **Must be empty** for `*neurt*`, `*qhexrt*`, `*private*`. If `release.yml` still creates a non-draft release, that is the live product cut — confirm asset names before anyone bottles from it. Stamp Formula from -the macOS sidecar (`scripts/stamp-formula.py`) when that path is wired. +the macOS sidecar (`scripts/release/stamp-formula.py`) when that path is wired. ## 6. Overlays after the public cut @@ -93,21 +93,21 @@ Private packs stay off the public release. For a machine that should load NeuRT / QHexRT: ```bash -export RCLI_PRIVATE_OVERLAY=/path/to/RunAnywhere-cpp-desktop-macos-arm64-neurt-private-v$SDK.tar.gz +export WALLY_PRIVATE_OVERLAY=/path/to/RunAnywhere-cpp-desktop-macos-arm64-neurt-private-v$SDK.tar.gz # or windows-arm64-qhexrt ``` -`RCLI_REQUIRE_PRIVATE=1` fails closed when the overlay is missing. Default CI +`WALLY_REQUIRE_PRIVATE=1` fails closed when the overlay is missing. Default CI must pass without it. -Device proof is not CI: rebuild overlay `rcli` on a Mac (NeuRT image gen) and +Device proof is not CI: rebuild overlay `wally` on a Mac (NeuRT image gen) and on Snapdragon ARM64 Windows (QHexRT). Match QAIRT to the device Hexagon skel -(`ADSP_LIBRARY_PATH`); see **rcli-e2e**. Public `v0.5.1` bottles are OSS — -NeuRT/QHexRT will never appear in `rcli backends` on those artifacts. +(`ADSP_LIBRARY_PATH`); see **wally-e2e**. Public `v0.5.1` bottles are OSS — +NeuRT/QHexRT will never appear in `wally backends` on those artifacts. ## Do not -- Attach `rcli-*` assets onto an SDK GitHub Release. +- Attach `wally-*` assets onto an SDK GitHub Release. - Pin `fetch-kit.sh` at a draft SDK tag. - Ship NeuRT / QHexRT inside the public bottle "just this once". - Use macos-14 or macos-15 for the Apple job (need macos-26 / Swift 6.2). diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d9f0c57 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# The pinned contract is hashed byte-for-byte against a SHA-256 baked into +# console_contract.h at generation time (see test_wally_contract.cpp). Windows +# runners default to core.autocrlf=true, which rewrites LF to CRLF on checkout +# and breaks that hash. Keep it untouched on every platform. +contracts/wally-cli-v1.openapi.json -text diff --git a/.github/actions/build-wally/action.yml b/.github/actions/build-wally/action.yml new file mode 100644 index 0000000..30e994a --- /dev/null +++ b/.github/actions/build-wally/action.yml @@ -0,0 +1,71 @@ +name: Build wally +description: >- + Configure, build, and optionally package a wally bottle from a pinned C++ + desktop kit. The configure→build sequence is identical across ci.yml and + release.yml and every platform; this is the one place it lives. The dev/prod + bake is read from the WALLY_BAKED_CONSOLE_* environment the caller sets (empty + for prod), so nothing endpoint-shaped is passed here. + +inputs: + platform: + description: macos-arm64 | linux-x64 | windows-x64 | windows-arm64 + required: true + kit-dir: + description: Path to the fetched kit (CMAKE_PREFIX_PATH). + required: true + variant: + description: prod | dev — only affects the packaged filename (-dev). + required: false + default: prod + package: + description: 'true to also produce the release bottle; false to only build.' + required: false + default: 'false' + +runs: + using: composite + steps: + - name: Configure + build (${{ inputs.variant }}) + shell: bash + run: | + set -euo pipefail + prefix="${{ inputs.kit-dir }}" + # Windows ninja wants a forward-slash prefix. + if command -v cygpath >/dev/null 2>&1; then prefix="$(cygpath -m "$prefix")"; fi + extra=() + # Linux links the product but not the header-only-httplib test exes. + [[ "${{ inputs.platform }}" == linux-x64 ]] && extra+=(-DBUILD_TESTING=OFF) + # Compiler cache is best-effort: use whichever the caller's cache step + # installed (ccache, or sccache for MSVC), never fail the build for its + # absence. + if command -v ccache >/dev/null 2>&1; then + extra+=(-DCMAKE_CXX_COMPILER_LAUNCHER=ccache) + elif command -v sccache >/dev/null 2>&1; then + extra+=(-DCMAKE_CXX_COMPILER_LAUNCHER=sccache) + fi + cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release "${extra[@]}" \ + -DCMAKE_PREFIX_PATH="$prefix" + case "${{ inputs.platform }}" in + macos-arm64) cmake --build build -j "$(sysctl -n hw.logicalcpu)" ;; + linux-x64) cmake --build build --target wally -j "$(nproc)" ;; + *) cmake --build build --config Release ;; + esac + + - name: Package (${{ inputs.variant }}) + if: ${{ inputs.package == 'true' }} + shell: bash + env: + WALLY_SDK_KIT: ${{ inputs.kit-dir }} + run: | + set -euo pipefail + ver="${WALLY_VERSION#v}" + case "${{ inputs.platform }}" in + macos-arm64) + WALLY_VERSION="$ver" bash scripts/build/package-wally.sh build macos-arm64 "${{ inputs.variant }}" ;; + linux-x64) + WALLY_VERSION="$ver" bash scripts/build/package-wally.sh build linux-x86_64 "${{ inputs.variant }}" ;; + windows-x64) + pwsh -File scripts/build/package-wally-windows.ps1 -BuildDir build -Version "$ver" -KitDir "${{ inputs.kit-dir }}" -Channel "${{ inputs.variant }}" ;; + windows-arm64) + pwsh -File scripts/build/package-wally-windows.ps1 -BuildDir build -Version "$ver" -KitDir "${{ inputs.kit-dir }}" -Platform windows-arm64 -Channel "${{ inputs.variant }}" ;; + esac diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index 68c57d2..8def891 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -17,7 +17,7 @@ permissions: jobs: auto-tag: if: github.event.pull_request.merged == true - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Detect release label on merged PR id: label @@ -57,14 +57,14 @@ jobs: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - - name: Read version from CMakeLists.txt + - name: Read version from versions.toml if: steps.label.outputs.should-release == 'true' id: version run: | set -euo pipefail - REVIEWED=$(sed -nE 's/^project\(rcli VERSION ([0-9]+\.[0-9]+\.[0-9]+).*/\1/p' CMakeLists.txt | head -1) + REVIEWED=$(sed -nE 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*"([0-9]+\.[0-9]+\.[0-9]+)".*/\1/p' versions.toml | head -1) if [ -z "$REVIEWED" ]; then - echo "::error::Could not read project(rcli VERSION ...) from CMakeLists.txt" + echo "::error::Could not read the product version from versions.toml" exit 1 fi echo "Reviewed version: $REVIEWED" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02d2698..8968d27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ on: # `main` plus the launch branches, because a pull request stacked onto one # of those is still a pull request that has to be green. With `main` alone # the branches filter matches on the PR's base, so retargeting a PR from - # `main` to `launch/open-frontier-rcli` did not fail CI — it ran no CI at + # `main` to `launch/open-frontier-wally` did not fail CI — it ran no CI at # all, and the PR reported nothing rather than reporting a problem. branches: [main, 'launch/**'] @@ -31,9 +31,13 @@ jobs: run: | set -euo pipefail python3 tests/test_release_assets.py - python3 -m py_compile scripts/verify-release-assets.py scripts/stamp-formula.py - bash -n install.sh scripts/package-rcli.sh scripts/update-tap.sh - ruby -c Formula/rcli.rb + python3 -m py_compile scripts/release/verify-release-assets.py scripts/release/stamp-formula.py + bash -n install.sh scripts/build/package-wally.sh scripts/release/update-tap.sh + ruby -c Formula/wally.rb + - name: Console binding matches its pinned contract + run: python3 contracts/generate_console_binding.py --check + - name: Versions consistent with versions.toml + run: python3 scripts/ci/check-versions.py macos: # SDK Package.swift is swift-tools-version 6.2 (Xcode 26). macos-15 is @@ -41,7 +45,7 @@ jobs: runs-on: macos-26 timeout-minutes: 90 env: - RCLI_SDK_SWIFT_PATH: ${{ github.workspace }}/.deps/runanywhere-sdks + WALLY_SDK_SWIFT_PATH: ${{ github.workspace }}/.deps/runanywhere-sdks steps: - uses: actions/checkout@v4 with: @@ -55,13 +59,23 @@ jobs: path: .deps/runanywhere-sdks - name: Install ninja run: brew install ninja + - uses: hendrikmuhs/ccache-action@f09c25b45002a07be2955cbe52e8cee55643f89d + with: + key: ccache-ci-macos-arm64 + - name: Cache kit + id: kit + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: kit + key: kit-macos-arm64-${{ hashFiles('versions.toml') }} - name: Download C++ desktop kit + if: steps.kit.outputs.cache-hit != 'true' env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail kit="${GITHUB_WORKSPACE}/kit" - bash scripts/fetch-kit.sh macos-arm64 "$kit" + bash scripts/build/fetch-kit.sh macos-arm64 "$kit" if [[ ! -f "$kit/include/runanywhere/proto/model_types.pb.h" ]]; then echo "kit missing generated proto headers; listing prefix:" >&2 find "$kit" -maxdepth 4 -type d -print >&2 @@ -72,16 +86,57 @@ jobs: find "$kit/share" -print >&2 exit 1 fi - - name: Configure - run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="${GITHUB_WORKSPACE}/kit" - - name: Build - run: cmake --build build -j "$(sysctl -n hw.logicalcpu)" + - uses: ./.github/actions/build-wally + with: + platform: macos-arm64 + kit-dir: ${{ github.workspace }}/kit - name: Unit tests run: ctest --test-dir build --output-on-failure - name: Product e2e (MLX host) env: - RCLI_SDK_KIT: ${{ github.workspace }}/kit - run: bash scripts/e2e.sh ./build/rcli + WALLY_SDK_KIT: ${{ github.workspace }}/kit + run: bash scripts/test/e2e.sh ./build/wally + + linux: + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Install build deps + run: | + sudo apt-get update + sudo apt-get install -y ninja-build cmake libcurl4-openssl-dev + - uses: hendrikmuhs/ccache-action@f09c25b45002a07be2955cbe52e8cee55643f89d + with: + key: ccache-ci-linux-x64 + - name: Cache kit + id: kit + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: kit + key: kit-linux-x64-${{ hashFiles('versions.toml') }} + - name: Download C++ desktop kit + if: steps.kit.outputs.cache-hit != 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + bash scripts/build/fetch-kit.sh linux-x64 "${GITHUB_WORKSPACE}/kit" + # BUILD_TESTING=OFF (set inside the composite action for linux): the product + # `wally` links cleanly on Linux, but the unit-test executables trip a + # single-pass-ld quirk resolving header-only httplib's weak inline symbols + # out of wally_core.a. Those tests run on macOS/Windows; Linux validates the + # shipped binary through e2e. + - uses: ./.github/actions/build-wally + with: + platform: linux-x64 + kit-dir: ${{ github.workspace }}/kit + - name: Product e2e + env: + WALLY_SDK_KIT: ${{ github.workspace }}/kit + run: bash scripts/test/e2e.sh ./build/wally windows-arm64: # Native ARM64 runner, free for public repos. Cross-compiling from the x64 @@ -97,32 +152,37 @@ jobs: with: arch: arm64 - uses: seanmiddleditch/gha-setup-ninja@master + - uses: hendrikmuhs/ccache-action@f09c25b45002a07be2955cbe52e8cee55643f89d + with: + key: ccache-ci-windows-arm64 + variant: sccache + - name: Cache kit + id: kit + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: kit + key: kit-windows-arm64-${{ hashFiles('versions.toml') }} - name: Download C++ desktop kit + if: steps.kit.outputs.cache-hit != 'true' env: GH_TOKEN: ${{ github.token }} shell: bash run: | set -euo pipefail kit="${GITHUB_WORKSPACE}/kit" - bash scripts/fetch-kit.sh windows-arm64 "$kit" + bash scripts/build/fetch-kit.sh windows-arm64 "$kit" test -f "$kit/include/runanywhere/proto/model_types.pb.h" - - name: Configure - shell: bash - run: | - prefix="${GITHUB_WORKSPACE}/kit" - if command -v cygpath >/dev/null 2>&1; then - prefix="$(cygpath -m "$prefix")" - fi - cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="$prefix" - - name: Build - run: cmake --build build --config Release + - uses: ./.github/actions/build-wally + with: + platform: windows-arm64 + kit-dir: ${{ github.workspace }}/kit - name: Unit tests run: ctest --test-dir build --output-on-failure -C Release - name: Smoke shell: bash env: - RCLI_SDK_KIT: ${{ github.workspace }}/kit - run: bash scripts/e2e.sh ./build/rcli.exe + WALLY_SDK_KIT: ${{ github.workspace }}/kit + run: bash scripts/test/e2e.sh ./build/wally.exe windows: runs-on: windows-2022 @@ -133,29 +193,34 @@ jobs: persist-credentials: false - uses: ilammy/msvc-dev-cmd@v1 - uses: seanmiddleditch/gha-setup-ninja@master + - uses: hendrikmuhs/ccache-action@f09c25b45002a07be2955cbe52e8cee55643f89d + with: + key: ccache-ci-windows-x64 + variant: sccache + - name: Cache kit + id: kit + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: kit + key: kit-windows-x64-${{ hashFiles('versions.toml') }} - name: Download C++ desktop kit + if: steps.kit.outputs.cache-hit != 'true' env: GH_TOKEN: ${{ github.token }} shell: bash run: | set -euo pipefail kit="${GITHUB_WORKSPACE}/kit" - bash scripts/fetch-kit.sh windows-x64 "$kit" + bash scripts/build/fetch-kit.sh windows-x64 "$kit" test -f "$kit/include/runanywhere/proto/model_types.pb.h" - - name: Configure - shell: bash - run: | - prefix="${GITHUB_WORKSPACE}/kit" - if command -v cygpath >/dev/null 2>&1; then - prefix="$(cygpath -m "$prefix")" - fi - cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="$prefix" - - name: Build - run: cmake --build build --config Release + - uses: ./.github/actions/build-wally + with: + platform: windows-x64 + kit-dir: ${{ github.workspace }}/kit - name: Unit tests run: ctest --test-dir build --output-on-failure -C Release - name: Smoke shell: bash env: - RCLI_SDK_KIT: ${{ github.workspace }}/kit - run: bash scripts/e2e.sh ./build/rcli.exe + WALLY_SDK_KIT: ${{ github.workspace }}/kit + run: bash scripts/test/e2e.sh ./build/wally.exe diff --git a/.github/workflows/oss-keyless-telemetry.yml b/.github/workflows/oss-keyless-telemetry.yml index 78a268d..e542449 100644 --- a/.github/workflows/oss-keyless-telemetry.yml +++ b/.github/workflows/oss-keyless-telemetry.yml @@ -1,8 +1,8 @@ # ============================================================================= -# OSS keyless telemetry gate — rcli development → public staging backend +# OSS keyless telemetry gate — wally development → public staging backend # ============================================================================= # Primary CI for the open-source contract: consume a published C++ desktop kit, -# build rcli, and keyless-blast all 12 modalities at the staging backend +# build wally, and keyless-blast all 12 modalities at the staging backend # (PUBLIC org). No API key. Staging origin comes from repo secrets/vars — # not hardcoded hosts. # @@ -26,7 +26,7 @@ permissions: jobs: keyless-staging-blast: - name: rcli keyless → staging backend + name: wally keyless → staging backend runs-on: macos-14 timeout-minutes: 60 steps: @@ -51,7 +51,7 @@ jobs: echo "skip=false" >> "$GITHUB_OUTPUT" fi - - name: Fetch kit + build rcli + keyless blast + - name: Fetch kit + build wally + keyless blast if: steps.origin.outputs.skip != 'true' env: STAGING_BASE_URL: ${{ secrets.STAGING_BASE_URL }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 72b5fa4..6ed0b73 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,16 +15,28 @@ permissions: contents: write env: - RCLI_VERSION: ${{ github.event.inputs.version || github.ref_name }} + WALLY_VERSION: ${{ github.event.inputs.version || github.ref_name }} +# Every platform builds two bottles from one kit: `prod` (empty bake -> the +# production console defaults in credentials.cpp) and `dev` (dev console +# endpoints baked from the WALLY_DEV_* repo variables, so a dev build targets +# the dev backend with no env vars). The bake reaches the configure step as +# environment only — never on the command line, never in committed source. The +# configure→build→package sequence lives in .github/actions/build-wally; e2e +# runs on the binary it produced, so the bottle is built exactly once. jobs: macos: - # SDK Package.swift is swift-tools-version 6.2 (Xcode 26). macos-15 is - # Xcode 16.4 / Swift 6.1; macos-14 is Xcode 15 / Swift 5.10. + # SDK Package.swift is swift-tools-version 6.2 (Xcode 26). runs-on: macos-26 timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + variant: [prod, dev] env: - RCLI_SDK_SWIFT_PATH: ${{ github.workspace }}/.deps/runanywhere-sdks + WALLY_SDK_SWIFT_PATH: ${{ github.workspace }}/.deps/runanywhere-sdks + WALLY_BAKED_CONSOLE_API_URL: ${{ matrix.variant == 'dev' && vars.WALLY_DEV_CONSOLE_API_URL || '' }} + WALLY_BAKED_CONSOLE_WEB_ORIGIN: ${{ matrix.variant == 'dev' && vars.WALLY_DEV_CONSOLE_WEB_ORIGIN || '' }} steps: - uses: actions/checkout@v4 with: @@ -38,39 +50,54 @@ jobs: path: .deps/runanywhere-sdks - name: Install ninja run: brew install ninja + - uses: hendrikmuhs/ccache-action@f09c25b45002a07be2955cbe52e8cee55643f89d + with: + key: ccache-release-macos-arm64 + - name: Cache kit + id: kit + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: kit + key: kit-macos-arm64-${{ hashFiles('versions.toml') }} - name: Download C++ desktop kit + if: steps.kit.outputs.cache-hit != 'true' env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - bash scripts/fetch-kit.sh macos-arm64 "${GITHUB_WORKSPACE}/kit" - - name: Configure + build - run: | - cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="${GITHUB_WORKSPACE}/kit" - cmake --build build -j "$(sysctl -n hw.logicalcpu)" + bash scripts/build/fetch-kit.sh macos-arm64 "${GITHUB_WORKSPACE}/kit" + - uses: ./.github/actions/build-wally + with: + platform: macos-arm64 + kit-dir: ${{ github.workspace }}/kit + variant: ${{ matrix.variant }} + package: 'true' - name: Product e2e env: - RCLI_SDK_KIT: ${{ github.workspace }}/kit - run: bash scripts/e2e.sh ./build/rcli - - name: Package + WALLY_SDK_KIT: ${{ github.workspace }}/kit + run: bash scripts/test/e2e.sh ./build/wally + - name: Verify archive run: | - VERSION="${RCLI_VERSION#v}" - RCLI_VERSION="${VERSION}" RCLI_SDK_KIT="${GITHUB_WORKSPACE}/kit" \ - bash scripts/package-rcli.sh build macos-arm64 - python3 scripts/verify-release-assets.py \ - "dist/rcli-${VERSION}-macos-arm64.tar.gz" \ - "dist/rcli-${VERSION}-macos-arm64.tar.gz.sha256" + VERSION="${WALLY_VERSION#v}" + suffix=""; [ "${{ matrix.variant }}" = dev ] && suffix="-dev" + python3 scripts/release/verify-release-assets.py \ + "dist/wally-${VERSION}-macos-arm64${suffix}.tar.gz" \ + "dist/wally-${VERSION}-macos-arm64${suffix}.tar.gz.sha256" - uses: actions/upload-artifact@v4 with: - name: rcli-macos-arm64 - path: dist/rcli-*-macos-arm64.tar.gz* + name: wally-macos-arm64-${{ matrix.variant }} + path: dist/wally-*-macos-arm64*.tar.gz* windows-arm64: - # install.ps1 already prefers rcli--windows-arm64.zip and warns when it - # is absent; until this job existed the preferred path could never be taken - # and every Windows-on-ARM user silently got the emulated x64 build. runs-on: windows-11-arm timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + variant: [prod, dev] + env: + WALLY_BAKED_CONSOLE_API_URL: ${{ matrix.variant == 'dev' && vars.WALLY_DEV_CONSOLE_API_URL || '' }} + WALLY_BAKED_CONSOLE_WEB_ORIGIN: ${{ matrix.variant == 'dev' && vars.WALLY_DEV_CONSOLE_WEB_ORIGIN || '' }} steps: - uses: actions/checkout@v4 with: @@ -79,40 +106,46 @@ jobs: with: arch: arm64 - uses: seanmiddleditch/gha-setup-ninja@master + - uses: hendrikmuhs/ccache-action@f09c25b45002a07be2955cbe52e8cee55643f89d + with: + key: ccache-release-windows-arm64 + variant: sccache + - name: Cache kit + id: kit + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: kit + key: kit-windows-arm64-${{ hashFiles('versions.toml') }} - name: Download C++ desktop kit + if: steps.kit.outputs.cache-hit != 'true' env: GH_TOKEN: ${{ github.token }} shell: bash run: | set -euo pipefail - bash scripts/fetch-kit.sh windows-arm64 "${GITHUB_WORKSPACE}/kit" - - name: Configure + build - shell: bash - run: | - prefix="${GITHUB_WORKSPACE}/kit" - if command -v cygpath >/dev/null 2>&1; then - prefix="$(cygpath -m "$prefix")" - fi - cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="$prefix" - cmake --build build --config Release + bash scripts/build/fetch-kit.sh windows-arm64 "${GITHUB_WORKSPACE}/kit" + - uses: ./.github/actions/build-wally + with: + platform: windows-arm64 + kit-dir: ${{ github.workspace }}/kit + variant: ${{ matrix.variant }} + package: 'true' - name: Product e2e shell: bash env: - RCLI_SDK_KIT: ${{ github.workspace }}/kit - run: bash scripts/e2e.sh ./build/rcli.exe - - name: Package - shell: pwsh + WALLY_SDK_KIT: ${{ github.workspace }}/kit + run: bash scripts/test/e2e.sh ./build/wally.exe + - name: Verify archive + shell: bash run: | - $ver = $env:RCLI_VERSION.TrimStart('v') - $kit = Join-Path $env:GITHUB_WORKSPACE "kit" - $env:RCLI_SDK_KIT = $kit - & powershell -File scripts/package-rcli-windows.ps1 -BuildDir build -Version $ver -KitDir $kit -Platform windows-arm64 - python scripts/verify-release-assets.py ` - "dist/rcli-$ver-windows-arm64.zip" ` - "dist/rcli-$ver-windows-arm64.zip.sha256" + ver="${WALLY_VERSION#v}" + suffix=""; [ "${{ matrix.variant }}" = dev ] && suffix="-dev" + python scripts/release/verify-release-assets.py \ + "dist/wally-$ver-windows-arm64$suffix.zip" \ + "dist/wally-$ver-windows-arm64$suffix.zip.sha256" - uses: actions/upload-artifact@v4 with: - name: rcli-windows-arm64 + name: wally-windows-arm64-${{ matrix.variant }} path: | dist/*.zip dist/*.zip.sha256 @@ -121,55 +154,122 @@ jobs: windows: runs-on: windows-2022 timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + variant: [prod, dev] + env: + WALLY_BAKED_CONSOLE_API_URL: ${{ matrix.variant == 'dev' && vars.WALLY_DEV_CONSOLE_API_URL || '' }} + WALLY_BAKED_CONSOLE_WEB_ORIGIN: ${{ matrix.variant == 'dev' && vars.WALLY_DEV_CONSOLE_WEB_ORIGIN || '' }} steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: ilammy/msvc-dev-cmd@v1 - uses: seanmiddleditch/gha-setup-ninja@master + - uses: hendrikmuhs/ccache-action@f09c25b45002a07be2955cbe52e8cee55643f89d + with: + key: ccache-release-windows-x64 + variant: sccache + - name: Cache kit + id: kit + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: kit + key: kit-windows-x64-${{ hashFiles('versions.toml') }} - name: Download C++ desktop kit + if: steps.kit.outputs.cache-hit != 'true' env: GH_TOKEN: ${{ github.token }} shell: bash run: | set -euo pipefail - bash scripts/fetch-kit.sh windows-x64 "${GITHUB_WORKSPACE}/kit" - - name: Configure + build - shell: bash - run: | - prefix="${GITHUB_WORKSPACE}/kit" - if command -v cygpath >/dev/null 2>&1; then - prefix="$(cygpath -m "$prefix")" - fi - cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="$prefix" - cmake --build build --config Release + bash scripts/build/fetch-kit.sh windows-x64 "${GITHUB_WORKSPACE}/kit" + - uses: ./.github/actions/build-wally + with: + platform: windows-x64 + kit-dir: ${{ github.workspace }}/kit + variant: ${{ matrix.variant }} + package: 'true' - name: Product e2e shell: bash env: - RCLI_SDK_KIT: ${{ github.workspace }}/kit - run: bash scripts/e2e.sh ./build/rcli.exe - - name: Package - shell: pwsh + WALLY_SDK_KIT: ${{ github.workspace }}/kit + run: bash scripts/test/e2e.sh ./build/wally.exe + - name: Verify archive + shell: bash run: | - $ver = $env:RCLI_VERSION.TrimStart('v') - $kit = Join-Path $env:GITHUB_WORKSPACE "kit" - $env:RCLI_SDK_KIT = $kit - & powershell -File scripts/package-rcli-windows.ps1 -BuildDir build -Version $ver -KitDir $kit - python scripts/verify-release-assets.py ` - "dist/rcli-$ver-windows-x86_64.zip" ` - "dist/rcli-$ver-windows-x86_64.zip.sha256" - # Zip only. onnxruntime.dll lives next to rcli.exe *inside* the archive; - # uploading the loose DLL/exe made them look like first-class release assets. + ver="${WALLY_VERSION#v}" + suffix=""; [ "${{ matrix.variant }}" = dev ] && suffix="-dev" + python scripts/release/verify-release-assets.py \ + "dist/wally-$ver-windows-x86_64$suffix.zip" \ + "dist/wally-$ver-windows-x86_64$suffix.zip.sha256" - uses: actions/upload-artifact@v4 with: - name: rcli-windows-x64 + name: wally-windows-x64-${{ matrix.variant }} path: | dist/*.zip dist/*.zip.sha256 if-no-files-found: error + linux: + runs-on: ubuntu-24.04 + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + variant: [prod, dev] + env: + WALLY_BAKED_CONSOLE_API_URL: ${{ matrix.variant == 'dev' && vars.WALLY_DEV_CONSOLE_API_URL || '' }} + WALLY_BAKED_CONSOLE_WEB_ORIGIN: ${{ matrix.variant == 'dev' && vars.WALLY_DEV_CONSOLE_WEB_ORIGIN || '' }} + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Install build deps + run: | + sudo apt-get update + sudo apt-get install -y ninja-build cmake libcurl4-openssl-dev + - uses: hendrikmuhs/ccache-action@f09c25b45002a07be2955cbe52e8cee55643f89d + with: + key: ccache-release-linux-x64 + - name: Cache kit + id: kit + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: kit + key: kit-linux-x64-${{ hashFiles('versions.toml') }} + - name: Download C++ desktop kit + if: steps.kit.outputs.cache-hit != 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + bash scripts/build/fetch-kit.sh linux-x64 "${GITHUB_WORKSPACE}/kit" + - uses: ./.github/actions/build-wally + with: + platform: linux-x64 + kit-dir: ${{ github.workspace }}/kit + variant: ${{ matrix.variant }} + package: 'true' + - name: Product e2e + env: + WALLY_SDK_KIT: ${{ github.workspace }}/kit + run: bash scripts/test/e2e.sh ./build/wally + - name: Verify archive + run: | + VERSION="${WALLY_VERSION#v}" + suffix=""; [ "${{ matrix.variant }}" = dev ] && suffix="-dev" + python3 scripts/release/verify-release-assets.py \ + "dist/wally-${VERSION}-linux-x86_64${suffix}.tar.gz" \ + "dist/wally-${VERSION}-linux-x86_64${suffix}.tar.gz.sha256" + - uses: actions/upload-artifact@v4 + with: + name: wally-linux-x86_64-${{ matrix.variant }} + path: dist/wally-*-linux-x86_64*.tar.gz* + publish: - needs: [macos, windows, windows-arm64] + needs: [macos, windows, windows-arm64, linux] runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -182,31 +282,38 @@ jobs: - name: Verify release archives run: | set -euo pipefail - VERSION="${RCLI_VERSION#v}" - python3 scripts/verify-release-assets.py \ - "artifacts/rcli-${VERSION}-macos-arm64.tar.gz" \ - "artifacts/rcli-${VERSION}-macos-arm64.tar.gz.sha256" - python3 scripts/verify-release-assets.py \ - "artifacts/rcli-${VERSION}-windows-x86_64.zip" \ - "artifacts/rcli-${VERSION}-windows-x86_64.zip.sha256" - python3 scripts/verify-release-assets.py \ - "artifacts/rcli-${VERSION}-windows-arm64.zip" \ - "artifacts/rcli-${VERSION}-windows-arm64.zip.sha256" + VERSION="${WALLY_VERSION#v}" + # Both bottles of every platform: prod (no suffix) and dev (-dev). + for suffix in "" "-dev"; do + python3 scripts/release/verify-release-assets.py \ + "artifacts/wally-${VERSION}-macos-arm64${suffix}.tar.gz" \ + "artifacts/wally-${VERSION}-macos-arm64${suffix}.tar.gz.sha256" + python3 scripts/release/verify-release-assets.py \ + "artifacts/wally-${VERSION}-windows-x86_64${suffix}.zip" \ + "artifacts/wally-${VERSION}-windows-x86_64${suffix}.zip.sha256" + python3 scripts/release/verify-release-assets.py \ + "artifacts/wally-${VERSION}-windows-arm64${suffix}.zip" \ + "artifacts/wally-${VERSION}-windows-arm64${suffix}.zip.sha256" + python3 scripts/release/verify-release-assets.py \ + "artifacts/wally-${VERSION}-linux-x86_64${suffix}.tar.gz" \ + "artifacts/wally-${VERSION}-linux-x86_64${suffix}.tar.gz.sha256" + done - name: Generate Homebrew formula update run: | set -euo pipefail - VERSION="${RCLI_VERSION#v}" - sidecar="artifacts/rcli-${VERSION}-macos-arm64.tar.gz.sha256" + VERSION="${WALLY_VERSION#v}" + # Homebrew ships the production bottle; the dev bottle is not tapped. + sidecar="artifacts/wally-${VERSION}-macos-arm64.tar.gz.sha256" digest=$(awk 'NF == 2 { print $1 }' "$sidecar") [[ "$digest" =~ ^[0-9a-f]{64}$ ]] - python3 scripts/stamp-formula.py "$VERSION" "macos-arm64=${digest}" - ruby -c Formula/rcli.rb + python3 scripts/release/stamp-formula.py "$VERSION" "macos-arm64=${digest}" + ruby -c Formula/wally.rb mkdir -p release-metadata - cp Formula/rcli.rb release-metadata/rcli.rb + cp Formula/wally.rb release-metadata/wally.rb - uses: actions/upload-artifact@v4 with: - name: rcli-homebrew-formula - path: release-metadata/rcli.rb + name: wally-homebrew-formula + path: release-metadata/wally.rb if-no-files-found: error - name: Create the release uses: softprops/action-gh-release@v2 @@ -214,10 +321,10 @@ jobs: tag_name: ${{ github.event_name == 'push' && github.ref_name || format('v{0}', github.event.inputs.version) }} name: ${{ github.event_name == 'push' && github.ref_name || format('v{0}', github.event.inputs.version) }} files: | - artifacts/rcli-*.tar.gz - artifacts/rcli-*.tar.gz.sha256 - artifacts/rcli-*.zip - artifacts/rcli-*.zip.sha256 + artifacts/wally-*.tar.gz + artifacts/wally-*.tar.gz.sha256 + artifacts/wally-*.zip + artifacts/wally-*.zip.sha256 generate_release_notes: true fail_on_unmatched_files: true env: diff --git a/.gitignore b/.gitignore index f3d6fd2..845f0a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ -# Build output -build/ -build-*/ -cmake-build-*/ -dist/ +# Build output (root-anchored so .github/actions/build-wally/ isn't caught). +/build/ +/build-*/ +/cmake-build-*/ +/dist/ # Shallow SDK checkout used by CI to link the Swift MLX host. .deps/ @@ -18,7 +18,7 @@ swift/.build/ # creates this now; the pattern stays so an old working copy is not committed. deps/ -# Model weights, in case any land in the tree. rcli keeps them out of the repo, +# Model weights, in case any land in the tree. wally keeps them out of the repo, # under ~/.local/share/runanywhere. # Use /models/ (root-level only) to avoid ignoring src/models/ headers /models/ @@ -84,3 +84,4 @@ dist/ # Local review notes — never part of the product repo. /comments/ +__pycache__/ diff --git a/AGENTS.md b/AGENTS.md index c8fdc89..d040f1e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,17 +1,17 @@ -# AGENTS.md — RCLI +# AGENTS.md — Wally > **`AGENTS.md` is the real file.** `CLAUDE.md` is a committed symlink to it. > Editing either name edits the same bytes. `bash scripts/ci/check-agents-sync.sh --fix` > recreates the link and mirrors `.claude/skills/` → `.agents/skills/`. CI fails > if the pair drifts. -This repository is the official `rcli` product CLI. It consumes a **packaged +This repository is the official `wally` product CLI. It consumes a **packaged C++ desktop kit** via `find_package(RunAnywhere)`. It does not `add_subdirectory` or FetchContent the SDK, and it does not compile llama.cpp / Sherpa / ONNX / MLX from source. -Pin: `cmake/sdk-pin.cmake`. Prefix: `-DCMAKE_PREFIX_PATH=` or `-DRCLI_SDK_KIT=`. -Pointing `RCLI_SDK_DIR` at SDK **source** is a configure error. +Pin: `cmake/sdk-pin.cmake`. Prefix: `-DCMAKE_PREFIX_PATH=` or `-DWALLY_SDK_KIT=`. +Pointing `WALLY_SDK_DIR` at SDK **source** is a configure error. `BEST_PRACTISES.md` / `BEST_PRACTICES.md` (if present) is a local playbook and must stay gitignored. The rules below are the subset that applies to this CLI. @@ -67,12 +67,24 @@ contract hash. adapter, fixtures, and drift/conformance tests land atomically; CI fails when any one is stale. -Do not wrap protobuf in OpenAPI merely to change protocol names. When RCLI only +Do not wrap protobuf in OpenAPI merely to change protocol names. When Wally only transports SDK-owned bytes, protobuf generation and `SCHEMA_LOCK` satisfy this -rule. When RCLI directly owns an HTTP call, the OpenAPI requirement applies. +rule. When Wally directly owns an HTTP call, the OpenAPI requirement applies. + +The console's six CLI calls (`/auth/cli/{start,poll,refresh,revoke}`, `/v1/me`, +`/v1/cli/usage`) follow this. `contracts/wally-cli-v1.openapi.json` is the pinned +artifact, extracted from InferenceInfra's `control-plane-v1.openapi.json` by +`contracts/extract-cli-contract.py`. `contracts/generate_console_binding.py` +turns it into `src/account/console_contract.h` (typed requests and responses, +DO NOT EDIT), which `console.cpp` uses instead of hand-built JSON. Requests +serialize strictly; responses read tolerantly (a missing field defaults, a wrong +type or unknown enum value still fails) so the CLI survives a server that lags +the contract. `test_wally_contract` and the CI `--check` fail the build if the +header, the pin, and the artifact drift. To re-vendor: run the extractor against +a newer source contract, run the generator, commit all three together. Consistency with the SDK is `idl/SCHEMA_LOCK`, copied into the kit as -`share/runanywhere/SCHEMA_LOCK` and pinned here as `RCLI_PINNED_IDL_SCHEMA_SHA256`. +`share/runanywhere/SCHEMA_LOCK` and pinned here as `WALLY_PINNED_IDL_SCHEMA_SHA256`. Configure fails if the kit's lock does not match (`cmake/RunAnywhereSDK.cmake`). When the schema changes, consume a new kit and bump the pin — never regenerate headers locally. @@ -91,7 +103,7 @@ or a retired MetalRT / hardcoded catalog. ## Signing in to a console -`rcli login` is a device flow, the same shape `gh auth login` uses. The terminal +`wally login` is a device flow, the same shape `gh auth login` uses. The terminal asks for a code, a browser the person already trusts approves it, and the terminal collects a key. No password ever reaches the CLI. @@ -111,16 +123,28 @@ the page a person approves the sign-in on. | | Default | Override | |---|---|---| -| API | `https://inference.runanywhere.ai` | `RCLI_CONSOLE_URL` | -| Approval page | `https://console.runanywhere.ai` | `RCLI_CONSOLE_WEB_URL` | +| API | `https://inference.runanywhere.ai` | `WALLY_CONSOLE_URL` | +| Approval page | `https://console.runanywhere.ai` | `WALLY_CONSOLE_WEB_URL` | Both defaults live together in `src/account/credentials.cpp` so they cannot drift apart, and `TrustedBrowserOrigins()` is what pairs them: with no override it trusts the deployed console, and for any other API origin it trusts only -that origin. Pointing at a local dev console needs `RCLI_CONSOLE_URL` set -explicitly, e.g. `RCLI_CONSOLE_URL=http://localhost:8080`. - -`RCLI_PROFILE_DIR` moves the credential file, which is what lets several +that origin. Pointing at a local dev console needs `WALLY_CONSOLE_URL` set +explicitly, e.g. `WALLY_CONSOLE_URL=http://localhost:8080`. + +The API URL may carry a path, because the deployed development console is one: +`https://inference.runanywhere.ai/api-dev`, where the load balancer strips the +prefix and forwards to the dev control plane. Every endpoint is appended to +whatever is configured, so the prefix follows the whole flow. Its approval page +is on Railway rather than that host, so dev also needs +`WALLY_CONSOLE_WEB_URL=https://runanywhere-frontend-development.up.railway.app`. + +Do not reach for the dev backend's own Cloud Run hostname instead. It answers, +but it is behind the load balancer, so `/v1/chat/completions` lands on the +control plane rather than the gateway and returns 502 — a route no installed +binary can produce, and a day lost to debugging it. + +`WALLY_PROFILE_DIR` moves the credential file, which is what lets several accounts share one machine. The credential is a normal API key with the customer's credit behind it. Treat @@ -129,13 +153,13 @@ never logged. ## Building against the SDK -`RCLI_SDK_KIT` points at a built kit, not at SDK source. `cmake/sdk-pin.cmake` +`WALLY_SDK_KIT` points at a built kit, not at SDK source. `cmake/sdk-pin.cmake` pins the IDL version and its hash; a mismatch is a hard error and the fix is to consume a matching kit or bump the pin, **never to run protoc**. -Two binaries come out of a build. `rcli-cxx` is the CLI. `rcli` is the same -thing plus the MLX backend, and it only builds when `RCLI_SDK_SWIFT_PATH` names -an SDK checkout with the Swift tree. Ship `rcli`. +Two binaries come out of a build. `wally-cxx` is the CLI. `wally` is the same +thing plus the MLX backend, and it only builds when `WALLY_SDK_SWIFT_PATH` names +an SDK checkout with the Swift tree. Ship `wally`. MLX resolves its Metal shaders from `mlx-swift_Cmlx.bundle` beside the executable. Copy the binary somewhere on its own and MLX silently fails to @@ -151,25 +175,25 @@ register, so an install puts both together and points a wrapper at them. ## Tests -Hermetic unit tests (`tests/test_rcli_unit.cpp`): no models, no network, no real +Hermetic unit tests (`tests/test_wally_unit.cpp`): no models, no network, no real keys. `ctest` is the default CI bar. -Smoke / e2e (`scripts/smoke.sh`, `scripts/e2e.sh`) prove the product promise -against a **pinned kit**, not SDK source. `scripts/e2e-modalities.sh` (called +Smoke / e2e (`scripts/test/smoke.sh`, `scripts/test/e2e.sh`) prove the product promise +against a **pinned kit**, not SDK source. `scripts/test/e2e-modalities.sh` (called from `e2e.sh`) runs optional round-trips **by modality** — LLM, STT, TTS, VLM, embed, image, VAD, rerank, segment — and never requires `--engine`. Discover -models via `RCLI_E2E_` (path or catalog id), `RCLI_E2E_MODEL_ROOTS`, or -`RCLI_E2E_AUTO=1`. Legacy `RCLI_E2E_MODEL` / `RCLI_E2E_MLX_MODEL` / -`RCLI_E2E_NEURT_MODEL` / `RCLI_E2E_QHEXRT_MODEL` still map onto those +models via `WALLY_E2E_` (path or catalog id), `WALLY_E2E_MODEL_ROOTS`, or +`WALLY_E2E_AUTO=1`. Legacy `WALLY_E2E_MODEL` / `WALLY_E2E_MLX_MODEL` / +`WALLY_E2E_NEURT_MODEL` / `WALLY_E2E_QHEXRT_MODEL` still map onto those primitives. Public CI leaves every knob unset (skip). There is one CLI named -`rcli`. On Apple, `cmake --build` links the Swift MLX host as `build/rcli` -(llama.cpp + ONNX + Sherpa + MLX). Windows is `build/rcli.exe` (no MLX). -`rcli-cxx` is an Apple compile artifact, not the product. Full MLX model -smoke: `scripts/smoke-mlx.sh`. +`wally`. On Apple, `cmake --build` links the Swift MLX host as `build/wally` +(llama.cpp + ONNX + Sherpa + MLX). Windows is `build/wally.exe` (no MLX). +`wally-cxx` is an Apple compile artifact, not the product. Full MLX model +smoke: `scripts/test/smoke-mlx.sh`. ## CI -Minimum: fetch the pinned kit (`scripts/fetch-kit.sh`) → configure → build → +Minimum: fetch the pinned kit (`scripts/build/fetch-kit.sh`) → configure → build → ctest → smoke. Fail on pin / schema mismatch. `scripts/ci/check-agents-sync.sh` fails the PR when `CLAUDE.md` is not a symlink to `AGENTS.md`, or when `.claude/skills` and `.agents/skills` differ. @@ -183,16 +207,16 @@ cmake -B build -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH=/path/to/cpp-desktop-- cmake --build build -j "$(sysctl -n hw.logicalcpu)" ctest --test-dir build --output-on-failure -# Apple: ./build/rcli Windows: ./build/rcli.exe -bash scripts/e2e.sh ./build/rcli +# Apple: ./build/wally Windows: ./build/wally.exe +bash scripts/test/e2e.sh ./build/wally ``` -On Apple Silicon, `cmake --build` produces `build/rcli` (Swift host wrapping -`rcli_run_main`). Users never run `rcli-cxx`; that name exists only so CMake +On Apple Silicon, `cmake --build` produces `build/wally` (Swift host wrapping +`wally_run_main`). Users never run `wally-cxx`; that name exists only so CMake cannot overwrite the product binary. Independent clones set -`RCLI_SDK_SWIFT_PATH` to a runanywhere-sdks checkout (CI does this). Nested -`EXTERNAL/RCLI` finds `../../Package.swift` automatically. Disable the host -with `-DRCLI_APPLE_MLX_HOST=OFF` only for a C++-only compile loop. +`WALLY_SDK_SWIFT_PATH` to a runanywhere-sdks checkout (CI does this). Nested +`EXTERNAL/Wally` finds `../../Package.swift` automatically. Disable the host +with `-DWALLY_APPLE_MLX_HOST=OFF` only for a C++-only compile loop. NeuRT and QHexRT are optional private packs (`NEURUN_TOKEN`), never required to configure or link the public bottle. QHexRT is not shipped for Windows x64. @@ -205,10 +229,10 @@ Canonical tree: `.claude/skills/` (Claude Code). Mirror: `.agents/skills/` | Skill | When | |---|---| -| `rcli-architecture` | New commands, layering, proto, "where does this logic go?" | -| `rcli-kit-pin` | Bump `cmake/sdk-pin.cmake`, consume a new kit | -| `rcli-e2e` | macOS / Windows verification against a kit | -| `rcli-release` | Cut a product release (independent of SDK version) | +| `wally-architecture` | New commands, layering, proto, "where does this logic go?" | +| `wally-kit-pin` | Bump `cmake/sdk-pin.cmake`, consume a new kit | +| `wally-e2e` | macOS / Windows verification against a kit | +| `wally-release` | Cut a product release (independent of SDK version) | ## Definition of done diff --git a/CMakeLists.txt b/CMakeLists.txt index cfd4481..652fb41 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,22 @@ cmake_minimum_required(VERSION 3.24) -project(rcli VERSION 0.5.2 LANGUAGES C CXX) -# Product version is independent of the SDK kit pin in cmake/sdk-pin.cmake. -# scripts/package-rcli.sh and the release workflow stamp this into Formula/rcli.rb. +# The product version is read from versions.toml, the single source for every +# version and pin in this repo -- never hardcoded here. Parsed before project() +# with a plain regex, since versions.toml is flat `key = "value"` by design. +file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/versions.toml" _wally_version_line + REGEX "^[ \t]*version[ \t]*=") +if(NOT _wally_version_line) + message(FATAL_ERROR "versions.toml is missing the product version") +endif() +list(GET _wally_version_line 0 _wally_version_line) +string(REGEX REPLACE "^[^\"]*\"([^\"]*)\".*$" "\\1" _wally_version "${_wally_version_line}") + +project(wally VERSION "${_wally_version}" LANGUAGES C CXX) + +# The SDK kit pin (cmake/sdk-pin.cmake, also from versions.toml) moves on its +# own cadence. scripts/build/package-wally.sh and the release workflow stamp the +# product version into Formula/wally.rb; scripts/ci/check-versions.py fails a build +# where the formula or the Swift package pin has drifted from versions.toml. set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -15,15 +29,17 @@ endif() list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") include(RunAnywhereSDK) -# In-tree rcli sources (copied from runanywhere-sdks/rcli). Command files are +# In-tree wally sources (copied from runanywhere-sdks/wally). Command files are # thin: parse → bootstrap() → one rac_* call → render. -set(RCLI_SOURCES +set(WALLY_SOURCES src/app.cpp + src/cli_formatter.cpp src/bootstrap.cpp src/catalog/catalog.cpp src/catalog/model_ref.cpp src/commands/cmd_version.cpp src/commands/cmd_info.cpp + src/commands/cmd_about.cpp src/commands/cmd_auth.cpp src/commands/cmd_backends.cpp src/commands/cmd_list.cpp @@ -66,6 +82,7 @@ set(RCLI_SOURCES src/config/cli_paths.cpp src/device_info.cpp src/net/control_plane.cpp + src/net/loopback_auth.cpp src/io/wav_io.cpp src/io/image_io.cpp src/io/output.cpp @@ -74,23 +91,23 @@ set(RCLI_SOURCES src/util/term.cpp ) -add_library(rcli_core STATIC ${RCLI_SOURCES}) +add_library(wally_core STATIC ${WALLY_SOURCES}) if(WIN32) - target_compile_definitions(rcli_core PUBLIC RCLI_NO_LINENOISE=1) + target_compile_definitions(wally_core PUBLIC WALLY_NO_LINENOISE=1) if(MSVC) - target_compile_options(rcli_core PUBLIC + target_compile_options(wally_core PUBLIC "/FI${CMAKE_CURRENT_SOURCE_DIR}/src/windows_proto_compat.h") else() - target_compile_options(rcli_core PUBLIC + target_compile_options(wally_core PUBLIC -include "${CMAKE_CURRENT_SOURCE_DIR}/src/windows_proto_compat.h") endif() else() - target_sources(rcli_core PRIVATE third_party/linenoise/linenoise.c) + target_sources(wally_core PRIVATE third_party/linenoise/linenoise.c) set_source_files_properties(third_party/linenoise/linenoise.c PROPERTIES COMPILE_OPTIONS "-w") endif() -target_include_directories(rcli_core PUBLIC +target_include_directories(wally_core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" "${CMAKE_CURRENT_SOURCE_DIR}/src" "${CMAKE_CURRENT_SOURCE_DIR}/third_party" @@ -98,6 +115,26 @@ target_include_directories(rcli_core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/third_party/linenoise" ) +# Dev endpoint bake. The values come from the ENVIRONMENT of the configure +# step, never from -D flags: nothing endpoint-shaped lands on the command +# line, in CMakeCache.txt, or in this log (only ON/off is printed). The +# generated header lives in the build tree, which is gitignored — no endpoint +# ever enters committed source. Empty env (the normal case) generates empty +# macros and the production resolution order is untouched. +set(WALLY_BAKED_CONSOLE_API_URL "$ENV{WALLY_BAKED_CONSOLE_API_URL}") +set(WALLY_BAKED_CONSOLE_WEB_ORIGIN "$ENV{WALLY_BAKED_CONSOLE_WEB_ORIGIN}") +if(WALLY_BAKED_CONSOLE_API_URL) + message(STATUS "wally: dev endpoint bake ON (values withheld from log)") +else() + message(STATUS "wally: dev endpoint bake off (production defaults)") +endif() +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/src/account/baked_endpoints.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/generated/account/baked_endpoints.h" + @ONLY +) +target_include_directories(wally_core PUBLIC "${CMAKE_CURRENT_BINARY_DIR}/generated") + # The Anthropic translator and the JetBrains proxy need an HTTP client and a # JSON reader. Both used to arrive from the SDK's own source build; a kit # consumer never configures that build, so they are fetched here. @@ -120,31 +157,31 @@ FetchContent_Declare(nlohmann_json FetchContent_MakeAvailable(cpp_httplib nlohmann_json) find_package(Threads REQUIRED) -target_link_libraries(rcli_core PUBLIC rac_commons Threads::Threads) -target_link_libraries(rcli_core PRIVATE httplib::httplib nlohmann_json::nlohmann_json) +target_link_libraries(wally_core PUBLIC rac_commons Threads::Threads) +target_link_libraries(wally_core PRIVATE httplib::httplib nlohmann_json::nlohmann_json) if(WIN32) # The console client uses the Windows system HTTP stack so source builds and - # release archives do not need curl/OpenSSL DLLs beside rcli.exe. - target_link_libraries(rcli_core PRIVATE winhttp) + # release archives do not need curl/OpenSSL DLLs beside wally.exe. + target_link_libraries(wally_core PRIVATE winhttp) else() find_package(CURL REQUIRED) - target_link_libraries(rcli_core PRIVATE CURL::libcurl) + target_link_libraries(wally_core PRIVATE CURL::libcurl) endif() -target_compile_definitions(rcli_core PUBLIC - RCLI_VERSION="${PROJECT_VERSION}" - RCLI_PINNED_SDK_VERSION="${RCLI_PINNED_SDK_VERSION}" +target_compile_definitions(wally_core PUBLIC + WALLY_VERSION="${PROJECT_VERSION}" + WALLY_PINNED_SDK_VERSION="${WALLY_PINNED_SDK_VERSION}" RAC_HAVE_PROTOBUF=1 CLI11_HAS_CODECVT=0) -rcli_define_engine_macros(rcli_core) +wally_define_engine_macros(wally_core) if(RunAnywhere_HAS_RAG) - target_compile_definitions(rcli_core PUBLIC RAC_HAVE_RAG=1) + target_compile_definitions(wally_core PUBLIC RAC_HAVE_RAG=1) endif() if(RunAnywhere_HAS_SERVER) - target_compile_definitions(rcli_core PUBLIC RCLI_HAS_SERVER=1) + target_compile_definitions(wally_core PUBLIC WALLY_HAS_SERVER=1) endif() # Proto types are the SOT. The kit ships the *.pb.h that commons was compiled -# with (same protoc, same isolated runtime). RCLI never runs protoc and never +# with (same protoc, same isolated runtime). WALLY never runs protoc and never # compiles *.pb.cc — those objects already live in librac_commons.a. if(NOT RunAnywhere_PROTO_INCLUDE_DIR OR NOT EXISTS "${RunAnywhere_PROTO_INCLUDE_DIR}/model_types.pb.h" @@ -158,57 +195,69 @@ endif() if(APPLE) # SecItem, for the credential a JetBrains IDE reads its provider key from. - target_link_libraries(rcli_core PRIVATE "-framework Security" "-framework CoreFoundation") - target_link_libraries(rcli_core PUBLIC "-framework IOKit" "-framework CoreFoundation") + target_link_libraries(wally_core PRIVATE "-framework Security" "-framework CoreFoundation") + target_link_libraries(wally_core PUBLIC "-framework IOKit" "-framework CoreFoundation") endif() if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") - target_compile_options(rcli_core PRIVATE -Wall -Wextra) + target_compile_options(wally_core PRIVATE -Wall -Wextra) endif() -add_executable(rcli src/main.cpp) -target_link_libraries(rcli PRIVATE rcli_core) +add_executable(wally src/main.cpp) +target_link_libraries(wally PRIVATE wally_core) if(MINGW) - target_link_options(rcli PRIVATE -static-libgcc -static-libstdc++ -static) + target_link_options(wally PRIVATE -static-libgcc -static-libstdc++ -static) endif() +# On Apple the shipped product is the Swift MLX host at build/wally (see the +# wally-mlx target below); the pure-C++ build of this target is an intermediate +# the Swift host links, not a product. Name it wally-cxx so `cmake --build` +# cannot clobber the fat Apple `wally`, and by default keep it out of the build +# root so a normal build surfaces only `wally`. Set WALLY_APPLE_CXX_BINARY=ON to +# get the standalone C++ binary at build/wally-cxx for a fast C++-only loop. +option(WALLY_APPLE_MLX_HOST + "On Apple, link the Swift MLX host as build/wally (the product CLI)" ON) +option(WALLY_APPLE_CXX_BINARY + "On Apple, emit the standalone C++ wally-cxx binary in the build root" OFF) + +set(_wally_runtime_dir "${CMAKE_BINARY_DIR}") if(APPLE) - # Shipped `rcli` is scripts/build-mlx.sh + Swift MLX host. This binary is - # rcli-cxx so cmake --build cannot clobber the fat Apple binary. - set_target_properties(rcli PROPERTIES OUTPUT_NAME "rcli-cxx") + set_target_properties(wally PROPERTIES OUTPUT_NAME "wally-cxx") + if(WALLY_APPLE_MLX_HOST AND NOT WALLY_APPLE_CXX_BINARY) + set(_wally_runtime_dir "${CMAKE_BINARY_DIR}/cxx-host") + endif() endif() -set_target_properties(rcli PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}") +set_target_properties(wally PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${_wally_runtime_dir}") if(APPLE) - set_target_properties(rcli PROPERTIES + set_target_properties(wally PROPERTIES BUILD_RPATH "${RunAnywhere_THIRD_PARTY_DIR}" INSTALL_RPATH "@loader_path/../lib") elseif(UNIX) - set_target_properties(rcli PROPERTIES + set_target_properties(wally PROPERTIES BUILD_RPATH "${RunAnywhere_THIRD_PARTY_DIR}" INSTALL_RPATH "\$ORIGIN/../lib") endif() -rcli_stage_windows_runtime_dlls(rcli) - -install(TARGETS rcli RUNTIME DESTINATION bin) - -# Apple product binary is the Swift MLX host (`build/rcli`), not `rcli-cxx`. -# Users still type `rcli`; llama.cpp / ONNX / Sherpa register from C++ bootstrap -# and MLX from the Swift entry. Turn off only for a C++-only compile loop. -option(RCLI_APPLE_MLX_HOST - "On Apple, link the Swift MLX host as build/rcli (the product CLI)" ON) -if(APPLE AND RCLI_APPLE_MLX_HOST) - if(NOT RCLI_SDK_ROOT) - message(FATAL_ERROR "RCLI_SDK_ROOT is empty; cannot build the MLX host") +wally_stage_windows_runtime_dlls(wally) + +install(TARGETS wally RUNTIME DESTINATION bin) + +# Apple product binary is the Swift MLX host (`build/wally`), not `wally-cxx`. +# Users still type `wally`; llama.cpp / ONNX / Sherpa register from C++ bootstrap +# and MLX from the Swift entry. Turn WALLY_APPLE_MLX_HOST (declared above) off +# only for a C++-only compile loop. +if(APPLE AND WALLY_APPLE_MLX_HOST) + if(NOT WALLY_SDK_ROOT) + message(FATAL_ERROR "WALLY_SDK_ROOT is empty; cannot build the MLX host") endif() - add_custom_target(rcli-mlx ALL + add_custom_target(wally-mlx ALL COMMAND "${CMAKE_COMMAND}" -E env - "RCLI_SDK_KIT=${RCLI_SDK_ROOT}" - bash "${CMAKE_SOURCE_DIR}/scripts/build-mlx.sh" "${CMAKE_BINARY_DIR}" - DEPENDS rcli + "WALLY_SDK_KIT=${WALLY_SDK_ROOT}" + bash "${CMAKE_SOURCE_DIR}/scripts/build/build-mlx.sh" "${CMAKE_BINARY_DIR}" + DEPENDS wally WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" - COMMENT "Swift MLX host → ${CMAKE_BINARY_DIR}/rcli" + COMMENT "Swift MLX host → ${CMAKE_BINARY_DIR}/wally" VERBATIM) endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 352aa3d..ef51e3c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to rcli +# Contributing to wally -`rcli` is the desktop CLI for RunAnywhere. Inference, catalog, download, and +`wally` is the desktop CLI for RunAnywhere. Inference, catalog, download, and lifecycle live in the **C++ desktop kit** (`find_package(RunAnywhere)`). This repo is argv, terminal I/O, and the Apple MLX host. @@ -11,8 +11,8 @@ fix belongs in the SDK, then a new kit — not a workaround here. ## Prerequisites - CMake 3.24+, a C++20 compiler -- A staged kit matching `cmake/sdk-pin.cmake` (`RCLI_PINNED_SDK_VERSION`) -- Xcode 26+ only if you are linking the shipping Apple binary (`scripts/build-mlx.sh`) +- A staged kit matching `cmake/sdk-pin.cmake` (`WALLY_PINNED_SDK_VERSION`) +- Xcode 26+ only if you are linking the shipping Apple binary (`scripts/build/build-mlx.sh`) Build a kit from a runanywhere-sdks checkout: @@ -30,25 +30,25 @@ The prefix is `dist/cpp-desktop-macos-arm64` in that checkout. cmake -B build -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH=/path/to/cpp-desktop-macos-arm64 cmake --build build -j "$(sysctl -n hw.logicalcpu)" -./build/rcli version -./build/rcli backends +./build/wally version +./build/wally backends ``` -On Apple Silicon that `rcli` is the Swift MLX host (llama.cpp, ONNX, Sherpa, -and MLX in one process). `rcli-cxx` is only a CMake intermediate. Independent -clones need `RCLI_SDK_SWIFT_PATH` pointing at a runanywhere-sdks tree. +On Apple Silicon that `wally` is the Swift MLX host (llama.cpp, ONNX, Sherpa, +and MLX in one process). `wally-cxx` is only a CMake intermediate. Independent +clones need `WALLY_SDK_SWIFT_PATH` pointing at a runanywhere-sdks tree. -`RCLI_SDK_DIR` / `RCLI_SDK_KIT` is an alias for that **kit prefix**. Pointing it -at a source tree is a configure error. C++-only: `-DRCLI_APPLE_MLX_HOST=OFF`. +`WALLY_SDK_DIR` / `WALLY_SDK_KIT` is an alias for that **kit prefix**. Pointing it +at a source tree is a configure error. C++-only: `-DWALLY_APPLE_MLX_HOST=OFF`. ## Tests ```bash -cmake --build build --target test_rcli_unit -ctest --test-dir build -R rcli --output-on-failure -bash scripts/e2e.sh ./build/rcli +cmake --build build --target test_wally_unit +ctest --test-dir build -R wally --output-on-failure +bash scripts/test/e2e.sh ./build/wally # Device / overlay: primitives, not engines -# bash scripts/e2e-modalities.sh ./build/rcli +# bash scripts/test/e2e-modalities.sh ./build/wally ``` ## Layout @@ -57,10 +57,10 @@ bash scripts/e2e.sh ./build/rcli src/commands/ one file per command; parse → bootstrap() → one rac_* call src/catalog/ thin helpers over commons catalog APIs src/repl/ linenoise REPL -include/ rcli_run_main for the Swift host +include/ wally_run_main for the Swift host cmake/sdk-pin.cmake EXACT kit version (+ sha256 once kits are published) third_party/ CLI11 + linenoise, vendored -swift/ Apple entry: register MLX, then rcli_run_main +swift/ Apple entry: register MLX, then wally_run_main ``` ## Adding a command @@ -73,4 +73,4 @@ swift/ Apple entry: register MLX, then rcli_run_main ## Pull requests Branch off `main`. `cmake --build` must stay green. On Apple that includes the -MLX host (`build/rcli`). Do not add a second CLI binary. +MLX host (`build/wally`). Do not add a second CLI binary. diff --git a/Formula/rcli-beta.rb b/Formula/rcli-beta.rb deleted file mode 100644 index c7b637b..0000000 --- a/Formula/rcli-beta.rb +++ /dev/null @@ -1,43 +0,0 @@ -class RcliBeta < Formula - desc "Run language, speech and image models on your own machine (beta)" - homepage "https://github.com/RunanywhereAI/RCLI" - url "https://github.com/RunanywhereAI/RCLI/releases/download/v0.4.0-beta.1/rcli-0.4.0-beta.1-Darwin-arm64.tar.gz" - sha256 "0000000000000000000000000000000000000000000000000000000000000000" - license "MIT" - version "0.4.0-beta.1" - - depends_on :macos - depends_on arch: :arm64 - - # Installed as rcli-beta so it can sit alongside a stable rcli. Each formula - # gets its own libexec, so the two Metal bundles do not collide. - def install - libexec.install "libexec/rcli", "libexec/mlx-swift_Cmlx.bundle" - bin.install_symlink libexec/"rcli" => "rcli-beta" - end - - def caveats - <<~EOS - This is a prerelease. It installs as rcli-beta and leaves a stable rcli - alone, so both can be on the machine at once. - - Models are downloaded on demand and kept in - ~/.local/share/runanywhere - - Getting started: - rcli-beta list --all every model in the catalog - rcli-beta pull qwen3-0.6b download one - rcli-beta run qwen3-0.6b talk to it, /? for commands - - To remove: - brew uninstall rcli-beta - EOS - end - - test do - assert_match "rcli", shell_output("#{bin}/rcli-beta --version") - # Proves the Metal bundle survived the install: without it MLX drops out - # silently and the other five engines carry on. - assert_match "mlx", shell_output("#{bin}/rcli-beta backends") - end -end diff --git a/Formula/rcli.rb b/Formula/rcli.rb deleted file mode 100644 index 42f6765..0000000 --- a/Formula/rcli.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -class Rcli < Formula - desc "Run language, speech and image models on your own machine" - homepage "https://github.com/RunanywhereAI/RCLI" - version "0.5.2" - license "MIT" - - # macOS arm64 ships the Swift MLX host - # (llama.cpp + ONNX + Sherpa + MLX). Linux is not in this cut. - on_macos do - on_arm do - url "https://github.com/RunanywhereAI/RCLI/releases/download/v0.5.2/rcli-0.5.2-macos-arm64.tar.gz" - sha256 "fc16e5e3fdef1e62b7d05d569ce8a69cc86cdd2b880d5983c495a5e240384430" - end - end - - def install - bin.install "bin/rcli" - bin.install Dir["bin/*.bundle"] if Dir["bin/*.bundle"].any? - lib.install Dir["lib/*"] if Dir["lib/*"].any? - end - - def caveats - <<~EOS - Models are downloaded on demand and kept in - ~/.local/share/runanywhere - - Getting started: - rcli list downloaded models - rcli pull qwen3-0.6b download one - rcli run qwen3-0.6b talk to it - rcli run qwen3-0.6b "hi" ask once and exit - - Also: - rcli tts "hello" speak text - rcli stt recording.wav transcribe audio - rcli backends which engines this build linked - EOS - end - - test do - assert_match "rcli", shell_output("#{bin}/rcli version") - backends = shell_output("#{bin}/rcli backends") - assert_match(/llama/i, backends) - assert_match(/onnx/i, backends) - assert_match(/sherpa/i, backends) - assert_match(/mlx/i, backends) if OS.mac? && Hardware::CPU.arm? - end -end diff --git a/Formula/wally-beta.rb b/Formula/wally-beta.rb new file mode 100644 index 0000000..55d0b8d --- /dev/null +++ b/Formula/wally-beta.rb @@ -0,0 +1,43 @@ +class WallyBeta < Formula + desc "Run language, speech and image models on your own machine (beta)" + homepage "https://github.com/RunanywhereAI/wally" + url "https://github.com/RunanywhereAI/wally/releases/download/v0.4.0-beta.1/wally-0.4.0-beta.1-Darwin-arm64.tar.gz" + sha256 "0000000000000000000000000000000000000000000000000000000000000000" + license "MIT" + version "0.4.0-beta.1" + + depends_on :macos + depends_on arch: :arm64 + + # Installed as wally-beta so it can sit alongside a stable wally. Each + # formula gets its own libexec, so the two Metal bundles do not collide. + def install + libexec.install "libexec/wally", "libexec/mlx-swift_Cmlx.bundle" + bin.install_symlink libexec/"wally" => "wally-beta" + end + + def caveats + <<~EOS + This is a prerelease. It installs as wally-beta and leaves a stable wally + alone, so both can be on the machine at once. + + Models are downloaded on demand and kept in + ~/.local/share/runanywhere + + Getting started: + wally-beta list --all every model in the catalog + wally-beta pull qwen3-0.6b download one + wally-beta run qwen3-0.6b talk to it, /? for commands + + To remove: + brew uninstall wally-beta + EOS + end + + test do + assert_match "wally", shell_output("#{bin}/wally-beta --version") + # Proves the Metal bundle survived the install: without it MLX drops out + # silently and the other five engines carry on. + assert_match "mlx", shell_output("#{bin}/wally-beta backends") + end +end diff --git a/Formula/wally.rb b/Formula/wally.rb new file mode 100644 index 0000000..86e24b8 --- /dev/null +++ b/Formula/wally.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class Wally < Formula + desc "Run language, speech and image models on your own machine" + homepage "https://github.com/RunanywhereAI/wally" + version "0.5.3" + license "MIT" + + # macOS arm64 ships the Swift MLX host + # (llama.cpp + ONNX + Sherpa + MLX). Linux is not in this cut. + on_macos do + on_arm do + url "https://github.com/RunanywhereAI/wally/releases/download/v0.5.3/wally-0.5.3-macos-arm64.tar.gz" + # Placeholder -- no v0.5.3 release has published a wally-named asset + # yet. scripts/release/update-tap.sh re-stamps this from the real release + # checksum; a stale value here fails brew install's own hash check + # rather than installing something unverified. + sha256 "0000000000000000000000000000000000000000000000000000000000000" + end + end + + def install + bin.install "bin/wally" + bin.install Dir["bin/*.bundle"] if Dir["bin/*.bundle"].any? + lib.install Dir["lib/*"] if Dir["lib/*"].any? + end + + def caveats + <<~EOS + Models are downloaded on demand and kept in + ~/.local/share/runanywhere + + Getting started: + wally list downloaded models + wally pull qwen3-0.6b download one + wally run qwen3-0.6b talk to it + wally run qwen3-0.6b "hi" ask once and exit + + Also: + wally tts "hello" speak text + wally stt recording.wav transcribe audio + wally backends which engines this build linked + EOS + end + + test do + assert_match "wally", shell_output("#{bin}/wally version") + backends = shell_output("#{bin}/wally backends") + assert_match(/llama/i, backends) + assert_match(/onnx/i, backends) + assert_match(/sherpa/i, backends) + assert_match(/mlx/i, backends) if OS.mac? && Hardware::CPU.arm? + end +end diff --git a/README.md b/README.md index e820044..ccb581c 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,23 @@ -# RunAnywhere CLI +
-Run open models on your machine. +Wally, the RunAnywhere mascot + +# Wally + +**Run open models on your own machine, or hosted when the job outgrows it.** + +Chat, vision, speech and embeddings, all from the terminal. Local models never +leave the device; hosted ones go through a console you sign in to and are +metered against your own credit. + +
```bash -rcli pull qwen3 -rcli run qwen3 +wally pull qwen3 # download a model +wally run qwen3 # chat with it, offline ``` -Chat, vision, speech, and embeddings — all local. Nothing leaves the device. +That is the whole first run. No account, no key, nothing leaves the machine. ## Signing in @@ -15,9 +25,10 @@ Models you have pulled run on this machine and need no account. To use a hosted model instead, sign in to a RunAnywhere console: ```bash -rcli login # opens a browser; approve it there -rcli whoami # who you are, and what you have used this month -rcli logout +wally login # opens a browser; approve it there +wally whoami # the account you are signed in as +wally usage # credit, and what you have used this month +wally logout ``` The terminal never asks for a password. It shows a code, you approve it in the @@ -27,27 +38,27 @@ on the console's Cloud keys page and can be revoked there at any time. Against a console running on your own machine: ```bash -export RCLI_CONSOLE_URL=http://localhost:8002 -rcli login +export WALLY_CONSOLE_URL=http://localhost:8002 +wally login ``` Then hand a hosted model to a coding session: ```bash -rcli opencode -m gemma-4 +wally opencode -m gemma-4 ``` For the Open Frontier hosted path, make the choice explicit and pass any OpenCode arguments after `--`: ```bash -rcli opencode --cloud --model -- --agent build +wally opencode --cloud --model -- --agent build ``` -`--cloud` never falls back to a local model. The existing `rcli opencode -m` -form remains available for the parent PR's local-or-upstream harness flow. +`--cloud` never falls back to a local model. `wally opencode -m ` runs +whichever model you name, local or hosted. -If the model is on this machine, rcli serves it locally. If it is not, the +If the model is on this machine, wally serves it locally. If it is not, the request goes to the console you are signed in to, is checked against your balance before it runs, and is metered. @@ -56,13 +67,13 @@ balance before it runs, and is metered. ### macOS (Apple Silicon) ```bash -brew install runanywhereai/rcli/rcli +brew install runanywhereai/wally/wally ``` or ```bash -curl -fsSL https://raw.githubusercontent.com/RunanywhereAI/RCLI/main/install.sh | sh +curl -fsSL https://raw.githubusercontent.com/RunanywhereAI/wally/main/install.sh | sh ``` ### From source @@ -70,22 +81,22 @@ curl -fsSL https://raw.githubusercontent.com/RunanywhereAI/RCLI/main/install.sh Needs a built SDK kit, not SDK source: ```bash -cmake -B build -DRCLI_SDK_KIT=/dist/cpp-desktop-macos-arm64 -export RCLI_SDK_SWIFT_PATH= # for the MLX backend on Apple +cmake -B build -DWALLY_SDK_KIT=/dist/cpp-desktop-macos-arm64 +export WALLY_SDK_SWIFT_PATH= # for the MLX backend on Apple cmake --build build -j8 ``` -`build/rcli` is the full binary. `build/rcli-cxx` is the same CLI without MLX, -and is what you get if `RCLI_SDK_SWIFT_PATH` is unset. +`build/wally` is the full binary. `build/wally-cxx` is the same CLI without MLX, +and is what you get if `WALLY_SDK_SWIFT_PATH` is unset. MLX loads its Metal shaders from `mlx-swift_Cmlx.bundle` next to the executable, so install the pair together: ```bash -mkdir -p ~/.local/lib/rcli -cp -R build/mlx-swift_Cmlx.bundle build/rcli ~/.local/lib/rcli/ -printf '#!/bin/sh\nexec "$HOME/.local/lib/rcli/rcli" "$@"\n' > ~/.local/bin/rcli -chmod +x ~/.local/bin/rcli +mkdir -p ~/.local/lib/wally +cp -R build/mlx-swift_Cmlx.bundle build/wally ~/.local/lib/wally/ +printf '#!/bin/sh\nexec "$HOME/.local/lib/wally/wally" "$@"\n' > ~/.local/bin/wally +chmod +x ~/.local/bin/wally ``` Copy the binary on its own and MLX will not register. @@ -93,7 +104,7 @@ Copy the binary on its own and MLX will not register. ### Windows (x64) ```powershell -irm https://raw.githubusercontent.com/RunanywhereAI/RCLI/main/install.ps1 | iex +irm https://raw.githubusercontent.com/RunanywhereAI/wally/main/install.ps1 | iex ``` ### Linux (x86_64) @@ -105,28 +116,28 @@ was installed. ## Get started ```bash -rcli pull qwen3 # download -rcli run qwen3 # chat -rcli run qwen3 "Hello" # one-shot -rcli serve qwen3 # OpenAI-compatible API on :8080 (macOS/Linux) +wally pull qwen3 # download +wally run qwen3 # chat +wally run qwen3 "Hello" # one-shot +wally serve qwen3 # OpenAI-compatible API on :8080 (macOS/Linux) ``` -`rcli models list --all` is the full catalog. Short names work everywhere (`qwen3`, `llama3.2`, `whisper-tiny`, `piper`, …). Any Hugging Face GGUF works too: +`wally models list --all` is the full catalog. Short names work everywhere (`qwen3`, `llama3.2`, `whisper-tiny`, `piper`, …). Any Hugging Face GGUF works too: ```bash -rcli pull hf.co/Qwen/Qwen3-0.6B-GGUF/Qwen3-0.6B-Q8_0.gguf +wally pull hf.co/Qwen/Qwen3-0.6B-GGUF/Qwen3-0.6B-Q8_0.gguf ``` ## Backends -One `rcli` binary. **Catalog models already name their engine** (GGUF → llama.cpp, `mlx-*` → MLX, Core ML → NeuRT, QNN-context → QHexRT). You normally do not pick one. +One `wally` binary. **Catalog models already name their engine** (GGUF → llama.cpp, `mlx-*` → MLX, Core ML → NeuRT, QNN-context → QHexRT). You normally do not pick one. Override only when you mean it: ```bash -rcli llm generate --engine mlx -m mlx-qwen3 "Hello" -rcli run --engine qhexrt /path/to/lfm2_5_230m_HNPU "Hello" -rcli image generate --engine neurt --prompt "a red cube" --out out.png +wally llm generate --engine mlx -m mlx-qwen3 "Hello" +wally run --engine qhexrt /path/to/lfm2_5_230m_HNPU "Hello" +wally image generate --engine neurt --prompt "a red cube" --out out.png ``` `--engine` accepts `mlx`, `llamacpp`, `sherpa`, `onnx`, `neurt` / `coreml` / `ane`, and `qhexrt` / `qnn` / `npu` / `hexagon`. If you omit it, commons picks the highest-priority **registered** backend that implements that primitive: @@ -140,14 +151,14 @@ rcli image generate --engine neurt --prompt "a red cube" --out out.png | 90 | Sherpa-ONNX | STT / TTS / VAD | | 50 | ONNX Runtime | embeddings / VAD / diarization / segmentation | -`rcli backends` is the source of truth for **this** binary. Public bottles never list `neurt` or `qhexrt`. Those engines are private overlays, never Homebrew / GitHub Release assets. +`wally backends` is the source of truth for **this** binary. Public bottles never list `neurt` or `qhexrt`. Those engines are private overlays, never Homebrew / GitHub Release assets. ### Where each engine exists | Backend | macOS Apple Silicon | Windows x64 | Windows ARM64 | Linux x64 | |---|---|---|---|---| | [llama.cpp](https://github.com/ggml-org/llama.cpp) | public bottle | public bottle | — | source build only | -| [MLX](https://github.com/ml-explore/mlx) (Apple GPU) | public bottle (product `rcli`, not `rcli-cxx`) | — | — | — | +| [MLX](https://github.com/ml-explore/mlx) (Apple GPU) | public bottle (product `wally`, not `wally-cxx`) | — | — | — | | [Sherpa-ONNX](https://github.com/k2-fsa/sherpa-onnx) | public bottle | public bottle | — | source build only | | [ONNX Runtime](https://onnxruntime.ai) | public bottle | public bottle | — | source build only | | NeuRT (Apple Neural Engine; Core ML is the format) | **overlay** rebuild | — | — | — | @@ -157,20 +168,20 @@ Public Windows ARM64 kits are commons-only (no llama.cpp / ONNX / Sherpa on MSVC ### Modalities × engines -Yes = this engine implements the primitive. Try = a catalog id that `rcli pull` / a local path can run. Overlay engines still need the matching **on-disk bundle** (compiled `.mlmodelc` tree, or `*_HNPU` / `v81/` QNN-context dir) — a Hugging Face *repo page* is HTML, not a model. +Yes = this engine implements the primitive. Try = a catalog id that `wally pull` / a local path can run. Overlay engines still need the matching **on-disk bundle** (compiled `.mlmodelc` tree, or `*_HNPU` / `v81/` QNN-context dir) — a Hugging Face *repo page* is HTML, not a model. | Modality | Command | llama.cpp | MLX | Sherpa | ONNX | NeuRT | QHexRT | |---|---|---|---|---|---|---|---| -| LLM | `rcli run` / `llm generate` | yes · `smollm2`, `qwen3` | yes · `mlx-qwen3` | — | — | yes · `lfm2-230m-ane` local Core ML tree | yes · `lfm2-230m-npu` local `*_HNPU` | -| VLM | `rcli vlm generate --image` | yes · `smolvlm2` | yes · `mlx-qwen2-vl` | — | — | — | yes · `internvl-1b-npu` local HNPU | -| TTS | `rcli tts synthesize -o out.wav` | — | yes · `mlx-soprano` | yes · `piper` | — | — | yes · `kitten-micro-npu` local HNPU | -| STT | `rcli stt transcribe audio.wav` | — | yes · `mlx-qwen3-asr` | yes · `whisper-tiny` | — | yes · `parakeet-tdt-v2-ane` local Core ML | yes · `whisper-base-npu` local HNPU | -| VAD | `rcli vad detect audio.wav` | — | — | yes | yes · `silero` | — | — | -| Embeddings | `rcli embed` | yes · `nemotron-3-embed` | yes · `mlx-qwen3-embed` | — | yes · `minilm` | — | yes · `embeddinggemma-npu` local HNPU | -| Rerank | `rcli rerank -d …` | yes · `bge-reranker` | — | — | — | — | yes · `nv-rerank-npu` local HNPU | -| Segmentation | `rcli segment image.ppm` (binary P6 PPM) | — | — | — | yes · `segformer` | — | — | -| Diarization | `rcli diarize audio.wav` | — | — | — | yes · `sortformer` | — | — | -| Image gen | `rcli image generate --prompt … --out …` | — | — | — | — | yes · `sd15` (compiled Core ML zip, not the HF repo HTML) | yes · `cosmos3-diffusion-npu` local HNPU | +| LLM | `wally run` / `llm generate` | yes · `smollm2`, `qwen3` | yes · `mlx-qwen3` | — | — | yes · `lfm2-230m-ane` local Core ML tree | yes · `lfm2-230m-npu` local `*_HNPU` | +| VLM | `wally vlm generate --image` | yes · `smolvlm2` | yes · `mlx-qwen2-vl` | — | — | — | yes · `internvl-1b-npu` local HNPU | +| TTS | `wally tts synthesize -o out.wav` | — | yes · `mlx-soprano` | yes · `piper` | — | — | yes · `kitten-micro-npu` local HNPU | +| STT | `wally stt transcribe audio.wav` | — | yes · `mlx-qwen3-asr` | yes · `whisper-tiny` | — | yes · `parakeet-tdt-v2-ane` local Core ML | yes · `whisper-base-npu` local HNPU | +| VAD | `wally vad detect audio.wav` | — | — | yes | yes · `silero` | — | — | +| Embeddings | `wally embed` | yes · `nemotron-3-embed` | yes · `mlx-qwen3-embed` | — | yes · `minilm` | — | yes · `embeddinggemma-npu` local HNPU | +| Rerank | `wally rerank -d …` | yes · `bge-reranker` | — | — | — | — | yes · `nv-rerank-npu` local HNPU | +| Segmentation | `wally segment image.ppm` (binary P6 PPM) | — | — | — | yes · `segformer` | — | — | +| Diarization | `wally diarize audio.wav` | — | — | — | yes · `sortformer` | — | — | +| Image gen | `wally image generate --prompt … --out …` | — | — | — | — | yes · `sd15` (compiled Core ML zip, not the HF repo HTML) | yes · `cosmos3-diffusion-npu` local HNPU | MLX registers with a one-line `-811` then Swift callbacks install it — that warning is expected. `image generate` is compiled only when NeuRT is linked; `--prompt` and `--out` are required (not a positional prompt). `--steps 4` is enough for a smoke PNG. @@ -207,7 +218,7 @@ Catalog models are grouped by the org that trains them. GGUF rows run on llama.c | NVIDIA | Nemotron Omni | `nemotron-omni` | ```bash -rcli vlm generate --model smolvlm2 --image photo.png "What is in this picture?" +wally vlm generate --model smolvlm2 --image photo.png "What is in this picture?" ``` ### Speech @@ -223,8 +234,8 @@ rcli vlm generate --model smolvlm2 --image photo.png "What is in this picture?" | [Silero](https://github.com/snakers4/silero-vad) | Silero | VAD | `silero` | ```bash -rcli tts synthesize "Hello from the device." -o hello.wav -rcli stt transcribe hello.wav +wally tts synthesize "Hello from the device." -o hello.wav +wally stt transcribe hello.wav ``` ### Embeddings, rerank, other @@ -241,25 +252,25 @@ rcli stt transcribe hello.wav ## macOS vs Windows -**macOS Apple Silicon** (public bottle): llama.cpp + MLX + Sherpa + ONNX. Pull `qwen3` (GGUF) or `mlx-qwen3` (GPU). Image generation is NeuRT (`sd15`) and only works after the private overlay is linked into product `rcli`. +**macOS Apple Silicon** (public bottle): llama.cpp + MLX + Sherpa + ONNX. Pull `qwen3` (GGUF) or `mlx-qwen3` (GPU). Image generation is NeuRT (`sd15`) and only works after the private overlay is linked into product `wally`. **Windows x64** (public zip): GGUF / ONNX / Sherpa. No MLX, no NeuRT, no QHexRT. **Windows ARM64** (Snapdragon): public kit has no llama.cpp/ONNX/Sherpa. The QHexRT overlay runs Hexagon NPU models from a local `*_HNPU` tree. Do not expect `mlx-*`, GGUF, or `sd15` on that binary. -`rcli serve` is macOS and Linux. +`wally serve` is macOS and Linux. -Device round-trips are **by modality**, not by engine. `scripts/e2e.sh` always -runs `scripts/e2e-modalities.sh`; public CI leaves the knobs unset and skips. +Device round-trips are **by modality**, not by engine. `scripts/test/e2e.sh` always +runs `scripts/test/e2e-modalities.sh`; public CI leaves the knobs unset and skips. On a machine that already has models: ```bash export RUNANYWHERE_HOME=/path/to/home # already-pulled OSS models -export RCLI_E2E_MODEL_ROOTS=/path/to/hnpu # *_HNPU / *_ANE / *.mlmodelc trees -bash scripts/e2e-modalities.sh /path/to/rcli # no --engine required +export WALLY_E2E_MODEL_ROOTS=/path/to/hnpu # *_HNPU / *_ANE / *.mlmodelc trees +bash scripts/test/e2e-modalities.sh /path/to/wally # no --engine required ``` -`RCLI_E2E_LLM`, `RCLI_E2E_STT`, `RCLI_E2E_IMAGE`, … pin one primitive. Catalog +`WALLY_E2E_LLM`, `WALLY_E2E_STT`, `WALLY_E2E_IMAGE`, … pin one primitive. Catalog ids (`mlx-qwen3`, `whisper-base-npu`) pin the framework; a Hugging Face repo page is HTML, not a bundle. @@ -267,30 +278,30 @@ page is HTML, not a bundle. | | | |---|---| -| `rcli run` / `rcli chat` | chat (REPL with no prompt) | -| `rcli pull` / `rcli models download` | download | -| `rcli list` / `rcli ls` | local models (`--all` = catalog) | -| `rcli show` | one model | -| `rcli rm` | delete | -| `rcli llm generate` / `stream` | completion | -| `rcli vlm generate --image` | vision | -| `rcli stt transcribe` | speech → text | -| `rcli tts synthesize` | text → WAV | -| `rcli vad detect` | voice activity | -| `rcli embed` | embeddings | -| `rcli rerank` | rerank documents | -| `rcli image generate` | text → image (NeuRT / Apple Silicon) | -| `rcli serve` | OpenAI-compatible HTTP (macOS/Linux) | -| `rcli backends` | registered engines | -| `rcli info` | versions and paths | +| `wally run` / `wally chat` | chat (REPL with no prompt) | +| `wally pull` / `wally models download` | download | +| `wally list` / `wally ls` | local models (`--all` = catalog) | +| `wally show` | one model | +| `wally rm` | delete | +| `wally llm generate` / `stream` | completion | +| `wally vlm generate --image` | vision | +| `wally stt transcribe` | speech → text | +| `wally tts synthesize` | text → WAV | +| `wally vad detect` | voice activity | +| `wally embed` | embeddings | +| `wally rerank` | rerank documents | +| `wally image generate` | text → image (NeuRT / Apple Silicon) | +| `wally serve` | OpenAI-compatible HTTP (macOS/Linux) | +| `wally backends` | registered engines | +| `wally info` | versions and paths | | `--engine` | force mlx / llamacpp / sherpa / onnx / neurt / qhexrt | -| `rcli login` / `logout` / `whoami` | sign in to the console that serves upstream models | -| `rcli usage` | credit left, then tokens and spend over the last hour and day | -| `rcli claude-code` / `claude-desktop` | open Claude against a model | -| `rcli clion` / `rustrover` | point a JetBrains IDE at a model | -| `rcli opencode` | open a coding session against a model | +| `wally login` / `logout` / `whoami` | sign in to the console that serves upstream models | +| `wally usage` | credit left, then tokens and spend over the last hour and day | +| `wally claude-code` / `claude-desktop` | open Claude against a model | +| `wally clion` / `rustrover` | point a JetBrains IDE at a model | +| `wally opencode` | open a coding session against a model | -`rcli --help` and `rcli --help` cover the rest. +`wally --help` and `wally --help` cover the rest. ## Editors and coding agents @@ -298,13 +309,13 @@ One command points a tool at a model and starts it. There is nothing to configure by hand: ```bash -rcli claude-code -m qwen3-0.6b -rcli clion -m models/gemma-4-31b-it -rcli claude-desktop -m models/gemma-4-31b-it +wally claude-code -m qwen3-0.6b +wally clion -m models/gemma-4-31b-it +wally claude-desktop -m models/gemma-4-31b-it ``` The model can be one on this machine or one the console serves. Without `-m` the -tool starts the way you already have it configured, and rcli wires nothing. +tool starts the way you already have it configured, and wally wires nothing. | Tool | How it is wired | | --- | --- | @@ -313,19 +324,19 @@ tool starts the way you already have it configured, and rcli wires nothing. | `clion`, `rustrover` | AI Assistant's OpenAI-compatible provider, which works without a JetBrains AI subscription | 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 rcli about gets +of launching anything, which is how a tool nobody has taught wally about gets wired up. `--restore` puts Claude Desktop or a JetBrains IDE back the way it was and starts nothing; a normal run already undoes its own configuration when the app quits, so this is for the run that was interrupted before it could. -The first `rcli clion` on a machine takes a while, because it installs the AI +The first `wally clion` on a machine takes a while, because it installs the AI Assistant plugin headlessly before starting the IDE. Later runs are quick. That endpoint sits on a fixed port rather than whatever happened to be free, because -the IDE reads the address once at startup out of a file rcli writes beforehand, +the IDE reads the address once at startup out of a file wally writes beforehand, and a port that moved would leave that file naming something dead. Claude Code and Claude Desktop speak Anthropic's Messages API, while the models -rcli serves speak OpenAI's, so a translator sits between them. It carries tool +wally serves speak OpenAI's, so a translator sits between them. It carries tool definitions out, tool calls back, and the results of those calls out again, which is what lets an agent on the far side run the tools it was given rather than describe them. The JetBrains IDEs need no translator, because AI Assistant @@ -336,54 +347,53 @@ speaks OpenAI already. A model you have not downloaded can still answer, if the console serves it: ```bash -rcli login -rcli whoami -rcli run models/gemma-4-31b-it "why is the sky blue" +wally login +wally whoami +wally run models/gemma-4-31b-it "why is the sky blue" ``` -`rcli login` opens the console in a browser and waits for you to approve the -machine. `rcli logout` deletes the session. +`wally login` opens the console in a browser and waits for you to approve the +machine. `wally logout` deletes the session. -Where the credential is kept depends on the platform, and `RCLI_PROFILE_DIR` +Where the credential is kept depends on the platform, and `WALLY_PROFILE_DIR` moves it anywhere: | | Path | |---|---| -| macOS, Linux | `$XDG_CONFIG_HOME/rcli/credentials.json`, or `~/.config/rcli` when unset | -| Windows | `%LOCALAPPDATA%\RunAnywhere\RCLI\credentials.dat`, encrypted with DPAPI | +| macOS, Linux | `$XDG_CONFIG_HOME/wally/credentials.json`, or `~/.config/wally` when unset | +| Windows | `%LOCALAPPDATA%\RunAnywhere\Wally\credentials.dat`, encrypted with DPAPI | -`RCLI_CONSOLE_URL` points the CLI at a console API other than the default, and -`RCLI_CONSOLE_WEB_URL` at the page that approves the sign-in. Those are two +`WALLY_CONSOLE_URL` points the CLI at a console API other than the default, and +`WALLY_CONSOLE_WEB_URL` at the page that approves the sign-in. Those are two different hosts; see AGENTS.md. -This is separate from `rcli auth login`, which signs the device in to the -control plane with an API key. The two are being unified; see the auth work in -flight. +This is separate from `wally auth login`, which signs a device in with an API +key rather than a browser. Most people want `wally login`. ## Build from source -Stage a C++ desktop kit from [runanywhere-sdks](https://github.com/RunanywhereAI/runanywhere-sdks). The pin is `cmake/sdk-pin.cmake` (`RCLI_PINNED_SDK_VERSION`). +Stage a C++ desktop kit from [runanywhere-sdks](https://github.com/RunanywhereAI/runanywhere-sdks). The pin is `cmake/sdk-pin.cmake` (`WALLY_PINNED_SDK_VERSION`). -**C++-only** (`rcli-cxx` on Apple; `rcli` elsewhere): +**C++-only** (`wally-cxx` on Apple; `wally` elsewhere): ```bash cmake -B build -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH=/path/to/kit cmake --build build -./build/rcli version # ./build/rcli-cxx on Apple -./build/rcli backends +./build/wally version # ./build/wally-cxx on Apple +./build/wally backends ``` -**Apple Silicon product binary** is the Swift MLX host (`build/rcli`). Independent clones need the SDK Swift tree (`RCLI_SDK_SWIFT_PATH`) and `RCLI_APPLE_MLX_HOST=ON` (the default): +**Apple Silicon product binary** is the Swift MLX host (`build/wally`). Independent clones need the SDK Swift tree (`WALLY_SDK_SWIFT_PATH`) and `WALLY_APPLE_MLX_HOST=ON` (the default): ```bash -export RCLI_SDK_SWIFT_PATH=/path/to/runanywhere-sdks +export WALLY_SDK_SWIFT_PATH=/path/to/runanywhere-sdks cmake -B build -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH=/path/to/kit cmake --build build -# or: scripts/build-mlx.sh build -./build/rcli version -./build/rcli backends +# or: scripts/build/build-mlx.sh build +./build/wally version +./build/wally backends ``` See [CONTRIBUTING.md](./CONTRIBUTING.md). diff --git a/cmake/RunAnywhereSDK.cmake b/cmake/RunAnywhereSDK.cmake index 9146700..a2dca87 100644 --- a/cmake/RunAnywhereSDK.cmake +++ b/cmake/RunAnywhereSDK.cmake @@ -3,71 +3,71 @@ # # CMAKE_PREFIX_PATH= # dist/cpp-desktop-- # or RUNANYWHERE_ROOT= -# or RCLI_SDK_KIT= +# or WALLY_SDK_KIT= # -# RCLI_SDK_DIR is accepted only as an alias for a *kit* prefix (must contain +# WALLY_SDK_DIR is accepted only as an alias for a *kit* prefix (must contain # lib/cmake/RunAnywhere/RunAnywhereConfig.cmake). Pointing it at the monorepo # source tree is a hard error. -set(RCLI_SDK_KIT "" CACHE PATH "Path to a staged RunAnywhere C++ desktop kit") -set(RCLI_SDK_DIR "" CACHE PATH "Deprecated alias for RCLI_SDK_KIT (kit prefix, not source)") -set(RCLI_SDK_VERSION "" CACHE STRING "EXACT find_package version; default from cmake/sdk-pin.cmake") +set(WALLY_SDK_KIT "" CACHE PATH "Path to a staged RunAnywhere C++ desktop kit") +set(WALLY_SDK_DIR "" CACHE PATH "Deprecated alias for WALLY_SDK_KIT (kit prefix, not source)") +set(WALLY_SDK_VERSION "" CACHE STRING "EXACT find_package version; default from cmake/sdk-pin.cmake") -if(RCLI_SDK_DIR AND NOT RCLI_SDK_KIT) - set(RCLI_SDK_KIT "${RCLI_SDK_DIR}") +if(WALLY_SDK_DIR AND NOT WALLY_SDK_KIT) + set(WALLY_SDK_KIT "${WALLY_SDK_DIR}") endif() include("${CMAKE_CURRENT_LIST_DIR}/sdk-pin.cmake") -if(RCLI_SDK_VERSION AND NOT RCLI_SDK_VERSION STREQUAL RCLI_PINNED_SDK_VERSION) +if(WALLY_SDK_VERSION AND NOT WALLY_SDK_VERSION STREQUAL WALLY_PINNED_SDK_VERSION) message(FATAL_ERROR - "RCLI_SDK_VERSION=${RCLI_SDK_VERSION} does not match " - "RCLI_PINNED_SDK_VERSION=${RCLI_PINNED_SDK_VERSION} in cmake/sdk-pin.cmake. " + "WALLY_SDK_VERSION=${WALLY_SDK_VERSION} does not match " + "WALLY_PINNED_SDK_VERSION=${WALLY_PINNED_SDK_VERSION} in cmake/sdk-pin.cmake. " "Bump the pin (and kit checksums) instead of overriding the version.") endif() -if(NOT RCLI_SDK_VERSION) - set(RCLI_SDK_VERSION "${RCLI_PINNED_SDK_VERSION}") +if(NOT WALLY_SDK_VERSION) + set(WALLY_SDK_VERSION "${WALLY_PINNED_SDK_VERSION}") endif() -if(RCLI_SDK_KIT) - if(EXISTS "${RCLI_SDK_KIT}/CMakeLists.txt" AND NOT EXISTS - "${RCLI_SDK_KIT}/lib/cmake/RunAnywhere/RunAnywhereConfig.cmake") +if(WALLY_SDK_KIT) + if(EXISTS "${WALLY_SDK_KIT}/CMakeLists.txt" AND NOT EXISTS + "${WALLY_SDK_KIT}/lib/cmake/RunAnywhere/RunAnywhereConfig.cmake") message(FATAL_ERROR - "RCLI_SDK_KIT/RCLI_SDK_DIR points at SDK source (${RCLI_SDK_KIT}). " + "WALLY_SDK_KIT/WALLY_SDK_DIR points at SDK source (${WALLY_SDK_KIT}). " "Build the C++ desktop kit first:\n" " cmake --preset cpp-desktop-macos-arm64 && " "cmake --build --preset cpp-desktop-macos-arm64 --target package-cpp-desktop\n" - "then pass -DRCLI_SDK_KIT=/dist/cpp-desktop--") + "then pass -DWALLY_SDK_KIT=/dist/cpp-desktop--") endif() - list(PREPEND CMAKE_PREFIX_PATH "${RCLI_SDK_KIT}") + list(PREPEND CMAKE_PREFIX_PATH "${WALLY_SDK_KIT}") endif() -if(DEFINED ENV{RUNANYWHERE_ROOT} AND NOT RCLI_SDK_KIT) +if(DEFINED ENV{RUNANYWHERE_ROOT} AND NOT WALLY_SDK_KIT) list(PREPEND CMAKE_PREFIX_PATH "$ENV{RUNANYWHERE_ROOT}") endif() -find_package(RunAnywhere ${RCLI_SDK_VERSION} EXACT REQUIRED CONFIG) +find_package(RunAnywhere ${WALLY_SDK_VERSION} EXACT REQUIRED CONFIG) # Proto is the SOT across the two repos. The kit stamps SCHEMA_LOCK into -# find_package vars; this pin must match. Do not run protoc in RCLI to "fix" +# find_package vars; this pin must match. Do not run protoc in WALLY to "fix" # a mismatch — bump the pin after consuming a new kit. if(NOT RunAnywhere_IDL_SCHEMA_SHA256) message(FATAL_ERROR "RunAnywhere kit is missing IDL schema metadata " "(RunAnywhere_IDL_SCHEMA_SHA256). Rebuild with package-cpp-desktop.") endif() -if(NOT RunAnywhere_IDL_SCHEMA_SHA256 STREQUAL RCLI_PINNED_IDL_SCHEMA_SHA256 - OR NOT RunAnywhere_IDL_VERSION STREQUAL RCLI_PINNED_IDL_VERSION - OR NOT RunAnywhere_IDL_PROTOC_VERSION STREQUAL RCLI_PINNED_IDL_PROTOC_VERSION) +if(NOT RunAnywhere_IDL_SCHEMA_SHA256 STREQUAL WALLY_PINNED_IDL_SCHEMA_SHA256 + OR NOT RunAnywhere_IDL_VERSION STREQUAL WALLY_PINNED_IDL_VERSION + OR NOT RunAnywhere_IDL_PROTOC_VERSION STREQUAL WALLY_PINNED_IDL_PROTOC_VERSION) message(FATAL_ERROR "RunAnywhere kit IDL does not match cmake/sdk-pin.cmake.\n" " kit: ${RunAnywhere_IDL_VERSION} / ${RunAnywhere_IDL_SCHEMA_SHA256} / protoc ${RunAnywhere_IDL_PROTOC_VERSION}\n" - " pin: ${RCLI_PINNED_IDL_VERSION} / ${RCLI_PINNED_IDL_SCHEMA_SHA256} / protoc ${RCLI_PINNED_IDL_PROTOC_VERSION}\n" + " pin: ${WALLY_PINNED_IDL_VERSION} / ${WALLY_PINNED_IDL_SCHEMA_SHA256} / protoc ${WALLY_PINNED_IDL_PROTOC_VERSION}\n" "Consume the kit that matches this pin, or update the pin after a schema bump. " - "RCLI must not run protoc.") + "WALLY must not run protoc.") endif() -set(RCLI_SDK_ROOT "${RunAnywhere_INCLUDE_DIR}/.." CACHE INTERNAL "") -set(RCLI_IDL_DIR "${RunAnywhere_IDL_DIR}" CACHE INTERNAL "") -set(RCLI_PROTO_INCLUDE_DIR "${RunAnywhere_PROTO_INCLUDE_DIR}" CACHE INTERNAL "") +set(WALLY_SDK_ROOT "${RunAnywhere_INCLUDE_DIR}/.." CACHE INTERNAL "") +set(WALLY_IDL_DIR "${RunAnywhere_IDL_DIR}" CACHE INTERNAL "") +set(WALLY_PROTO_INCLUDE_DIR "${RunAnywhere_PROTO_INCLUDE_DIR}" CACHE INTERNAL "") if(NOT TARGET RunAnywhere::commons) message(FATAL_ERROR "find_package(RunAnywhere) did not import RunAnywhere::commons") @@ -77,29 +77,29 @@ endif() # third_party/onnxruntime.dll into INTERFACE_LINK_LIBRARIES. Drop any # .dll from the imported target and, when present, link the import lib. if(WIN32) - get_target_property(_rcli_ra_ifaces RunAnywhere::commons INTERFACE_LINK_LIBRARIES) - if(_rcli_ra_ifaces) - set(_rcli_ra_kept "") - foreach(_lib IN LISTS _rcli_ra_ifaces) + get_target_property(_wally_ra_ifaces RunAnywhere::commons INTERFACE_LINK_LIBRARIES) + if(_wally_ra_ifaces) + set(_wally_ra_kept "") + foreach(_lib IN LISTS _wally_ra_ifaces) if(_lib MATCHES "\\.[Dd][Ll][Ll]$") continue() endif() - list(APPEND _rcli_ra_kept "${_lib}") + list(APPEND _wally_ra_kept "${_lib}") endforeach() - set_property(TARGET RunAnywhere::commons PROPERTY INTERFACE_LINK_LIBRARIES "${_rcli_ra_kept}") + set_property(TARGET RunAnywhere::commons PROPERTY INTERFACE_LINK_LIBRARIES "${_wally_ra_kept}") endif() if(DEFINED RunAnywhere_LIBRARY_DIR AND EXISTS "${RunAnywhere_LIBRARY_DIR}/onnxruntime.lib") - get_target_property(_rcli_ra_ifaces RunAnywhere::commons INTERFACE_LINK_LIBRARIES) - set(_rcli_ra_ifaces "${_rcli_ra_ifaces}") - if(NOT _rcli_ra_ifaces MATCHES "onnxruntime\\.lib") + get_target_property(_wally_ra_ifaces RunAnywhere::commons INTERFACE_LINK_LIBRARIES) + set(_wally_ra_ifaces "${_wally_ra_ifaces}") + if(NOT _wally_ra_ifaces MATCHES "onnxruntime\\.lib") set_property(TARGET RunAnywhere::commons APPEND PROPERTY INTERFACE_LINK_LIBRARIES "${RunAnywhere_LIBRARY_DIR}/onnxruntime.lib") endif() endif() if(DEFINED RunAnywhere_LIBRARY_DIR AND EXISTS "${RunAnywhere_LIBRARY_DIR}/libcurl.lib") - get_target_property(_rcli_ra_ifaces RunAnywhere::commons INTERFACE_LINK_LIBRARIES) - set(_rcli_ra_ifaces "${_rcli_ra_ifaces}") - if(NOT _rcli_ra_ifaces MATCHES "libcurl\\.lib") + get_target_property(_wally_ra_ifaces RunAnywhere::commons INTERFACE_LINK_LIBRARIES) + set(_wally_ra_ifaces "${_wally_ra_ifaces}") + if(NOT _wally_ra_ifaces MATCHES "libcurl\\.lib") set_property(TARGET RunAnywhere::commons APPEND PROPERTY INTERFACE_LINK_LIBRARIES "${RunAnywhere_LIBRARY_DIR}/libcurl.lib") endif() @@ -107,63 +107,105 @@ if(WIN32) # Static libcurl/libarchive in 0.20.26 kits omit these Windows imports. # Keep them here until a later kit bakes them into SYSTEM_LIBS. foreach(_sys IN ITEMS iphlpapi xmllite ole32) - get_target_property(_rcli_ra_ifaces RunAnywhere::commons INTERFACE_LINK_LIBRARIES) - set(_rcli_ra_ifaces "${_rcli_ra_ifaces}") - if(NOT _rcli_ra_ifaces MATCHES "${_sys}") + get_target_property(_wally_ra_ifaces RunAnywhere::commons INTERFACE_LINK_LIBRARIES) + set(_wally_ra_ifaces "${_wally_ra_ifaces}") + if(NOT _wally_ra_ifaces MATCHES "${_sys}") set_property(TARGET RunAnywhere::commons APPEND PROPERTY INTERFACE_LINK_LIBRARIES "${_sys}") endif() endforeach() endif() +# GNU ld resolves static archives in a single left-to-right pass, so the kit's +# many interdependent .a files (protobuf<->absl, a backend<->its runtime) leave +# undefined references when their order does not happen to satisfy every +# cross-reference. Wrap the whole imported link interface in a linker group so +# ld re-scans it until resolved. Apple's ld64 and MSVC link.exe do multi-pass +# resolution and need no group; only GNU ld (Linux) does. +if(UNIX AND NOT APPLE) + # On Linux, sherpa-onnx ships as a shared library in the kit's third_party + # dir (macOS/Windows bundle it as a static .a). The kit config wires sherpa + # only through a Windows .lib glob, so the SherpaOnnx* symbols the sherpa + # backend needs are otherwise unresolved here. Link the .so explicitly; its + # dir is already on the build rpath (RunAnywhere_THIRD_PARTY_DIR), and the + # release bottle stages it beside the binary. + if(DEFINED RunAnywhere_THIRD_PARTY_DIR + AND EXISTS "${RunAnywhere_THIRD_PARTY_DIR}/libsherpa-onnx-c-api.so") + set_property(TARGET RunAnywhere::commons APPEND PROPERTY INTERFACE_LINK_LIBRARIES + "${RunAnywhere_THIRD_PARTY_DIR}/libsherpa-onnx-c-api.so") + endif() + # ggml-cpu is built with OpenMP; GNU needs libgomp linked explicitly. + find_package(OpenMP QUIET) + # GNU ld resolves static archives in a single left-to-right pass. Wrap the + # kit's interdependent .a files in a linker group so ld re-scans until + # resolved — and include librac_commons.a itself, because the backend + # archives reference symbols (the CPU runtime registry, etc.) that live in + # commons, which the imported target otherwise places ahead of the group. + # Apple ld64 and MSVC do multi-pass resolution and need no group. + get_target_property(_wally_ra_ifaces RunAnywhere::commons INTERFACE_LINK_LIBRARIES) + set(_wally_grp_head "") + if(DEFINED RunAnywhere_LIBRARY_DIR AND EXISTS "${RunAnywhere_LIBRARY_DIR}/librac_commons.a") + list(APPEND _wally_grp_head "${RunAnywhere_LIBRARY_DIR}/librac_commons.a") + endif() + set_property(TARGET RunAnywhere::commons PROPERTY INTERFACE_LINK_LIBRARIES + "-Wl,--start-group" ${_wally_grp_head} ${_wally_ra_ifaces} "-Wl,--end-group") + if(OpenMP_CXX_FOUND) + set_property(TARGET RunAnywhere::commons APPEND PROPERTY + INTERFACE_LINK_LIBRARIES OpenMP::OpenMP_CXX) + endif() +endif() + # Back-compat name used by the rest of this CMakeLists. if(NOT TARGET rac_commons) add_library(rac_commons ALIAS RunAnywhere::commons) endif() -function(rcli_define_engine_macros target) +function(wally_define_engine_macros target) if(RunAnywhere_HAS_LLAMACPP) - target_compile_definitions(${target} PRIVATE RCLI_HAS_LLAMACPP=1) + target_compile_definitions(${target} PRIVATE WALLY_HAS_LLAMACPP=1) endif() if(RunAnywhere_HAS_ONNX) - target_compile_definitions(${target} PRIVATE RCLI_HAS_ONNX=1) + target_compile_definitions(${target} PRIVATE WALLY_HAS_ONNX=1) endif() if(RunAnywhere_HAS_SHERPA) - target_compile_definitions(${target} PRIVATE RCLI_HAS_SHERPA=1) + target_compile_definitions(${target} PRIVATE WALLY_HAS_SHERPA=1) endif() if(RunAnywhere_HAS_MLX) - target_compile_definitions(${target} PRIVATE RCLI_HAS_MLX=1) + target_compile_definitions(${target} PRIVATE WALLY_HAS_MLX=1) endif() if(RunAnywhere_HAS_CLOUD) - target_compile_definitions(${target} PRIVATE RCLI_HAS_CLOUD=1) + target_compile_definitions(${target} PRIVATE WALLY_HAS_CLOUD=1) endif() if(TARGET RunAnywhere::neurt) - target_compile_definitions(${target} PRIVATE RCLI_HAS_NEURT=1) + target_compile_definitions(${target} PRIVATE WALLY_HAS_NEURT=1) target_link_libraries(${target} PRIVATE RunAnywhere::neurt) endif() if(TARGET RunAnywhere::qhexrt) - target_compile_definitions(${target} PRIVATE RCLI_HAS_QHEXRT=1) + target_compile_definitions(${target} PRIVATE WALLY_HAS_QHEXRT=1) target_link_libraries(${target} PRIVATE RunAnywhere::qhexrt) endif() if(RunAnywhere_HAS_RAG) - target_compile_definitions(${target} PRIVATE RCLI_HAS_RAG=1) + target_compile_definitions(${target} PRIVATE WALLY_HAS_RAG=1) endif() if(RunAnywhere_HAS_SERVER) - target_compile_definitions(${target} PRIVATE RCLI_HAS_SERVER=1) + target_compile_definitions(${target} PRIVATE WALLY_HAS_SERVER=1) + endif() + if(TARGET RunAnywhere::server) + target_link_libraries(${target} PRIVATE RunAnywhere::server) endif() endfunction() # Win32 LoadLibrary searches the exe directory then PATH. Kit third_party # (onnxruntime.dll, sherpa-onnx-c-api.dll, …) must sit next to every binary -# that links rcli_core — rcli.exe and the unit-test exes. Linking the import +# that links wally_core — wally.exe and the unit-test exes. Linking the import # lib is not enough; 0xc0000135 is a missing DLL at process start. -function(rcli_stage_windows_runtime_dlls target) +function(wally_stage_windows_runtime_dlls target) if(NOT WIN32) return() endif() if(DEFINED RunAnywhere_THIRD_PARTY_DIR AND EXISTS "${RunAnywhere_THIRD_PARTY_DIR}") - file(GLOB _rcli_tp_dlls "${RunAnywhere_THIRD_PARTY_DIR}/*.dll") - foreach(_dll IN LISTS _rcli_tp_dlls) + file(GLOB _wally_tp_dlls "${RunAnywhere_THIRD_PARTY_DIR}/*.dll") + foreach(_dll IN LISTS _wally_tp_dlls) get_filename_component(_dll_name "${_dll}" NAME) add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different diff --git a/cmake/copy-overlay-dlls.cmake b/cmake/copy-overlay-dlls.cmake index d784f97..45eab16 100644 --- a/cmake/copy-overlay-dlls.cmake +++ b/cmake/copy-overlay-dlls.cmake @@ -1,4 +1,4 @@ -# Build-time copy of private-overlay runtime files next to rcli.exe. +# Build-time copy of private-overlay runtime files next to wally.exe. # GLOB runs here (not at configure) so a newly dropped overlay is picked up # on the next build without re-running CMake. # @@ -7,12 +7,12 @@ # security catalog, QHexRT/src/bonsai/fastrpc_win.cpp) through ADSP_LIBRARY_PATH # ∪ exe_dir() -- neither of which this exe's own directory satisfies unless the # skel/.cat pair is staged here too. A *.dll-only glob left them in kit/bin -# only, so rcli.exe ran every standard QNN-graph model fine (those resolve via +# only, so wally.exe ran every standard QNN-graph model fine (those resolve via # LoadLibraryW + PATH, which addSidecarDirToDllSearch-equivalent staging still # covers) while the ternary decoder failed remote_handle64_open with # AEE_EUNABLETOLOAD (0x80000406) -- content-verified, so this reads exactly # like a missing/mismatched catalog even though the real cause was the file -# never being copied here at all. See the rcli-e2e skill's "Device / overlay +# never being copied here at all. See the wally-e2e skill's "Device / overlay # gotchas" for the full trace. if(NOT DEFINED SRC_DIR OR NOT DEFINED DST_DIR) message(FATAL_ERROR "copy-overlay-dlls: SRC_DIR and DST_DIR required") diff --git a/cmake/sdk-pin.cmake b/cmake/sdk-pin.cmake index a57202d..3e556f4 100644 --- a/cmake/sdk-pin.cmake +++ b/cmake/sdk-pin.cmake @@ -1,21 +1,33 @@ -# Pinned SDK kit this RCLI tree is validated against. -# Bump together with the SHA-256 sidecars after an SDK release. +# Pinned SDK kit this WALLY tree is validated against. # -# The IDL pins are copied from the kit's share/runanywhere/SCHEMA_LOCK (which -# is idl/SCHEMA_LOCK from the SDK). RCLI never runs protoc — a mismatch here -# means consume a new kit and update this file, not regenerate headers. +# The values are NOT written here any more. They live in `versions.toml` at the +# repo root, the single source for every version and pin, and this file reads +# them so its consumers still see the same WALLY_PINNED_* variables. Bump the +# pins in versions.toml, not here. # -# 0.20.33 moves the IDL pin to 1.1.1 / ec50b7ca, retiring the 0.20.32 note that -# held it at 1.1.0. That note was correct for its release: 0.20.32 published via -# `publish_from_run_id`, reusing artifacts built before the monorepo bumped -# idl/VERSION for a COMMENT-ONLY .proto edit, so the kit genuinely carried the -# older lock and pinning 1.1.1 would have failed fetch-kit.sh against the very -# kit it validates. 0.20.33's kits were built after that bump, so these values -# are read from the shipped SCHEMA_LOCK rather than carried forward. -set(RCLI_PINNED_SDK_VERSION "0.20.34") -set(RCLI_PINNED_IDL_VERSION "1.1.1") -set(RCLI_PINNED_IDL_SCHEMA_SHA256 "ec50b7ca4beff9fa065c5ae1f12dbbc0c996d8b43e64c44bf350ef86a1fa41aa") -set(RCLI_PINNED_IDL_PROTOC_VERSION "35.1") -set(RCLI_PINNED_KIT_SHA256_MACOS_ARM64 "8bf2019b27f10001b33d78338b5dcf78a14977c70fcded3f8a3e0647ca8da188") -set(RCLI_PINNED_KIT_SHA256_WINDOWS_X64 "d472ada77c67bdf4445cd0ba12cb029e9f9cc3ff09937b18fe8025d5e28e5c6f") -set(RCLI_PINNED_KIT_SHA256_WINDOWS_ARM64 "4d1984ec8c3867a2fe23ed660e586b5ff8515b31211acf8eff7144dfee9cea22") +# The IDL pins mirror the kit's share/runanywhere/SCHEMA_LOCK (idl/SCHEMA_LOCK +# from the SDK). WALLY never runs protoc -- a mismatch means consume a new kit +# and update versions.toml, not regenerate headers. History on why the IDL pin +# moved from 1.1.0 to 1.1.1 across 0.20.32/0.20.33 is in versions.toml's [sdk] +# comment and this file's git log. + +# Pull one `key = "value"` out of versions.toml into ${out_var}. Flat format by +# design, so a regex is enough and CMake needs no TOML parser. +function(_wally_read_version key out_var) + file(STRINGS "${CMAKE_CURRENT_LIST_DIR}/../versions.toml" _line + REGEX "^[ \t]*${key}[ \t]*=") + if(NOT _line) + message(FATAL_ERROR "versions.toml is missing '${key}'") + endif() + list(GET _line 0 _line) + string(REGEX REPLACE "^[^\"]*\"([^\"]*)\".*$" "\\1" _value "${_line}") + set(${out_var} "${_value}" PARENT_SCOPE) +endfunction() + +_wally_read_version("kit_version" WALLY_PINNED_SDK_VERSION) +_wally_read_version("idl_version" WALLY_PINNED_IDL_VERSION) +_wally_read_version("idl_schema_sha256" WALLY_PINNED_IDL_SCHEMA_SHA256) +_wally_read_version("idl_protoc_version" WALLY_PINNED_IDL_PROTOC_VERSION) +_wally_read_version("kit_sha256_macos_arm64" WALLY_PINNED_KIT_SHA256_MACOS_ARM64) +_wally_read_version("kit_sha256_windows_x64" WALLY_PINNED_KIT_SHA256_WINDOWS_X64) +_wally_read_version("kit_sha256_windows_arm64" WALLY_PINNED_KIT_SHA256_WINDOWS_ARM64) diff --git a/contracts/extract-cli-contract.py b/contracts/extract-cli-contract.py new file mode 100644 index 0000000..b6c43e9 --- /dev/null +++ b/contracts/extract-cli-contract.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Carve the CLI-facing slice out of the full control-plane contract. + +The CLI uses six of the control plane's operations. Rather than vendor the whole +8000-line `control-plane-v1.openapi.json`, this extracts those operations and the +transitive closure of the schemas they reference into a self-contained, valid +OpenAPI document, `wally-cli-v1.openapi.json`, which is what gets pinned and fed +to `generate_console_binding.py`. + + python3 contracts/extract-cli-contract.py \\ + ../RA-Cloud-WorkSpace/InferenceInfra/contracts/control-plane-v1.openapi.json + +Run this only when re-vendoring after the upstream contract changes; then run +generate_console_binding.py and commit both outputs together. +""" + +from __future__ import annotations + +import argparse +import collections +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +OUT = ROOT / "wally-cli-v1.openapi.json" + +CLI_OPERATION_IDS = { + "startCliAuthorization", + "pollCliAuthorization", + "refreshCliAuthorization", + "revokeCliAuthorization", + "getCurrentIdentity", + "getCliUsage", +} +HTTP_METHODS = {"get", "post", "put", "delete", "patch"} + + +def _refs(value: object, out: set[str]) -> None: + if isinstance(value, dict): + if "$ref" in value: + out.add(value["$ref"].split("/")[-1]) + for child in value.values(): + _refs(child, out) + elif isinstance(value, list): + for child in value: + _refs(child, out) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("source", type=Path, help="path to control-plane-v1.openapi.json") + args = parser.parse_args() + + source = json.loads(args.source.read_text(encoding="utf-8")) + schemas = source["components"]["schemas"] + + closure: set[str] = set() + + def visit(name: str) -> None: + if name in closure or name not in schemas: + return + closure.add(name) + found: set[str] = set() + _refs(schemas[name], found) + for dependency in found: + visit(dependency) + + paths: "collections.OrderedDict[str, dict]" = collections.OrderedDict() + for path, item in source["paths"].items(): + kept = {} + for method, operation in item.items(): + if method in HTTP_METHODS and operation.get("operationId") in CLI_OPERATION_IDS: + kept[method] = operation + found: set[str] = set() + _refs(operation, found) + for dependency in found: + visit(dependency) + if kept: + paths[path] = kept + + extract = { + "openapi": source["openapi"], + "info": { + "title": "Wally CLI control-plane contract (extract)", + "version": source["info"]["version"], + "description": ( + "CLI-facing operations extracted from control-plane-v1.openapi.json by " + "contracts/extract-cli-contract.py. Do not hand-edit." + ), + }, + "paths": paths, + "components": {"schemas": {name: schemas[name] for name in sorted(closure)}}, + } + OUT.write_text(json.dumps(extract, indent=2, sort_keys=True) + "\n", encoding="utf-8") + operations = sum(len(methods) for methods in paths.values()) + print(f"wrote {OUT}: {operations} operations, {len(closure)} schemas") + + +if __name__ == "__main__": + main() diff --git a/contracts/generate_console_binding.py b/contracts/generate_console_binding.py new file mode 100644 index 0000000..4910bf5 --- /dev/null +++ b/contracts/generate_console_binding.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +"""Generate the typed C++ binding for the CLI's control-plane HTTP calls. + +The source of truth is the pinned OpenAPI extract `wally-cli-v1.openapi.json` +(itself carved from InferenceInfra's `control-plane-v1.openapi.json`). This +reads that artifact and emits `src/account/console_contract.h`: an enum class +per string enum, a struct per object schema, and nlohmann to/from-json for each, +so `console.cpp` never hand-builds a request body or parses a response field by +name. A `kContractSha256` constant pins the exact artifact the header was built +from; `test_wally_contract` fails the build if the two drift. + + python3 contracts/generate_console_binding.py # write the header + python3 contracts/generate_console_binding.py --check # fail if stale + +Run it and commit the header whenever the pinned contract changes. Never edit +the header by hand. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +CONTRACT = ROOT / "wally-cli-v1.openapi.json" +HEADER = ROOT.parent / "src" / "account" / "console_contract.h" + +INT = "std::int64_t" + + +def _snake_to_pascal(name: str) -> str: + return "".join(part.capitalize() for part in name.replace("-", "_").split("_")) + + +def _enum_constant(value: str) -> str: + # "24h" -> k24H, "claude_code" -> kClaudeCode. A digit-leading value is + # still a legal identifier once prefixed with k. + return "k" + _snake_to_pascal(value) + + +def _resolve_type(schema: dict, schemas: dict) -> tuple[str, bool]: + """Return (c++ type, is_optional). Nullable/anyOf-null collapses to optional.""" + if "$ref" in schema: + return schema["$ref"].split("/")[-1], False + if "anyOf" in schema: + branches = [b for b in schema["anyOf"] if b.get("type") != "null"] + had_null = any(b.get("type") == "null" for b in schema["anyOf"]) + inner, _ = _resolve_type(branches[0], schemas) + return inner, had_null + kind = schema.get("type") + if kind == "string": + return "std::string", False + if kind == "integer": + return INT, False + if kind == "boolean": + return "bool", False + if kind == "array": + item, _ = _resolve_type(schema["items"], schemas) + return f"std::vector<{item}>", False + raise SystemExit(f"unsupported schema shape: {schema}") + + +def _is_enum(schema: dict) -> bool: + return "enum" in schema and schema.get("type") == "string" + + +def _emit_enum(name: str, schema: dict) -> str: + values = schema["enum"] + lines = [f"enum class {name} {{"] + lines += [f" {_enum_constant(v)}," for v in values] + lines.append("};") + lines.append("") + # from_json: reject an unknown value rather than silently defaulting. + lines.append(f"inline void from_json(const nlohmann::json& j, {name}& value) {{") + lines.append(" const std::string raw = j.get();") + for v in values: + lines.append(f' if (raw == "{v}") {{ value = {name}::{_enum_constant(v)}; return; }}') + lines.append( + f' throw nlohmann::json::type_error::create(302, "unknown {name}: " + raw, &j);' + ) + lines.append("}") + lines.append("") + lines.append(f"inline void to_json(nlohmann::json& j, const {name}& value) {{") + lines.append(" switch (value) {") + for v in values: + lines.append(f' case {name}::{_enum_constant(v)}: j = "{v}"; return;') + lines.append(" }") + lines.append("}") + return "\n".join(lines) + + +def _emit_struct(name: str, schema: dict, schemas: dict) -> str: + required = set(schema.get("required", [])) + props = schema.get("properties", {}) + fields = [] + for prop, pschema in props.items(): + base, nullable = _resolve_type(pschema, schemas) + optional = nullable or prop not in required + cpp = f"std::optional<{base}>" if optional else base + fields.append((prop, cpp, base, optional)) + + lines = [f"struct {name} {{"] + for prop, cpp, _base, _opt in fields: + lines.append(f" {cpp} {prop};") + lines.append("};") + lines.append("") + + # from_json is a tolerant reader: a missing or null field defaults rather + # than throwing, so a server that predates a field this build knows about + # still parses. A present field is strictly typed -- a wrong type or an + # unknown enum value is still an error. Requests never go through here (they + # are built in code), so only responses feel the tolerance, which is the + # right posture for a client that deploys independently of the server. A + # value-initialized scoped enum is its first member, a fine default. + lines.append(f"inline void from_json(const nlohmann::json& j, {name}& value) {{") + for prop, _cpp, base, optional in fields: + lines.append(f' if (j.contains("{prop}") && !j.at("{prop}").is_null()) {{') + if optional: + lines.append(f' value.{prop} = j.at("{prop}").get<{base}>();') + lines.append(" } else {") + lines.append(f" value.{prop} = std::nullopt;") + else: + lines.append(f' value.{prop} = j.at("{prop}").get<{base}>();') + lines.append(" } else {") + lines.append(f" value.{prop} = {base}{{}};") + lines.append(" }") + lines.append("}") + lines.append("") + + # to_json: emit required fields always, optionals only when set. + lines.append(f"inline void to_json(nlohmann::json& j, const {name}& value) {{") + lines.append(" j = nlohmann::json::object();") + for prop, _cpp, _base, optional in fields: + if optional: + lines.append(f" if (value.{prop}.has_value()) {{") + lines.append(f' j["{prop}"] = *value.{prop};') + lines.append(" }") + else: + lines.append(f' j["{prop}"] = value.{prop};') + lines.append("}") + return "\n".join(lines) + + +def render() -> str: + raw = CONTRACT.read_bytes() + digest = hashlib.sha256(raw).hexdigest() + document = json.loads(raw) + schemas = document["components"]["schemas"] + + enums = [n for n in sorted(schemas) if _is_enum(schemas[n])] + objects = [ + n for n in sorted(schemas) if schemas[n].get("type") == "object" and not _is_enum(schemas[n]) + ] + + # Objects in dependency order: a struct must be declared after the structs + # it names. A topological sort over $ref edges between object schemas. + ordered: list[str] = [] + visiting: set[str] = set() + + def refs(name: str) -> list[str]: + found: list[str] = [] + + def walk(v: object) -> None: + if isinstance(v, dict): + if "$ref" in v: + found.append(v["$ref"].split("/")[-1]) + for x in v.values(): + walk(x) + elif isinstance(v, list): + for x in v: + walk(x) + + walk(schemas[name]) + return found + + def visit(name: str) -> None: + if name in ordered or name not in objects: + return + visiting.add(name) + for dependency in refs(name): + if dependency in objects and dependency not in visiting: + visit(dependency) + visiting.discard(name) + if name not in ordered: + ordered.append(name) + + for name in objects: + visit(name) + + out = [ + "// Generated by contracts/generate_console_binding.py from", + "// contracts/wally-cli-v1.openapi.json. DO NOT EDIT.", + "//", + "// Typed request and response models for the CLI's control-plane calls, so", + "// console.cpp neither builds a request body by hand nor reads a response", + "// field by name. Regenerate and commit whenever the pinned contract moves.", + "#ifndef WALLY_ACCOUNT_CONSOLE_CONTRACT_H", + "#define WALLY_ACCOUNT_CONSOLE_CONTRACT_H", + "", + "#include ", + "#include ", + "#include ", + "#include ", + "", + "#include ", + "", + "namespace wally::account::contract {", + "", + "// SHA-256 of contracts/wally-cli-v1.openapi.json this header was built from.", + f'inline constexpr char kContractSha256[] = "{digest}";', + "", + ] + for name in enums: + out.append(_emit_enum(name, schemas[name])) + out.append("") + for name in ordered: + out.append(_emit_struct(name, schemas[name], schemas)) + out.append("") + out.append("} // namespace wally::account::contract") + out.append("") + out.append("#endif // WALLY_ACCOUNT_CONSOLE_CONTRACT_H") + out.append("") + return "\n".join(out) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true", help="fail if the header is stale") + args = parser.parse_args() + rendered = render() + if args.check: + current = HEADER.read_text(encoding="utf-8") if HEADER.exists() else "" + if current != rendered: + sys.stderr.write( + "console_contract.h is stale. Run:\n" + " python3 contracts/generate_console_binding.py\n" + "and commit the result.\n" + ) + sys.exit(1) + print("console_contract.h matches the pinned contract") + return + HEADER.write_text(rendered, encoding="utf-8") + print(f"wrote {HEADER}") + + +if __name__ == "__main__": + main() diff --git a/contracts/wally-cli-v1.openapi.json b/contracts/wally-cli-v1.openapi.json new file mode 100644 index 0000000..0cc1f25 --- /dev/null +++ b/contracts/wally-cli-v1.openapi.json @@ -0,0 +1,968 @@ +{ + "components": { + "schemas": { + "ApiError": { + "additionalProperties": false, + "description": "One closed error shape for every non-public control-plane route.", + "properties": { + "code": { + "$ref": "#/components/schemas/ApiErrorCode" + }, + "message": { + "maxLength": 2048, + "minLength": 1, + "title": "Message", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "title": "ApiError", + "type": "object" + }, + "ApiErrorCode": { + "enum": [ + "invalid_request", + "unauthorized", + "forbidden", + "not_found", + "conflict", + "gone", + "payload_too_large", + "rate_limited", + "internal_error", + "upstream_unavailable", + "service_unavailable", + "timeout", + "bad_request", + "expired_api_key", + "gateway_unavailable", + "idempotency_key_reused", + "insufficient_credit", + "invalid_api_key", + "model_not_entitled", + "not_in_cohort", + "provisioning_unavailable", + "proxy_retired", + "revoked_api_key", + "signup_method_not_allowed", + "upstream_contract_violation", + "user_rate_limited", + "gateway_not_configured", + "gateway_timeout", + "gateway_protocol_error", + "gateway_operation_pending", + "reconciliation_unverified" + ], + "title": "ApiErrorCode", + "type": "string" + }, + "CliClient": { + "enum": [ + "rcli" + ], + "title": "CliClient", + "type": "string" + }, + "CliPlan": { + "enum": [ + "beta" + ], + "title": "CliPlan", + "type": "string" + }, + "CliPollRequest": { + "additionalProperties": false, + "properties": { + "poll_secret": { + "maxLength": 128, + "minLength": 32, + "pattern": "^[A-Za-z0-9_-]+$", + "title": "Poll Secret", + "type": "string" + }, + "request_code": { + "maxLength": 64, + "minLength": 16, + "pattern": "^[A-Za-z0-9_-]+$", + "title": "Request Code", + "type": "string" + } + }, + "required": [ + "request_code", + "poll_secret" + ], + "title": "CliPollRequest", + "type": "object" + }, + "CliRefreshRequest": { + "additionalProperties": false, + "properties": { + "refresh_token": { + "maxLength": 8192, + "minLength": 1, + "title": "Refresh Token", + "type": "string" + } + }, + "required": [ + "refresh_token" + ], + "title": "CliRefreshRequest", + "type": "object" + }, + "CliStartRequest": { + "additionalProperties": false, + "properties": { + "client": { + "$ref": "#/components/schemas/CliClient" + }, + "hostname": { + "maxLength": 255, + "minLength": 1, + "title": "Hostname", + "type": "string" + } + }, + "required": [ + "hostname", + "client" + ], + "title": "CliStartRequest", + "type": "object" + }, + "CliStartResponse": { + "additionalProperties": false, + "properties": { + "expires_in": { + "maximum": 2147483647.0, + "minimum": 1.0, + "title": "Expires In", + "type": "integer" + }, + "interval": { + "maximum": 2147483647.0, + "minimum": 1.0, + "title": "Interval", + "type": "integer" + }, + "poll_secret": { + "maxLength": 128, + "minLength": 32, + "pattern": "^[A-Za-z0-9_-]+$", + "title": "Poll Secret", + "type": "string" + }, + "request_code": { + "maxLength": 64, + "minLength": 16, + "pattern": "^[A-Za-z0-9_-]+$", + "title": "Request Code", + "type": "string" + }, + "verification_url": { + "maxLength": 2048, + "minLength": 1, + "pattern": "^https?://", + "title": "Verification Url", + "type": "string" + } + }, + "required": [ + "request_code", + "poll_secret", + "verification_url", + "expires_in", + "interval" + ], + "title": "CliStartResponse", + "type": "object" + }, + "CliUsageEvent": { + "additionalProperties": false, + "description": "One request, as the CLI shows it.\n\nA narrower projection than the console's: no prompt text, and none of the\nmax-token accounting a terminal has no use for.", + "properties": { + "cached_tokens": { + "maximum": 2147483647.0, + "minimum": 0.0, + "title": "Cached Tokens", + "type": "integer" + }, + "completion_tokens": { + "maximum": 2147483647.0, + "minimum": 0.0, + "title": "Completion Tokens", + "type": "integer" + }, + "cost_micros": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Cost Micros", + "type": "integer" + }, + "error_code": { + "anyOf": [ + { + "maxLength": 120, + "minLength": 1, + "pattern": "^[a-z][a-z0-9._-]*$", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Code" + }, + "harness": { + "anyOf": [ + { + "$ref": "#/components/schemas/Harness" + }, + { + "type": "null" + } + ] + }, + "model": { + "maxLength": 128, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:/-]*$", + "title": "Model", + "type": "string" + }, + "prompt_tokens": { + "maximum": 2147483647.0, + "minimum": 0.0, + "title": "Prompt Tokens", + "type": "integer" + }, + "request_id": { + "maxLength": 128, + "minLength": 1, + "title": "Request Id", + "type": "string" + }, + "status_code": { + "maximum": 599.0, + "minimum": 100.0, + "title": "Status Code", + "type": "integer" + }, + "ts_start": { + "format": "date-time", + "title": "Ts Start", + "type": "string" + }, + "ttft_ms": { + "anyOf": [ + { + "maximum": 2147483647.0, + "minimum": 0.0, + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Ttft Ms" + } + }, + "required": [ + "request_id", + "model", + "harness", + "prompt_tokens", + "completion_tokens", + "cached_tokens", + "cost_micros", + "ttft_ms", + "status_code", + "error_code", + "ts_start" + ], + "title": "CliUsageEvent", + "type": "object" + }, + "CliUsageResponse": { + "additionalProperties": false, + "properties": { + "credit": { + "$ref": "#/components/schemas/CreditResponse" + }, + "models": { + "items": { + "$ref": "#/components/schemas/UsageModelRollup" + }, + "title": "Models", + "type": "array" + }, + "recent": { + "items": { + "$ref": "#/components/schemas/CliUsageEvent" + }, + "title": "Recent", + "type": "array" + }, + "timeline": { + "items": { + "$ref": "#/components/schemas/UsageTimelinePoint" + }, + "title": "Timeline", + "type": "array" + }, + "totals": { + "$ref": "#/components/schemas/UsageTotals" + }, + "windows": { + "items": { + "$ref": "#/components/schemas/CliUsageWindow" + }, + "title": "Windows", + "type": "array" + } + }, + "required": [ + "credit", + "totals", + "windows", + "timeline", + "models", + "recent" + ], + "title": "CliUsageResponse", + "type": "object" + }, + "CliUsageWindow": { + "additionalProperties": false, + "description": "Spend over a window ending now, not a calendar bucket.\n\n`timeline` cannot answer this. It groups by `usage_date`, so its finest\ngrain is a whole day and its most recent point is \"today so far\", which\ndrifts from an hour to a day depending on what time it is read. It also\ncarries no `cached_tokens`. `UsageTotals` carries all four numbers a\nterminal shows, so a window is a totals query with a different `since`.\n\n`seconds` is the span, carried so a client never has to parse `window`.", + "properties": { + "seconds": { + "maximum": 2147483647.0, + "minimum": 1.0, + "title": "Seconds", + "type": "integer" + }, + "totals": { + "$ref": "#/components/schemas/UsageTotals" + }, + "window": { + "$ref": "#/components/schemas/CliUsageWindowLabel" + } + }, + "required": [ + "window", + "seconds", + "totals" + ], + "title": "CliUsageWindow", + "type": "object" + }, + "CliUsageWindowLabel": { + "description": "The two windows `rcli usage` draws. Closed, because the endpoint offers\nno way to ask for a third: a client can switch on these and be exhaustive.", + "enum": [ + "1h", + "24h" + ], + "title": "CliUsageWindowLabel", + "type": "string" + }, + "CreditResponse": { + "additionalProperties": false, + "properties": { + "balance_micros": { + "maximum": 9007199254740991.0, + "minimum": -9007199254740991.0, + "title": "Balance Micros", + "type": "integer" + }, + "granted_micros": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Granted Micros", + "type": "integer" + }, + "spent_micros": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Spent Micros", + "type": "integer" + } + }, + "required": [ + "balance_micros", + "granted_micros", + "spent_micros" + ], + "title": "CreditResponse", + "type": "object" + }, + "GrantResponse": { + "additionalProperties": false, + "properties": { + "access_token": { + "maxLength": 8192, + "minLength": 1, + "title": "Access Token", + "type": "string" + }, + "email": { + "maxLength": 320, + "minLength": 3, + "pattern": "^[^\\s@]+@[^\\s@]+$", + "title": "Email", + "type": "string" + }, + "expires_in": { + "maximum": 2147483647.0, + "minimum": 1.0, + "title": "Expires In", + "type": "integer" + }, + "plan": { + "$ref": "#/components/schemas/CliPlan" + }, + "refresh_token": { + "maxLength": 8192, + "minLength": 1, + "title": "Refresh Token", + "type": "string" + } + }, + "required": [ + "access_token", + "refresh_token", + "email", + "plan", + "expires_in" + ], + "title": "GrantResponse", + "type": "object" + }, + "Harness": { + "enum": [ + "claude_code", + "opencode", + "hermes", + "cursor", + "continue", + "aider", + "cline", + "roo", + "zed", + "openai_sdk", + "langchain", + "llamaindex", + "curl", + "console", + "playground", + "rcli", + "sdk", + "unknown" + ], + "title": "Harness", + "type": "string" + }, + "IdentityResponse": { + "additionalProperties": false, + "properties": { + "email": { + "maxLength": 320, + "minLength": 3, + "pattern": "^[^\\s@]+@[^\\s@]+$", + "title": "Email", + "type": "string" + }, + "monthly_token_limit": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Monthly Token Limit", + "type": "integer" + }, + "plan": { + "$ref": "#/components/schemas/CliPlan" + }, + "tokens_this_month": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Tokens This Month", + "type": "integer" + } + }, + "required": [ + "email", + "plan", + "tokens_this_month", + "monthly_token_limit" + ], + "title": "IdentityResponse", + "type": "object" + }, + "PollResponse": { + "additionalProperties": false, + "properties": { + "access_token": { + "anyOf": [ + { + "maxLength": 8192, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Access Token" + }, + "email": { + "anyOf": [ + { + "maxLength": 320, + "minLength": 3, + "pattern": "^[^\\s@]+@[^\\s@]+$", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Email" + }, + "expires_in": { + "anyOf": [ + { + "maximum": 2147483647.0, + "minimum": 1.0, + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Expires In" + }, + "plan": { + "anyOf": [ + { + "$ref": "#/components/schemas/CliPlan" + }, + { + "type": "null" + } + ] + }, + "refresh_token": { + "anyOf": [ + { + "maxLength": 8192, + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Refresh Token" + }, + "status": { + "$ref": "#/components/schemas/PollStatus" + } + }, + "required": [ + "status" + ], + "title": "PollResponse", + "type": "object" + }, + "PollStatus": { + "enum": [ + "pending", + "approved", + "denied", + "expired" + ], + "title": "PollStatus", + "type": "string" + }, + "UsageModelRollup": { + "additionalProperties": false, + "properties": { + "cached_tokens": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Cached Tokens", + "type": "integer" + }, + "completion_tokens": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Completion Tokens", + "type": "integer" + }, + "cost_micros": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Cost Micros", + "type": "integer" + }, + "model": { + "maxLength": 128, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:/-]*$", + "title": "Model", + "type": "string" + }, + "prompt_tokens": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Prompt Tokens", + "type": "integer" + }, + "requests": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Requests", + "type": "integer" + } + }, + "required": [ + "model", + "requests", + "prompt_tokens", + "completion_tokens", + "cached_tokens", + "cost_micros" + ], + "title": "UsageModelRollup", + "type": "object" + }, + "UsageTimelinePoint": { + "additionalProperties": false, + "properties": { + "completion_tokens": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Completion Tokens", + "type": "integer" + }, + "cost_micros": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Cost Micros", + "type": "integer" + }, + "date": { + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "title": "Date", + "type": "string" + }, + "prompt_tokens": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Prompt Tokens", + "type": "integer" + }, + "requests": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Requests", + "type": "integer" + } + }, + "required": [ + "date", + "requests", + "prompt_tokens", + "completion_tokens", + "cost_micros" + ], + "title": "UsageTimelinePoint", + "type": "object" + }, + "UsageTotals": { + "additionalProperties": false, + "properties": { + "cached_tokens": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Cached Tokens", + "type": "integer" + }, + "completion_tokens": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Completion Tokens", + "type": "integer" + }, + "cost_micros": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Cost Micros", + "type": "integer" + }, + "prompt_tokens": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Prompt Tokens", + "type": "integer" + }, + "requests": { + "maximum": 9007199254740991.0, + "minimum": 0.0, + "title": "Requests", + "type": "integer" + } + }, + "required": [ + "requests", + "prompt_tokens", + "completion_tokens", + "cached_tokens", + "cost_micros" + ], + "title": "UsageTotals", + "type": "object" + } + } + }, + "info": { + "description": "CLI-facing operations extracted from control-plane-v1.openapi.json. Generated by contracts/extract-cli-contract.py; do not hand-edit.", + "title": "Wally CLI control-plane contract (extract)", + "version": "0.1.0" + }, + "openapi": "3.1.0", + "paths": { + "/auth/cli/poll": { + "post": { + "operationId": "pollCliAuthorization", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CliPollRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PollResponse" + } + } + }, + "description": "Successful Response" + }, + "422": { + "$ref": "#/components/responses/ValidationFailed" + }, + "default": { + "$ref": "#/components/responses/ApiError" + } + }, + "summary": "Poll", + "tags": [ + "cli auth" + ] + } + }, + "/auth/cli/refresh": { + "post": { + "operationId": "refreshCliAuthorization", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CliRefreshRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GrantResponse" + } + } + }, + "description": "Successful Response" + }, + "422": { + "$ref": "#/components/responses/ValidationFailed" + }, + "default": { + "$ref": "#/components/responses/ApiError" + } + }, + "summary": "Refresh", + "tags": [ + "cli auth" + ] + } + }, + "/auth/cli/revoke": { + "post": { + "description": "Invalidate both halves of an RCLI login.\n\nThe response is always 204, including for an unknown or already-revoked\ncredential. Logout is safe to retry after a lost response, and callers do\nnot get a token-validity oracle.", + "operationId": "revokeCliAuthorization", + "parameters": [ + { + "$ref": "#/components/parameters/RevokeCliAuthorizationHeaderAuthorizationParameter" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CliRefreshRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "$ref": "#/components/responses/ValidationFailed" + }, + "default": { + "$ref": "#/components/responses/ApiError" + } + }, + "summary": "Revoke", + "tags": [ + "cli auth" + ] + } + }, + "/auth/cli/start": { + "post": { + "description": "Open an authorization request. Unauthenticated by design: the terminal\nhas no credential yet, which is the reason this flow exists.\n\nBeing unauthenticated is also why it is rate limited. Nothing here costs a\ncustomer anything, but a row is written per call and the endpoint is public.", + "operationId": "startCliAuthorization", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CliStartRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CliStartResponse" + } + } + }, + "description": "Successful Response" + }, + "422": { + "$ref": "#/components/responses/ValidationFailed" + }, + "default": { + "$ref": "#/components/responses/ApiError" + } + }, + "summary": "Start", + "tags": [ + "cli auth" + ] + } + }, + "/v1/cli/usage": { + "get": { + "description": "One read: the balance, the window's totals, and how they were spent.\n\nA terminal draws its whole report in one go, so splitting this across three\nround-trips would only give the CLI three chances to render a report whose\nparts disagree about when they were taken.", + "operationId": "getCliUsage", + "parameters": [ + { + "$ref": "#/components/parameters/GetCliUsageQueryDaysParameter" + }, + { + "$ref": "#/components/parameters/GetCliUsageQueryModelParameter" + }, + { + "$ref": "#/components/parameters/GetCliUsageQueryLimitParameter" + }, + { + "$ref": "#/components/parameters/GetCliUsageHeaderAuthorizationParameter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CliUsageResponse" + } + } + }, + "description": "Successful Response" + }, + "422": { + "$ref": "#/components/responses/ValidationFailed" + }, + "default": { + "$ref": "#/components/responses/ApiError" + } + }, + "summary": "Get Usage", + "tags": [ + "cli" + ] + } + }, + "/v1/me": { + "get": { + "description": "`rcli whoami`. Authenticated by the API key the CLI was issued, because\nthat is the only credential it has.", + "operationId": "getCurrentIdentity", + "parameters": [ + { + "$ref": "#/components/parameters/GetCurrentIdentityHeaderAuthorizationParameter" + }, + { + "$ref": "#/components/parameters/GetCurrentIdentityHeaderXUserIdParameter" + }, + { + "$ref": "#/components/parameters/GetCurrentIdentityHeaderXUserEmailParameter" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityResponse" + } + } + }, + "description": "Successful Response" + }, + "422": { + "$ref": "#/components/responses/ValidationFailed" + }, + "default": { + "$ref": "#/components/responses/ApiError" + } + }, + "summary": "Me", + "tags": [ + "cli auth" + ] + } + } + } +} diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 4c8d6ca..e90086d 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -1,6 +1,6 @@ # RunAnywhere CLI — Releasing -Contributor and release-engineering guide for shipping `rcli` binaries. +Contributor and release-engineering guide for shipping `wally` binaries. ## Control plane @@ -22,7 +22,7 @@ Two SDK environments: # OSS keyless blast → staging backend (PUBLIC org) # Unset ambient RUNANYWHERE_API_KEY or an invalid key will force a failed JWT login. $ unset RUNANYWHERE_API_KEY RUNANYWHERE_BASE_URL -$ rcli --environment development \ +$ wally --environment development \ --base-url "$STAGING_BASE_URL" \ telemetry blast --processing-ms 42.5 # Release builds can omit --base-url (baked STAGING_BASE_URL). @@ -30,11 +30,11 @@ $ rcli --environment development \ # STAGING_BASE_URL=… ./scripts/ci/oss_keyless_telemetry_blast.sh # Team / customer authed path -$ rcli --environment production \ +$ wally --environment production \ --base-url https://api.example.com \ --api-key $KEY auth login -$ rcli --environment production \ +$ wally --environment production \ --base-url https://api.example.com \ --api-key $KEY telemetry blast MODALITY RESULT STATUS RECEIVED STORED SKIPPED @@ -45,29 +45,45 @@ llm ok HTTP 200 1 1 0 - `auth login` runs the authenticated handshake (`/api/v1/auth/sdk/authenticate` → `/api/v1/devices/register` → model assignments). Production only. - `telemetry emit|blast` drive the real commons telemetry pipeline to `/api/v2/sdk/telemetry/{modality}`. Development is keyless (no JWT). Production logs in first. Modalities: `llm stt tts vlm rag imagegen embeddings vad voice lora model system`. Exit is non-zero when any POST fails or any tracked event never reached the backend. +## Versions live in one file + +`versions.toml` at the repo root is the single source for every version and pin. +CMake reads it directly, so `project(wally VERSION …)` and the SDK/IDL/kit pins +in `cmake/sdk-pin.cmake` come from it and nowhere else. `scripts/build/package-wally.sh` +and `auto-tag.yml` read the product version from it too. + +To cut a release, bump `[product] version` there and nothing else. The copies +that cannot read TOML at their own step, the Homebrew formula's version and the +Swift package's exact SDK pin, are checked against it by +`scripts/ci/check-versions.py`, which the CI distribution job and the local sim +both run, so a half-done bump fails rather than ships. The formula's per-platform +`sha256` lines are still stamped from the real release by +`scripts/release/update-tap.sh` / `stamp-formula.py`. + ## Published asset contract -The current `release.yml` publishes two archives and matching SHA-256 +The current `release.yml` publishes three archives and matching SHA-256 sidecars. It does not build or advertise a Linux release: | Platform | Asset | Required archive root | |---|---|---| -| macOS Apple Silicon | `rcli-X.Y.Z-macos-arm64.tar.gz` | `rcli-macos-arm64/` | -| Windows x86_64 | `rcli-X.Y.Z-windows-x86_64.zip` | `rcli-windows-x86_64/` | +| macOS Apple Silicon | `wally-X.Y.Z-macos-arm64.tar.gz` | `wally-macos-arm64/` | +| Windows ARM64 | `wally-X.Y.Z-windows-arm64.zip` | `wally-windows-arm64/` | +| Windows x86_64 | `wally-X.Y.Z-windows-x86_64.zip` | `wally-windows-x86_64/` | -Each root contains a non-empty `README.md` and `bin/rcli` or `bin/rcli.exe`. -Windows DLLs stay beside `bin/rcli.exe`. The macOS archive contains the Swift -MLX host and its resource bundles. `scripts/verify-release-assets.py` verifies +Each root contains a non-empty `README.md` and `bin/wally` or `bin/wally.exe`. +Windows DLLs stay beside `bin/wally.exe`. The macOS archive contains the Swift +MLX host and its resource bundles. `scripts/release/verify-release-assets.py` verifies the sidecar digest, filename, single-root layout, required files, executable mode, duplicate paths, traversal, links, and expansion limits. Packaging jobs and the publish job all run it before a release is created. ## Signing reality and production gates -Credential-free macOS packaging is ad-hoc signed. `scripts/package-rcli.sh` +Credential-free macOS packaging is ad-hoc signed. `scripts/build/package-wally.sh` checks that signature and can sign with an already-installed Developer ID -identity via `RCLI_CODESIGN_IDENTITY` and optional `RCLI_CODESIGN_KEYCHAIN`. -Set `RCLI_REQUIRE_DEVELOPER_ID=1` to make ad-hoc signing an error. The GitHub +identity via `WALLY_CODESIGN_IDENTITY` and optional `WALLY_CODESIGN_KEYCHAIN`. +Set `WALLY_REQUIRE_DEVELOPER_ID=1` to make ad-hoc signing an error. The GitHub workflow does **not** currently import an identity, notarize an archive, create a DMG, or staple a ticket. @@ -79,7 +95,7 @@ Therefore these are launch gates, not completed workflow features: - import a Developer ID Application identity into an ephemeral keychain, sign nested code and the host with hardened runtime/timestamp, notarize the exact distributed artifact, and validate Gatekeeper acceptance; -- Authenticode-sign `rcli.exe` and DLLs as required, then validate signatures on +- Authenticode-sign `wally.exe` and DLLs as required, then validate signatures on a clean Windows host; - provide the signing/notarization credentials through protected release environments and keep pull-request jobs credential-free. @@ -94,13 +110,14 @@ on macOS, and `Get-AuthenticodeSignature` on Windows. - `ci.yml` builds and tests macOS and Windows. Its distribution job also tests archive verification, shell syntax, formula syntax, and stamping code. - `release.yml` builds macOS and Windows, runs product e2e, packages, verifies - each archive twice, then publishes the two archives and sidecars. -- The publish job generates a `rcli-homebrew-formula` workflow artifact from + each archive twice, then publishes the three archives and sidecars. +- The publish job generates a `wally-homebrew-formula` workflow artifact from the verified macOS checksum. It does not pretend that an ephemeral checkout updated a default branch. -Homebrew still needs one ownership decision: `install.sh` taps the RCLI repo as -`runanywhereai/rcli`, while the historical update script targeted a separate -`homebrew-tap` repo. Until one is declared canonical, pass `RCLI_TAP_REPO` -explicitly to `scripts/update-tap.sh` and apply the generated formula to the +Homebrew still needs one ownership decision: `install.sh` taps the wally repo +itself under the alias `runanywhereai/wally`, while +the historical update script targeted a separate `homebrew-tap` repo. Until one +is declared canonical, pass `WALLY_TAP_REPO` +explicitly to `scripts/release/update-tap.sh` and apply the generated formula to the same tap users install from. diff --git a/docs/VERIFICATION.md b/docs/VERIFICATION.md index 0022eab..99cc23b 100644 --- a/docs/VERIFICATION.md +++ b/docs/VERIFICATION.md @@ -1,4 +1,4 @@ -# Local verification (kit → rcli) +# Local verification (kit → wally) Record pass/fail per OS. Do not treat a screenshot as a pass. @@ -10,28 +10,28 @@ Record pass/fail per OS. Do not treat a screenshot as a pass. # staged prefix: dist/cpp-desktop-macos-arm64 ``` -Kit must contain `include/rac/**`, `lib/librac_commons.a` (or equivalent), `lib/cmake/RunAnywhere/RunAnywhereConfig.cmake`, `share/runanywhere/idl/*.proto`. No `rcli` binary. +Kit must contain `include/rac/**`, `lib/librac_commons.a` (or equivalent), `lib/cmake/RunAnywhere/RunAnywhereConfig.cmake`, `share/runanywhere/idl/*.proto`. No `wally` binary. -## RCLI (this repo) +## Wally (this repo) ```bash cmake -B build -DCMAKE_PREFIX_PATH= -DCMAKE_BUILD_TYPE=Release cmake --build build -j "$(sysctl -n hw.logicalcpu)" -# Apple product binary is build/rcli (Swift MLX host). Windows: build/rcli.exe +# Apple product binary is build/wally (Swift MLX host). Windows: build/wally.exe ``` -`RCLI_SDK_DIR` pointing at SDK **source** must configure-fail. +`WALLY_SDK_DIR` pointing at SDK **source** must configure-fail. ## Matrix | Check | macOS arm64 | Windows x64 | | --- | --- | --- | | `--version` prints product + kit pin | | | -| `--help` / no-TTY bare `rcli` | | | -| TTY bare `rcli` → REPL | | | +| `--help` / no-TTY bare `wally` | | | +| TTY bare `wally` → REPL | | | | `backends` / `engines` (no stubs for missing packs) | | | | LLM generate + stream (llama.cpp) | | | -| MLX listed by `rcli backends` (same binary) | yes | n/a | +| MLX listed by `wally backends` (same binary) | yes | n/a | | NeuRT (only with pack + `NEURUN_TOKEN`) | skip | | | STT / TTS / VAD (Sherpa) | | | | embed / rerank | | | diff --git a/docs/assets/wally.gif b/docs/assets/wally.gif new file mode 100644 index 0000000..6f9f28f Binary files /dev/null and b/docs/assets/wally.gif differ diff --git a/include/rcli_host.h b/include/rcli_host.h deleted file mode 100644 index c761752..0000000 --- a/include/rcli_host.h +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef RCLI_HOST_H -#define RCLI_HOST_H - -#ifdef __cplusplus -extern "C" { -#endif - -int rcli_run_main(int argc, char** argv); - -#ifdef __cplusplus -} -#endif - -#endif // RCLI_HOST_H diff --git a/include/wally_host.h b/include/wally_host.h new file mode 100644 index 0000000..b81525a --- /dev/null +++ b/include/wally_host.h @@ -0,0 +1,14 @@ +#ifndef WALLY_HOST_H +#define WALLY_HOST_H + +#ifdef __cplusplus +extern "C" { +#endif + +int wally_run_main(int argc, char** argv); + +#ifdef __cplusplus +} +#endif + +#endif // WALLY_HOST_H diff --git a/install.ps1 b/install.ps1 index a55999d..8539704 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,10 +1,10 @@ $ErrorActionPreference = 'Stop' -$Repo = 'RunanywhereAI/RCLI' +$Repo = 'RunanywhereAI/wally' # There is no Homebrew here, so this installer does the whole job itself rather # than handing off to a package manager: download the release zip, check it, # unpack it, put it on PATH. -$InstallDir = Join-Path $env:LOCALAPPDATA 'Programs\rcli' +$InstallDir = Join-Path $env:LOCALAPPDATA 'Programs\wally' # Windows PowerShell picks the older protocols on some builds and api.github.com # refuses anything below TLS 1.2. @@ -31,7 +31,7 @@ function Fail([string]$Message) { throw "Error: $Message" } -Write-Info 'Checking latest RCLI release...' +Write-Info 'Checking latest Wally release...' try { $Release = Invoke-RestMethod "https://api.github.com/repos/$Repo/releases/latest" } catch { @@ -45,16 +45,16 @@ Write-Info "Latest version: v$Version" # under WOW64 says x86 while ARCHITEW6432 names what is really underneath. $Arch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } # Prism on Windows ARM64 runs the x64 zip. Native arm64 zips are preferred when present. -$AssetName = "rcli-$Version-windows-x86_64.zip" +$AssetName = "wally-$Version-windows-x86_64.zip" if ($Arch -eq 'ARM64') { - $ArmAsset = $Release.assets | Where-Object { $_.name -eq "rcli-$Version-windows-arm64.zip" } | Select-Object -First 1 + $ArmAsset = $Release.assets | Where-Object { $_.name -eq "wally-$Version-windows-arm64.zip" } | Select-Object -First 1 if ($ArmAsset) { - $AssetName = "rcli-$Version-windows-arm64.zip" + $AssetName = "wally-$Version-windows-arm64.zip" } else { Write-Warn "No native ARM64 zip; installing the x64 build (Windows on ARM can run it)." } } elseif ($Arch -ne 'AMD64') { - Fail "RCLI requires 64-bit Windows. Detected: $Arch" + Fail "Wally requires 64-bit Windows. Detected: $Arch" } $Asset = $Release.assets | Where-Object { $_.name -eq $AssetName } | Select-Object -First 1 if (-not $Asset) { @@ -65,7 +65,7 @@ if (-not $ShaAsset) { Fail "v$Version does not publish $AssetName.sha256. Refusing an unverified download." } -$Temp = Join-Path ([IO.Path]::GetTempPath()) ('rcli-' + [Guid]::NewGuid().ToString('N')) +$Temp = Join-Path ([IO.Path]::GetTempPath()) ('wally-' + [Guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $Temp -Force | Out-Null try { $Zip = Join-Path $Temp $AssetName @@ -95,7 +95,7 @@ try { } Write-Ok 'Checksum verified' - Write-Info "Installing RCLI v$Version to $InstallDir..." + Write-Info "Installing Wally v$Version to $InstallDir..." Expand-Archive -LiteralPath $Zip -DestinationPath $Temp -Force $Stem = [IO.Path]::GetFileNameWithoutExtension($AssetName) $Unpacked = Join-Path $Temp "$Stem\bin" @@ -104,21 +104,21 @@ try { } # Validate a complete candidate before replacing a working installation. - # rcli.exe and its DLLs stay together exactly as they are in archive bin/. + # wally.exe and its DLLs stay together exactly as they are in archive bin/. $Candidate = Join-Path $Temp 'install-candidate' New-Item -ItemType Directory -Path $Candidate -Force | Out-Null Copy-Item -Path (Join-Path $Unpacked '*') -Destination $Candidate -Recurse -Force - $CandidateExe = Join-Path $Candidate 'rcli.exe' + $CandidateExe = Join-Path $Candidate 'wally.exe' if (-not (Test-Path -LiteralPath $CandidateExe)) { - Fail "$AssetName is missing bin\rcli.exe." + Fail "$AssetName is missing bin\wally.exe." } $VersionOutput = @(& $CandidateExe --version 2>&1) if ($LASTEXITCODE -ne 0) { - Fail 'The downloaded rcli.exe does not run; the existing installation was left unchanged.' + Fail 'The downloaded wally.exe does not run; the existing installation was left unchanged.' } $EscapedVersion = [Regex]::Escape($Version) - if (($VersionOutput -join "`n") -notmatch "(?m)^rcli\s+$EscapedVersion(?:\s|$)") { - Fail "The downloaded executable does not report RCLI v$Version; the existing installation was left unchanged." + if (($VersionOutput -join "`n") -notmatch "(?m)^wally\s+$EscapedVersion(?:\s|$)") { + Fail "The downloaded executable does not report Wally v$Version; the existing installation was left unchanged." } $InstallParent = Split-Path $InstallDir -Parent @@ -141,10 +141,10 @@ try { Remove-Item -LiteralPath $Temp -Recurse -Force -ErrorAction SilentlyContinue } -$Exe = Join-Path $InstallDir 'rcli.exe' -if (-not (Test-Path -LiteralPath $Exe)) { Fail "Installation failed. rcli.exe is not in $InstallDir." } +$Exe = Join-Path $InstallDir 'wally.exe' +if (-not (Test-Path -LiteralPath $Exe)) { Fail "Installation failed. wally.exe is not in $InstallDir." } & $Exe --version | Out-Null -if ($LASTEXITCODE -ne 0) { Fail "Installation failed. rcli.exe is installed but does not run." } +if ($LASTEXITCODE -ne 0) { Fail "Installation failed. wally.exe is installed but does not run." } # The user's own PATH, never the machine's: this installs under LOCALAPPDATA for # one account and needs no administrator to do it. @@ -158,14 +158,14 @@ if ($Entries -contains $InstallDir) { Write-Ok "Added $InstallDir to your PATH" } -Write-Ok "RCLI v$Version installed successfully" +Write-Ok "Wally v$Version installed successfully" Write-Host '' -Write-Warn 'Open a new terminal before running rcli. This one was started with the old PATH.' +Write-Warn 'Open a new terminal before running wally. This one was started with the old PATH.' Write-Host '' Write-Info 'Getting started:' -Write-Host ' rcli list --all every model in the catalog' -Write-Host ' rcli pull qwen3-0.6b download one' -Write-Host ' rcli run qwen3-0.6b talk to it, /? for commands' -Write-Host ' rcli backends which engines this build linked' +Write-Host ' wally list --all every model in the catalog' +Write-Host ' wally pull qwen3-0.6b download one' +Write-Host ' wally run qwen3-0.6b talk to it, /? for commands' +Write-Host ' wally backends which engines this build linked' Write-Host '' Write-Host ' Models download on demand into %LOCALAPPDATA%\RunAnywhere' diff --git a/install.sh b/install.sh index f510a56..45605b2 100755 --- a/install.sh +++ b/install.sh @@ -1,19 +1,46 @@ #!/usr/bin/env bash set -euo pipefail -REPO="RunanywhereAI/RCLI" -TAP="RunanywhereAI/rcli" +# Repo, binary, formula and tap are all wally. The tap alias below is fully +# qualified on purpose (see FORMULA). +REPO="RunanywhereAI/wally" +TAP="RunanywhereAI/wally" # Fully qualified on purpose. runanywhereai/tap also provides a formula called -# rcli, and a bare `brew install rcli` on a machine with both taps fails with +# wally, and a bare `brew install wally` on a machine with both taps fails with # "Formulae found in multiple taps" rather than picking one. -FORMULA="runanywhereai/rcli/rcli" +FORMULA="runanywhereai/wally/wally" info() { printf "\033[1;34m==>\033[0m \033[1m%s\033[0m\n" "$*"; } ok() { printf "\033[1;32m==>\033[0m %s\n" "$*"; } warn() { printf "\033[1;33mWarning:\033[0m %s\n" "$*"; } fail() { printf "\033[1;31mError:\033[0m %s\n" "$*" >&2; exit 1; } -info "Checking latest RCLI release..." +# Which agent homes get the skill. Claude Code reads ~/.claude/skills; Cursor, +# Codex and other AGENTS.md tools read ~/.agents/skills. Install into every home +# the person already has, so a Codex-only user is not handed a skill their agent +# never reads. A fresh machine with neither is a Claude-first get-started, so it +# defaults to ~/.claude. One dir per line; callers set IFS=newline to be safe +# with a $HOME that contains spaces. +skill_target_dirs() { + targets="" + [ -d "${HOME}/.claude" ] && targets="${targets}${HOME}/.claude/skills/runanywhere +" + [ -d "${HOME}/.agents" ] && targets="${targets}${HOME}/.agents/skills/runanywhere +" + [ -n "${targets}" ] || targets="${HOME}/.claude/skills/runanywhere +" + printf '%s' "${targets}" +} + +# Debug-only: print the resolved skill targets and exit before any network or +# brew work. Exercised by scripts/test/test-install-skill-dirs.sh. Not part of +# the user-facing flow. +if [ "${1:-}" = "--print-skill-dirs" ]; then + skill_target_dirs + exit 0 +fi + +info "Checking latest Wally release..." VERSION=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \ | grep '"tag_name"' \ | sed 's/.*"v\([^"]*\)".*/\1/') @@ -26,9 +53,9 @@ case "${os}/${arch}" in # MLX is Metal and NeuRT is the Apple Neural Engine, so an Intel Mac would # get neither and there is no build for it. No Linux release is published. Darwin/arm64) ;; - Darwin/*) fail "RCLI needs an Apple Silicon Mac. Detected: ${arch}" ;; - Linux/*) fail "RCLI does not currently publish a Linux binary. Build from source: https://github.com/${REPO}#build-from-source" ;; - *) fail "RCLI has no build for ${os}. On Windows, use install.ps1." ;; + Darwin/*) fail "Wally needs an Apple Silicon Mac. Detected: ${arch}" ;; + Linux/*) fail "Wally does not currently publish a Linux binary. Build from source: https://github.com/${REPO}#build-from-source" ;; + *) fail "Wally has no build for ${os}. On Windows, use install.ps1." ;; esac if ! command -v brew &>/dev/null; then @@ -51,13 +78,13 @@ brew tap "$TAP" "https://github.com/$REPO.git" 2>/dev/null || true # Force-update the tap so Homebrew sees the latest formula brew update --force 2>/dev/null || true -info "Installing RCLI v${VERSION}..." +info "Installing Wally v${VERSION}..." if brew upgrade "$FORMULA" 2>/dev/null || brew install "$FORMULA" 2>/dev/null; then ok "Installed via Homebrew" else # There used to be a fallback here that unpacked the tarball into the Cellar - # by hand. Homebrew is the only supported path now: the formula puts rcli and - # mlx-swift_Cmlx.bundle in libexec and symlinks bin/rcli at the binary, and + # by hand. Homebrew is the only supported path now: the formula puts wally and + # mlx-swift_Cmlx.bundle in libexec and symlinks bin/wally at the binary, and # if the bundle does not end up beside the executable MLX quietly reports # itself unavailable while the other five engines carry on. That is not a # failure worth risking in an installer that cannot test for it. @@ -72,18 +99,18 @@ else fail "Could not install $FORMULA. If it still fails, open an issue at https://github.com/$REPO/issues with the output above." fi -if ! command -v rcli &>/dev/null; then - fail "Installation failed. rcli not found in PATH." +if ! command -v wally &>/dev/null; then + fail "Installation failed. wally not found in PATH." fi -installed_version="$(rcli --version 2>/dev/null \ - | sed -nE 's/^rcli ([0-9]+\.[0-9]+\.[0-9]+).*/\1/p' \ +installed_version="$(wally --version 2>/dev/null \ + | sed -nE 's/^wally ([0-9]+\.[0-9]+\.[0-9]+).*/\1/p' \ | head -1)" if [[ "${installed_version}" != "${VERSION}" ]]; then - fail "Homebrew installed RCLI v${installed_version:-unknown}, but GitHub's latest release is v${VERSION}. The tap formula must be updated before this installer can claim success." + fail "Homebrew installed Wally v${installed_version:-unknown}, but GitHub's latest release is v${VERSION}. The tap formula must be updated before this installer can claim success." fi -ok "RCLI v${VERSION} installed successfully" +ok "Wally v${VERSION} installed successfully" # The skill is what makes the next step self-explanatory in Claude Code: it # teaches the assistant the commands, the harnesses, and what to do when one is @@ -94,40 +121,50 @@ ok "RCLI v${VERSION} installed successfully" # instructions when the skill runs, so fetching it off a moving branch means a # push to main changes what an already-installed assistant does. The tag is the # same one the binary above came from, so the two cannot drift apart either. -SKILL_DIR="${HOME}/.claude/skills/runanywhere" -info "Installing the RunAnywhere skill for Claude Code..." -if mkdir -p "$SKILL_DIR" 2>/dev/null && - curl -fsSL "https://raw.githubusercontent.com/${REPO}/v${VERSION}/skills/runanywhere/SKILL.md" \ - -o "${SKILL_DIR}/SKILL.md"; then - ok "Skill installed at ${SKILL_DIR}/SKILL.md" -else - warn "Could not install the Claude skill. Everything else still works." -fi +SKILL_URL="https://raw.githubusercontent.com/${REPO}/v${VERSION}/skills/runanywhere/SKILL.md" +info "Installing the RunAnywhere skill for your coding agent..." +skill_installed=0 +old_ifs="$IFS" +IFS=' +' +for skill_dir in $(skill_target_dirs); do + IFS="$old_ifs" + if mkdir -p "$skill_dir" 2>/dev/null && curl -fsSL "$SKILL_URL" -o "${skill_dir}/SKILL.md"; then + ok "Skill installed at ${skill_dir}/SKILL.md" + skill_installed=1 + else + warn "Could not install the skill at ${skill_dir}." + fi + IFS=' +' +done +IFS="$old_ifs" +[ "$skill_installed" -eq 1 ] || warn "Could not install the RunAnywhere skill. Everything else still works." # Signing in is the point of the whole flow, so it happens here rather than # being left as an instruction the person has to notice. Already signed in is a # no-op, and a failure is not fatal: the CLI is installed either way. echo "" -if rcli whoami >/dev/null 2>&1; then +if wally whoami >/dev/null 2>&1; then ok "Already signed in" elif [[ ! -t 0 || ! -t 1 ]]; then # No terminal: piped into bash over SSH, or a CI step. The browser flow # would try to open a browser that is not there and then block until the # request expires, which reads as the installer hanging. info "Not an interactive terminal, so sign-in is left to you." - echo " rcli login sign in from a machine with a browser" - echo " rcli login --no-browser print the URL and approve it elsewhere" + echo " wally login sign in from a machine with a browser" + echo " wally login --no-browser print the URL and approve it elsewhere" else info "Opening the console to sign in..." - rcli login || warn "Sign-in did not finish. Run \`rcli login\` when you are ready." + wally login || warn "Sign-in did not finish. Run \`wally login\` when you are ready." fi echo "" info "Getting started:" -echo " rcli opencode --cloud -m glm-5.3 code against a hosted model" -echo " rcli usage credit left and what you spent" -echo " rcli pull qwen3-0.6b download a model to this machine" -echo " rcli run qwen3-0.6b talk to it, offline" +echo " wally opencode --cloud -m glm-5.3 code against a hosted model" +echo " wally usage credit left and what you spent" +echo " wally pull qwen3-0.6b download a model to this machine" +echo " wally run qwen3-0.6b talk to it, offline" echo "" echo " Models download on demand into ~/.local/share/runanywhere" echo " In Claude Code, ask: \"get me started with RunAnywhere\"" diff --git a/packaging/homebrew/rcli.rb.in b/packaging/homebrew/rcli.rb.in deleted file mode 100644 index a5ffff5..0000000 --- a/packaging/homebrew/rcli.rb.in +++ /dev/null @@ -1,52 +0,0 @@ -# rcli Homebrew formula template. -# -# Rendered by rcli/scripts/update-tap.sh after a GitHub release -# publishes the platform tarballs: @VERSION@, @SHA256_MACOS_ARM64@ and -# @SHA256_LINUX_X86_64@ are substituted from the release's .sha256 sidecars, -# then the result is committed to the RunanywhereAI/homebrew-tap repository as -# Formula/rcli.rb (`brew install runanywhereai/tap/rcli`). -class Rcli < Formula - desc "RunAnywhere on-device AI CLI — run, manage and serve local models" - homepage "https://github.com/RunanywhereAI/runanywhere-sdks" - version "@VERSION@" - license "MIT" - - on_macos do - on_arm do - url "https://github.com/RunanywhereAI/runanywhere-sdks/releases/download/v@VERSION@/rcli-macos-arm64-v@VERSION@.tar.gz" - sha256 "@SHA256_MACOS_ARM64@" - end - end - - on_linux do - on_intel do - url "https://github.com/RunanywhereAI/runanywhere-sdks/releases/download/v@VERSION@/rcli-linux-x86_64-v@VERSION@.tar.gz" - sha256 "@SHA256_LINUX_X86_64@" - end - depends_on "curl" - end - - def install - # Keep the executable colocated with its MLX metallib and SwiftPM resource - # bundles. The Linux archive contains only rcli here, so the same layout is - # harmless across both platforms. - libexec.install Dir["bin/*"] - bin.install_symlink libexec/"rcli" - lib.install Dir["lib/*"] unless Dir["lib/*"].empty? - doc.install "README.md" - end - - def caveats - <<~EOS - Models are stored under ~/.local/share/runanywhere (override with - RUNANYWHERE_HOME). Get started: - rcli list --all - rcli run qwen3 - EOS - end - - test do - assert_match version.to_s, shell_output("#{bin}/rcli version") - system bin/"rcli", "backends" - end -end diff --git a/scripts/build-mlx.sh b/scripts/build/build-mlx.sh similarity index 59% rename from scripts/build-mlx.sh rename to scripts/build/build-mlx.sh index d97902f..ec7c56e 100755 --- a/scripts/build-mlx.sh +++ b/scripts/build/build-mlx.sh @@ -1,44 +1,44 @@ #!/usr/bin/env bash -# Apple shipping binary: CMake `rcli-cxx` objects + Swift MLX host → build/rcli. +# Apple shipping binary: CMake `wally-cxx` objects + Swift MLX host → build/wally. # -# scripts/build-mlx.sh [build-dir] +# scripts/build/build-mlx.sh [build-dir] # # Requires: -# - cmake already built the rcli target (rcli-cxx + link.txt) -# - a kit prefix on CMAKE_PREFIX_PATH / RCLI_SDK_KIT (public headers) +# - cmake already built the wally target (wally-cxx + link.txt) +# - a kit prefix on CMAKE_PREFIX_PATH / WALLY_SDK_KIT (public headers) # - Xcode (xcodebuild compiles MLX Metal shaders; `swift build` cannot) set -euo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" BUILD="${1:-${ROOT}/build}" -KIT="${RCLI_SDK_KIT:-${CMAKE_PREFIX_PATH:-}}" +KIT="${WALLY_SDK_KIT:-${CMAKE_PREFIX_PATH:-}}" KIT="${KIT%%:*}" if [[ -z "${KIT}" || ! -d "${KIT}/include" ]]; then - echo "error: set RCLI_SDK_KIT to a staged C++ desktop kit prefix (include/rac)" >&2 + echo "error: set WALLY_SDK_KIT to a staged C++ desktop kit prefix (include/rac)" >&2 exit 1 fi -"${ROOT}/scripts/bundle-core.sh" "${BUILD}" +"${ROOT}/scripts/build/bundle-core.sh" "${BUILD}" flags=() while IFS= read -r entry; do [[ -n "${entry}" ]] || continue flags+=("${entry}") -done < "${BUILD}/rcli-link-flags.txt" +done < "${BUILD}/wally-link-flags.txt" # The published runanywhere-swift tarball does not export RunAnywhereMLXRuntime -# (Swift MLX without a second commons archive). Apple rcli therefore needs the -# SDK source tree: nested monorepo, or RCLI_SDK_SWIFT_PATH in CI. -if [[ -z "${RCLI_SDK_SWIFT_PATH:-}" && -f "${ROOT}/../../Package.swift" ]]; then +# (Swift MLX without a second commons archive). Apple wally therefore needs the +# SDK source tree: nested monorepo, or WALLY_SDK_SWIFT_PATH in CI. +if [[ -z "${WALLY_SDK_SWIFT_PATH:-}" && -f "${ROOT}/../../Package.swift" ]]; then # Canonicalize: SwiftPM's local package identity is the last path # component, so a trailing `/../..` would register the package as `..`. - export RCLI_SDK_SWIFT_PATH="$(cd "${ROOT}/../.." && pwd)" + export WALLY_SDK_SWIFT_PATH="$(cd "${ROOT}/../.." && pwd)" fi -if [[ -z "${RCLI_SDK_SWIFT_PATH:-}" || ! -f "${RCLI_SDK_SWIFT_PATH}/Package.swift" ]]; then - echo "error: Apple rcli needs the SDK Swift tree (RunAnywhereMLXRuntime)." >&2 - echo " export RCLI_SDK_SWIFT_PATH=/path/to/runanywhere-sdks" >&2 - echo " or build from EXTERNAL/RCLI inside that monorepo." >&2 +if [[ -z "${WALLY_SDK_SWIFT_PATH:-}" || ! -f "${WALLY_SDK_SWIFT_PATH}/Package.swift" ]]; then + echo "error: Apple wally needs the SDK Swift tree (RunAnywhereMLXRuntime)." >&2 + echo " export WALLY_SDK_SWIFT_PATH=/path/to/runanywhere-sdks" >&2 + echo " or build from EXTERNAL/WALLY inside that monorepo." >&2 exit 1 fi @@ -50,18 +50,18 @@ xcode_log="${BUILD}/xcodebuild-mlx.log" # is not pulled (it references cpp-httplib methods the kit never emitted). # Comments must not sit in a `\` continuation — they cut the command in half. plugin_ldflags=() -if [[ -f "${BUILD}/librcli_plugins.a" ]]; then - plugin_ldflags+=("-Wl,-force_load,${BUILD}/librcli_plugins.a") +if [[ -f "${BUILD}/libwally_plugins.a" ]]; then + plugin_ldflags+=("-Wl,-force_load,${BUILD}/libwally_plugins.a") fi set +e RUNANYWHERE_BUILD_MLX_DISTRIBUTION_FRAMEWORK=1 \ xcodebuild build \ - -scheme rcli-mlx \ + -scheme wally-mlx \ -destination "platform=macOS,arch=$(uname -m)" \ -configuration Release \ -derivedDataPath .build/xcode \ HEADER_SEARCH_PATHS="\$(inherited) ${KIT}/include ${ROOT}/include" \ - OTHER_LDFLAGS="${plugin_ldflags[*]} -L${BUILD} -lrcli_bundle -lc++ ${flags[*]}" \ + OTHER_LDFLAGS="${plugin_ldflags[*]} -L${BUILD} -lwally_bundle -lc++ ${flags[*]}" \ >"${xcode_log}" 2>&1 xcodebuild_status=$? set -e @@ -76,9 +76,9 @@ fi grep -E "error: |warning: .*[Mm]etal|BUILD SUCCEEDED" "${xcode_log}" || true PRODUCTS="${ROOT}/swift/.build/xcode/Build/Products/Release" -[[ -x "${PRODUCTS}/RCLIMLX" ]] || { echo "the MLX build produced no binary" >&2; exit 1; } +[[ -x "${PRODUCTS}/WallyMLX" ]] || { echo "the MLX build produced no binary" >&2; exit 1; } -cp "${PRODUCTS}/RCLIMLX" "${BUILD}/rcli" +cp "${PRODUCTS}/WallyMLX" "${BUILD}/wally" # Metal shader bundles must sit next to the executable. Copy every .bundle # xcodebuild laid down (mlx-swift_Cmlx.bundle, mlx-swift_Cmlx.bundle, …). shopt -s nullglob @@ -88,4 +88,4 @@ for bundle in "${PRODUCTS}"/*.bundle; do cp -R "${bundle}" "${dest}" done shopt -u nullglob -echo "built ${BUILD}/rcli" +echo "built ${BUILD}/wally" diff --git a/scripts/bundle-core.sh b/scripts/build/bundle-core.sh similarity index 81% rename from scripts/bundle-core.sh rename to scripts/build/bundle-core.sh index 1ffbab7..84969d2 100755 --- a/scripts/bundle-core.sh +++ b/scripts/build/bundle-core.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Merges everything the rcli executable links into one static archive, and +# Merges everything the wally executable links into one static archive, and # writes out the system flags that are left over. # # The Swift build needs both: SwiftPM owns the final link for the MLX binary @@ -10,23 +10,31 @@ set -euo pipefail BUILD="${1:-build}" -LINK_TXT="${BUILD}/CMakeFiles/rcli.dir/link.txt" -OUT_LIB="${BUILD}/librcli_bundle.a" -OUT_FLAGS="${BUILD}/rcli-link-flags.txt" +LINK_TXT="${BUILD}/CMakeFiles/wally.dir/link.txt" +OUT_LIB="${BUILD}/libwally_bundle.a" +OUT_FLAGS="${BUILD}/wally-link-flags.txt" # Ninja never writes CMakeFiles/.dir/link.txt (Makefiles only). The last -# command ninja runs for `rcli` is the link; compile lines also contain -# `CMakeFiles/rcli.dir/` so grepping for "rcli" picks compiles. -if [[ ! -s "${LINK_TXT}" && -f "${BUILD}/build.ninja" ]]; then - mkdir -p "${BUILD}/CMakeFiles/rcli.dir" +# command ninja runs for `wally` is the link; compile lines also contain +# `CMakeFiles/wally.dir/` so grepping for "wally" picks compiles. +# +# Always re-harvest here, rather than only when LINK_TXT is missing: unlike +# Makefiles (where this same path is CMake's own file, rewritten by `make` +# every build), for Ninja this path is a cache this script wrote by hand. A +# stale copy from before a private overlay (NeuRT, QHexRT, ...) was applied +# silently drops that overlay's archives from the merge — the build still +# succeeds, it just links a bundle missing the registrar. Re-harvesting is +# one cheap `ninja -t commands` query, not a rebuild. +if [[ -f "${BUILD}/build.ninja" ]]; then + mkdir -p "${BUILD}/CMakeFiles/wally.dir" ninja_bin="$(command -v ninja || command -v ninja-build || true)" if [[ -n "${ninja_bin}" ]]; then - "${ninja_bin}" -C "${BUILD}" -t commands rcli | tail -1 > "${LINK_TXT}" || true + "${ninja_bin}" -C "${BUILD}" -t commands wally | tail -1 > "${LINK_TXT}" || true fi fi if [[ ! -s "${LINK_TXT}" ]]; then - echo "no ${LINK_TXT} — build the rcli target first (Ninja or Makefiles)" >&2 + echo "no ${LINK_TXT} — build the wally target first (Ninja or Makefiles)" >&2 exit 1 fi @@ -64,10 +72,10 @@ contains() { return 1 } -# Plugin backends are -force_load on rcli-cxx so their static registrars run. +# Plugin backends are -force_load on wally-cxx so their static registrars run. # llama-common is a regular archive: force-loading it pulls download.cpp.o, # which references cpp-httplib methods that the kit never emitted as objects -# (rcli-cxx never pulled that TU). Split so Swift can force_load plugins +# (wally-cxx never pulled that TU). Split so Swift can force_load plugins # without dragging those undefineds in. plugins=() regular=() @@ -126,7 +134,7 @@ if [[ ${#regular[@]} -eq 0 ]]; then exit 1 fi -OUT_PLUGINS="${BUILD}/librcli_plugins.a" +OUT_PLUGINS="${BUILD}/libwally_plugins.a" # libtool rather than ar: it merges archives rather than nesting them, and it # is what ships with the toolchain that produced them. libtool -static -o "${OUT_LIB}" "${regular[@]}" diff --git a/scripts/fetch-kit.sh b/scripts/build/fetch-kit.sh similarity index 54% rename from scripts/fetch-kit.sh rename to scripts/build/fetch-kit.sh index c368d35..7cead0f 100755 --- a/scripts/fetch-kit.sh +++ b/scripts/build/fetch-kit.sh @@ -4,40 +4,38 @@ # scripts/fetch-kit.sh # # Requires: gh, and either shasum or sha256sum. -# Pins live in cmake/sdk-pin.cmake. SDK_VERSION, if set, must equal -# RCLI_PINNED_SDK_VERSION — checksums are keyed to that pin, not a repo variable. +# Pins live in versions.toml (the single source; cmake/sdk-pin.cmake reads the +# same file). SDK_VERSION, if set, must equal kit_version there -- checksums are +# keyed to that pin, not a repo variable. set -euo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# shellcheck source=scripts/lib/common.sh +source "${ROOT}/scripts/lib/common.sh" PLATFORM="${1:?usage: fetch-kit.sh }" DEST="${2:?usage: fetch-kit.sh }" -PIN="${ROOT}/cmake/sdk-pin.cmake" +PIN="${ROOT}/versions.toml" -case "$PLATFORM" in - macos-arm64) SHA_VAR=RCLI_PINNED_KIT_SHA256_MACOS_ARM64 ;; - windows-x64) SHA_VAR=RCLI_PINNED_KIT_SHA256_WINDOWS_X64 ;; - windows-arm64) SHA_VAR=RCLI_PINNED_KIT_SHA256_WINDOWS_ARM64 ;; - *) echo "error: unknown platform '$PLATFORM'" >&2; exit 2 ;; -esac - -pin_value() { - local key="$1" - sed -n "s/^set(${key} \"\\(.*\\)\")/\\1/p" "$PIN" | head -1 -} - -PINNED_SDK="$(pin_value RCLI_PINNED_SDK_VERSION)" -EXPECTED="$(pin_value "$SHA_VAR")" +PINNED_SDK="$(wally_kit_version)" +EXPECTED="$(wally_kit_sha "$PLATFORM")" || exit 2 if [[ -z "$PINNED_SDK" || -z "$EXPECTED" ]]; then - echo "error: missing $SHA_VAR or RCLI_PINNED_SDK_VERSION in $PIN" >&2 + echo "error: missing kit pin for '$PLATFORM' or kit_version in $PIN" >&2 exit 1 fi if [[ -n "${SDK_VERSION:-}" && "$SDK_VERSION" != "$PINNED_SDK" ]]; then - echo "error: SDK_VERSION=$SDK_VERSION does not match RCLI_PINNED_SDK_VERSION=$PINNED_SDK in $PIN" >&2 - echo " bump cmake/sdk-pin.cmake (version + SHA-256) together; do not override only SDK_VERSION" >&2 + echo "error: SDK_VERSION=$SDK_VERSION does not match kit_version=$PINNED_SDK in $PIN" >&2 + echo " bump versions.toml (kit_version + kit_sha256_*) together; do not override only SDK_VERSION" >&2 exit 1 fi SDK_VERSION="$PINNED_SDK" +# The kit's own version (baked into the tarball name from core/VERSION) is always +# SDK_VERSION. The GitHub Release it hangs on is separate: kit-only releases live +# on a `cpp-desktop-v` tag so they never trip the full SDK release train, +# while a kit cut by that train sits on the plain `v` tag. `kit_release_tag` +# in versions.toml names it; absent, we fall back to `v` for older pins. +RELEASE_TAG="$(wally_kit_release_tag)" + asset="RunAnywhere-cpp-desktop-${PLATFORM}-v${SDK_VERSION}.tar.gz" dl="$(mktemp -d)" trap 'rm -rf "$dl"' EXIT @@ -45,12 +43,12 @@ trap 'rm -rf "$dl"' EXIT # Draft GitHub Releases are invisible to another repo's GITHUB_TOKEN # (`release not found`). The pin must point at a published release # (prerelease is fine; latest is not required). -if ! gh release download "v${SDK_VERSION}" \ +if ! gh release download "$RELEASE_TAG" \ --repo RunanywhereAI/runanywhere-sdks \ --pattern "$asset" --dir "$dl"; then - echo "error: could not download $asset from RunanywhereAI/runanywhere-sdks@v${SDK_VERSION}" >&2 + echo "error: could not download $asset from RunanywhereAI/runanywhere-sdks@${RELEASE_TAG}" >&2 echo " that tag must be a published GitHub Release (drafts 404 for this token)." >&2 - gh release view "v${SDK_VERSION}" --repo RunanywhereAI/runanywhere-sdks >&2 || true + gh release view "$RELEASE_TAG" --repo RunanywhereAI/runanywhere-sdks >&2 || true exit 1 fi @@ -76,7 +74,7 @@ tar xzf "$file" -C "$DEST" --strip-components=1 echo "kit verified and extracted to $DEST" # Optional NeuRT / QHexRT overlay. Missing overlay is not a failure unless -# RCLI_REQUIRE_PRIVATE=1 — public bottles stay OSS. -if [[ -x "$ROOT/scripts/fetch-private-pack.sh" ]]; then - "$ROOT/scripts/fetch-private-pack.sh" "$PLATFORM" "$DEST" +# WALLY_REQUIRE_PRIVATE=1 — public bottles stay OSS. +if [[ -x "$ROOT/scripts/build/fetch-private-pack.sh" ]]; then + "$ROOT/scripts/build/fetch-private-pack.sh" "$PLATFORM" "$DEST" fi diff --git a/scripts/fetch-private-pack.sh b/scripts/build/fetch-private-pack.sh similarity index 74% rename from scripts/fetch-private-pack.sh rename to scripts/build/fetch-private-pack.sh index e6f6249..b35a009 100755 --- a/scripts/fetch-private-pack.sh +++ b/scripts/build/fetch-private-pack.sh @@ -5,13 +5,13 @@ # # There is no remote fetch (this repo has no token that can see SDK private # workflow artifacts). Resolution order: -# 1. RCLI_PRIVATE_OVERLAY — local tarball +# 1. WALLY_PRIVATE_OVERLAY — local tarball # 2. tarball next to the kit prefix named # RunAnywhere-cpp-desktop-{macos-arm64-neurt,windows-arm64-qhexrt}-private-v*.tar.gz -# 3. skip (OSS bottle) unless RCLI_REQUIRE_PRIVATE=1 +# 3. skip (OSS bottle) unless WALLY_REQUIRE_PRIVATE=1 set -euo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" PLATFORM="${1:?usage: fetch-private-pack.sh }" DEST="${2:?usage: fetch-private-pack.sh }" @@ -22,6 +22,10 @@ case "$PLATFORM" in echo "QHexRT is Windows ARM64 (Snapdragon NPU) only; no overlay on windows-x64." exit 0 ;; + linux-x64) + echo "No private engine overlay on linux-x64 (OSS kit only)." + exit 0 + ;; *) echo "error: unknown platform '$PLATFORM'" >&2 exit 2 @@ -37,12 +41,12 @@ apply_tar() { echo "private overlay applied: $tar -> $DEST" } -if [[ -n "${RCLI_PRIVATE_OVERLAY:-}" ]]; then - [[ -f "$RCLI_PRIVATE_OVERLAY" ]] || { - echo "error: RCLI_PRIVATE_OVERLAY is not a file: $RCLI_PRIVATE_OVERLAY" >&2 +if [[ -n "${WALLY_PRIVATE_OVERLAY:-}" ]]; then + [[ -f "$WALLY_PRIVATE_OVERLAY" ]] || { + echo "error: WALLY_PRIVATE_OVERLAY is not a file: $WALLY_PRIVATE_OVERLAY" >&2 exit 1 } - apply_tar "$RCLI_PRIVATE_OVERLAY" + apply_tar "$WALLY_PRIVATE_OVERLAY" exit 0 fi @@ -57,9 +61,9 @@ if [[ ${#local_hits[@]} -gt 0 ]]; then fi shopt -u nullglob -if [[ "${RCLI_REQUIRE_PRIVATE:-}" == "1" ]]; then +if [[ "${WALLY_REQUIRE_PRIVATE:-}" == "1" ]]; then echo "error: no private ${ENGINE} overlay found for $PLATFORM" >&2 - echo " set RCLI_PRIVATE_OVERLAY or drop the tarball next to the kit prefix" >&2 + echo " set WALLY_PRIVATE_OVERLAY or drop the tarball next to the kit prefix" >&2 exit 1 fi echo "skip: no private ${ENGINE} overlay for $PLATFORM (OSS kit only)" diff --git a/scripts/package-rcli-windows.ps1 b/scripts/build/package-wally-windows.ps1 similarity index 70% rename from scripts/package-rcli-windows.ps1 rename to scripts/build/package-wally-windows.ps1 index 0a5e7c0..0fa02ae 100644 --- a/scripts/package-rcli-windows.ps1 +++ b/scripts/build/package-wally-windows.ps1 @@ -8,13 +8,20 @@ param( [Parameter(Mandatory = $false)] [string]$KitDir = "", - # Names the archive. install.ps1 asks for rcli--windows-arm64.zip on an + # Names the archive. install.ps1 asks for wally--windows-arm64.zip on an # ARM64 host and falls back to x86_64, so the arm64 build must use exactly # that spelling or the native archive is never found. [Parameter(Mandatory = $false)] [ValidateSet("windows-x86_64", "windows-arm64")] - [string]$Platform = "windows-x86_64" + [string]$Platform = "windows-x86_64", + + # "prod" for the production bottle, "dev" for the dev-endpoint bottle. Only + # the archive filename changes (-dev); the staged tree stays wally-. + [Parameter(Mandatory = $false)] + [ValidateSet("prod", "dev")] + [string]$Channel = "prod" ) +$Suffix = if ($Channel -eq "dev") { "-dev" } else { "" } $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest @@ -27,24 +34,24 @@ $BuildDir = (Resolve-Path $BuildDir).Path if ([string]::IsNullOrWhiteSpace($Version)) { $cm = Get-Content (Join-Path $CliRoot "CMakeLists.txt") -Raw - if ($cm -match 'project\(rcli VERSION ([0-9.]+)') { $Version = $Matches[1] } + if ($cm -match 'project\(wally VERSION ([0-9.]+)') { $Version = $Matches[1] } } $Version = $Version.TrimStart("v") -if ([string]::IsNullOrWhiteSpace($Version)) { throw "cannot resolve RCLI version" } +if ([string]::IsNullOrWhiteSpace($Version)) { throw "cannot resolve Wally version" } $Binary = @( - (Join-Path $BuildDir "rcli.exe"), - (Join-Path $BuildDir "Release\rcli.exe"), - (Join-Path $BuildDir "RelWithDebInfo\rcli.exe") + (Join-Path $BuildDir "wally.exe"), + (Join-Path $BuildDir "Release\wally.exe"), + (Join-Path $BuildDir "RelWithDebInfo\wally.exe") ) | Where-Object { Test-Path $_ } | Select-Object -First 1 if (-not $Binary) { - $Binary = Get-ChildItem -Path $BuildDir -Filter "rcli.exe" -File -Recurse | + $Binary = Get-ChildItem -Path $BuildDir -Filter "wally.exe" -File -Recurse | Select-Object -ExpandProperty FullName -First 1 } -if (-not $Binary) { throw "rcli.exe was not found under $BuildDir" } +if (-not $Binary) { throw "wally.exe was not found under $BuildDir" } if ([string]::IsNullOrWhiteSpace($KitDir)) { - $KitDir = $env:RCLI_SDK_KIT + $KitDir = $env:WALLY_SDK_KIT } if ([string]::IsNullOrWhiteSpace($KitDir)) { $KitDir = $env:CMAKE_PREFIX_PATH @@ -52,13 +59,13 @@ if ([string]::IsNullOrWhiteSpace($KitDir)) { $DistDir = Join-Path $CliRoot "dist" $StageRoot = Join-Path $DistDir "stage" -$Stage = Join-Path $StageRoot "rcli-$Platform" +$Stage = Join-Path $StageRoot "wally-$Platform" $BinDir = Join-Path $Stage "bin" -$Zip = Join-Path $DistDir "rcli-$Version-$Platform.zip" +$Zip = Join-Path $DistDir "wally-$Version-$Platform$Suffix.zip" Remove-Item $Stage -Recurse -Force -ErrorAction SilentlyContinue New-Item $BinDir -ItemType Directory -Force | Out-Null -Copy-Item $Binary (Join-Path $BinDir "rcli.exe") +Copy-Item $Binary (Join-Path $BinDir "wally.exe") $Readme = Join-Path $CliRoot "README.md" if (Test-Path $Readme) { Copy-Item $Readme (Join-Path $Stage "README.md") } @@ -79,8 +86,8 @@ Get-ChildItem -Path (Split-Path $Binary -Parent) -Filter "*.dll" -File -ErrorAct $OldPath = $env:PATH try { $env:PATH = "$BinDir;$OldPath" - & (Join-Path $BinDir "rcli.exe") version - if ($LASTEXITCODE -ne 0) { throw "packaged rcli version smoke failed" } + & (Join-Path $BinDir "wally.exe") version + if ($LASTEXITCODE -ne 0) { throw "packaged wally version smoke failed" } } finally { $env:PATH = $OldPath } diff --git a/scripts/build/package-wally.sh b/scripts/build/package-wally.sh new file mode 100755 index 0000000..503fbaa --- /dev/null +++ b/scripts/build/package-wally.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# package-wally.sh +# +# Relocatable wally bottle from a kit-linked CMake build. Does not compile the SDK. +# +# platform-tag: macos-arm64 | linux-x86_64 +# version: $WALLY_VERSION, else project(wally VERSION …) +# macOS signing: $WALLY_CODESIGN_IDENTITY, optional $WALLY_CODESIGN_KEYCHAIN +# Set $WALLY_REQUIRE_DEVELOPER_ID=1 to reject ad-hoc signing. +# +# Layout: +# wally-/bin/wally +# wally-/lib/ +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +# shellcheck source=scripts/lib/common.sh +source "${ROOT}/scripts/lib/common.sh" +BUILD="${1:?usage: package-wally.sh [channel]}" +PLATFORM="${2:?usage: package-wally.sh [channel]}" +# channel: empty/prod for the production bottle, "dev" for the dev-endpoint +# bottle. Only the archive filename changes (-dev); the staged tree and its +# single root stay wally- so install scripts extract both the same. +CHANNEL="${3:-}" +SUFFIX="" +[[ "${CHANNEL}" == dev ]] && SUFFIX="-dev" +[[ "${BUILD}" = /* ]] || BUILD="${ROOT}/${BUILD}" + +VERSION="${WALLY_VERSION:-$(wally_version)}" +[[ -n "${VERSION}" ]] || { echo "error: cannot resolve WALLY version from versions.toml" >&2; exit 1; } + +KIT="${WALLY_SDK_KIT:-${CMAKE_PREFIX_PATH:-}}" +KIT="${KIT%%:*}" + +BIN="" +if [[ "$(uname -s)" == Darwin ]]; then + # The macOS bottle is the Swift MLX host. Never silently ship wally-cxx. + if [[ ! -x "${BUILD}/wally" ]]; then + echo "error: macOS bottle requires ${BUILD}/wally (Swift MLX host)." >&2 + echo " cmake --build with WALLY_APPLE_MLX_HOST=ON, or scripts/build/build-mlx.sh" >&2 + exit 1 + fi + BIN="${BUILD}/wally" +else + cands=("${BUILD}/wally-cxx" "${BUILD}/wally" "${BUILD}/wally.exe" "${BUILD}/Release/wally" "${BUILD}/Release/wally.exe") + for cand in "${cands[@]}"; do + if [[ -x "${cand}" ]]; then BIN="${cand}"; break; fi + done +fi +[[ -n "${BIN}" ]] || { echo "error: wally binary not found under ${BUILD}" >&2; exit 1; } + +DIST="${ROOT}/dist" +STAGE_ROOT="${DIST}/stage" +STAGE="${STAGE_ROOT}/wally-${PLATFORM}" +TARBALL="${DIST}/wally-${VERSION}-${PLATFORM}${SUFFIX}.tar.gz" + +rm -rf "${STAGE}" +mkdir -p "${STAGE}/bin" "${STAGE}/lib" +cp "${BIN}" "${STAGE}/bin/wally" +chmod +x "${STAGE}/bin/wally" +[[ -f "${ROOT}/README.md" ]] && cp "${ROOT}/README.md" "${STAGE}/README.md" +shopt -s nullglob +for bundle in "${BUILD}"/*.bundle; do + cp -R "${bundle}" "${STAGE}/bin/" +done +shopt -u nullglob +if [[ "$(uname -s)" == Darwin ]]; then + if [[ ! -d "${STAGE}/bin/mlx-swift_Cmlx.bundle" ]]; then + echo "error: macOS bottle requires mlx-swift_Cmlx.bundle next to wally (Metal shaders)." >&2 + echo " cmake --build with WALLY_APPLE_MLX_HOST=ON, or scripts/build/build-mlx.sh" >&2 + exit 1 + fi +fi + +copy_kit_runtime() { + local src="$1" + [[ -n "${src}" && -e "${src}" ]] || return 0 + cp -R "${src}" "${STAGE}/lib/" +} + +if [[ -n "${KIT}" && -d "${KIT}/third_party" ]]; then + case "${PLATFORM}" in + macos-*) + copy_kit_runtime "${KIT}/third_party/libonnxruntime.dylib" + ;; + linux-*) + shopt -s nullglob + for so in "${KIT}/third_party"/libonnxruntime.so*; do + copy_kit_runtime "${so}" + done + shopt -u nullglob + ;; + esac +fi + +case "${PLATFORM}" in + macos-*) + if [[ -d "${STAGE}/lib" ]] && compgen -G "${STAGE}/lib/*.dylib" >/dev/null; then + install_name_tool -add_rpath "@loader_path/../lib" "${STAGE}/bin/wally" 2>/dev/null || true + for lib in "${STAGE}/lib/"*.dylib; do + install_name_tool -id "@rpath/$(basename "${lib}")" "${lib}" 2>/dev/null || true + done + fi + sign_identity="${WALLY_CODESIGN_IDENTITY:--}" + if [[ "${WALLY_REQUIRE_DEVELOPER_ID:-0}" == 1 && "${sign_identity}" == - ]]; then + echo "error: production packaging requires WALLY_CODESIGN_IDENTITY" >&2 + exit 1 + fi + sign_args=(--force --sign "${sign_identity}") + if [[ "${sign_identity}" != - ]]; then + sign_args+=(--options runtime --timestamp) + fi + if [[ -n "${WALLY_CODESIGN_KEYCHAIN:-}" ]]; then + sign_args+=(--keychain "${WALLY_CODESIGN_KEYCHAIN}") + fi + shopt -s nullglob + for lib in "${STAGE}/lib/"*.dylib; do + codesign "${sign_args[@]}" "${lib}" + codesign --verify --strict "${lib}" + done + shopt -u nullglob + codesign "${sign_args[@]}" "${STAGE}/bin/wally" + codesign --verify --strict "${STAGE}/bin/wally" + ;; + linux-*) + if command -v patchelf >/dev/null; then + patchelf --set-rpath "\$ORIGIN/../lib" "${STAGE}/bin/wally" + fi + ;; +esac + +"${STAGE}/bin/wally" version >/dev/null + +mkdir -p "${DIST}" +rm -f "${TARBALL}" "${TARBALL}.sha256" +# macOS tar otherwise serializes Finder metadata as `._*` AppleDouble roots, +# breaking the single-root archive contract and surprising non-macOS clients. +COPYFILE_DISABLE=1 tar -czf "${TARBALL}" -C "${STAGE_ROOT}" "wally-${PLATFORM}" +(cd "${DIST}" && shasum -a 256 "$(basename "${TARBALL}")" > "$(basename "${TARBALL}").sha256") +echo "Packaged ${TARBALL}" +tar -tzf "${TARBALL}" | head -20 diff --git a/scripts/ci/check-agents-sync.sh b/scripts/ci/check-agents-sync.sh index a17d6ad..2e6d2b2 100755 --- a/scripts/ci/check-agents-sync.sh +++ b/scripts/ci/check-agents-sync.sh @@ -101,7 +101,7 @@ if [ ! -d "${DST}" ]; then printf 'error: missing skills mirror %s (run --fix)\n' "${DST}" >&2 exit 1 fi -if ! is_tracked "${SRC}/rcli-architecture/SKILL.md" || ! is_tracked "${DST}/rcli-architecture/SKILL.md"; then +if ! is_tracked "${SRC}/wally-architecture/SKILL.md" || ! is_tracked "${DST}/wally-architecture/SKILL.md"; then printf 'error: skill trees must be git-tracked (got ignored? check .gitignore)\n' >&2 status=1 fi diff --git a/scripts/ci/check-versions.py b/scripts/ci/check-versions.py new file mode 100644 index 0000000..d91e6ca --- /dev/null +++ b/scripts/ci/check-versions.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""Fail if a version that cannot read versions.toml at its own build step has +drifted from it. + +CMake reads versions.toml directly, so the build always agrees with it. Two +files cannot: the Homebrew formula's `version` line and the download URLs it +builds from that version, and the Swift package's exact SDK pin. This checks +both against versions.toml so a bump in one place that misses the other fails +CI rather than shipping a mismatch. + + python3 scripts/ci/check-versions.py + +Run from anywhere in the repo. Exits non-zero on the first mismatch. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent.parent +VERSIONS = ROOT / "versions.toml" +FORMULA = ROOT / "Formula" / "wally.rb" +PACKAGE = ROOT / "swift" / "Package.swift" +CMAKELISTS = ROOT / "CMakeLists.txt" +WORKFLOWS = ROOT / ".github" / "workflows" + + +def read_toml_value(key: str) -> str: + pattern = re.compile(rf'^\s*{re.escape(key)}\s*=\s*"([^"]*)"', re.M) + match = pattern.search(VERSIONS.read_text(encoding="utf-8")) + if not match: + sys.exit(f"versions.toml is missing '{key}'") + return match.group(1) + + +def read_toml_section(name: str) -> dict[str, str]: + text = VERSIONS.read_text(encoding="utf-8") + body = re.search(rf'^\[{re.escape(name)}\]\n(.*?)(?=^\[|\Z)', text, re.M | re.S) + if not body: + sys.exit(f"versions.toml is missing section '[{name}]'") + return dict(re.findall(r'^\s*(\w+)\s*=\s*"([^"]*)"', body.group(1), re.M)) + + +def main() -> None: + product = read_toml_value("version") + swift_pin = read_toml_value("sdk_package_version") + ci_ref = read_toml_value("sdk_ci_ref") + failures: list[str] = [] + + # The workflows that build the Apple MLX host check out runanywhere-sdks at + # a tag. Both must name the tracked ref, so a bump here reaches CI and the + # release together. + for workflow in ("ci.yml", "release.yml"): + path = ROOT / ".github" / "workflows" / workflow + text = path.read_text(encoding="utf-8") + if "runanywhere-sdks" in text: + for ref in re.findall(r"ref:\s*v([0-9]+\.[0-9]+\.[0-9]+)", text): + if ref != ci_ref: + failures.append(f"{path}: runanywhere-sdks ref v{ref} != versions.toml v{ci_ref}") + + # The formula's version line, and every release URL it builds, must name the + # product version. update-tap.sh re-stamps these from a real release; this + # catches the checked-in copy drifting from versions.toml between releases. + formula = FORMULA.read_text(encoding="utf-8") + formula_version = re.search(r'^\s*version\s+"([^"]*)"', formula, re.M) + if not formula_version: + failures.append(f"{FORMULA}: no version line found") + elif formula_version.group(1) != product: + failures.append( + f"{FORMULA}: version \"{formula_version.group(1)}\" != versions.toml \"{product}\"" + ) + for url in re.findall(r'url\s+"([^"]*)"', formula): + # A placeholder release with no published asset can name the product + # version in its path; only flag a URL that names a different one. + found = re.search(r"/v(\d+\.\d+\.\d+)/wally-(\d+\.\d+\.\d+)-", url) + if found and (found.group(1) != product or found.group(2) != product): + failures.append(f"{FORMULA}: url names {found.group(1)}/{found.group(2)}, not {product}") + + # The Swift package's exact SDK pin. + package = PACKAGE.read_text(encoding="utf-8") + package_pin = re.search(r'runanywhere-swift\.git",\s*exact:\s*"([^"]*)"', package) + if not package_pin: + failures.append(f"{PACKAGE}: no exact runanywhere-swift pin found") + elif package_pin.group(1) != swift_pin: + failures.append( + f"{PACKAGE}: swift SDK pin \"{package_pin.group(1)}\" != versions.toml \"{swift_pin}\"" + ) + + # CMake declares its own floor and C++ standard; hold them to the pins here. + toolchain = read_toml_section("toolchain") + cmake = CMAKELISTS.read_text(encoding="utf-8") + for label, pattern, key in ( + ("cmake_minimum_required", r"cmake_minimum_required\(VERSION\s+([0-9.]+)", "cmake_minimum"), + ("CMAKE_CXX_STANDARD", r"CMAKE_CXX_STANDARD\s+([0-9]+)\)", "cxx_standard"), + ): + found = re.search(pattern, cmake) + if not found: + failures.append(f"{CMAKELISTS}: no {label} found") + elif found.group(1) != toolchain.get(key): + failures.append( + f"{CMAKELISTS}: {label} {found.group(1)} != versions.toml {key} {toolchain.get(key)}" + ) + + # Every runner a workflow names must be one tracked here, so a new image + # can't slip in unpinned. + runners = set(read_toml_section("runners").values()) + for workflow in sorted(WORKFLOWS.glob("*.yml")): + for runner in re.findall(r"runs-on:\s*(\S+)", workflow.read_text(encoding="utf-8")): + if runner not in runners: + failures.append(f"{workflow}: runs-on '{runner}' is not a tracked runner") + + if failures: + sys.stderr.write("version drift from versions.toml:\n") + for line in failures: + sys.stderr.write(f" {line}\n") + sys.exit(1) + print(f"versions.toml consistent: product {product}, swift SDK {swift_pin}") + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/oss_keyless_telemetry_blast.sh b/scripts/ci/oss_keyless_telemetry_blast.sh index b4015c9..fe23a76 100755 --- a/scripts/ci/oss_keyless_telemetry_blast.sh +++ b/scripts/ci/oss_keyless_telemetry_blast.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# OSS keyless gate: kit-linked rcli → development → public staging backend blast. +# OSS keyless gate: kit-linked wally → development → public staging backend blast. # No API key. Asserts exit 0 and all 12 modalities stored ≥ 1. # # Requires a staging backend origin via env (never hardcode private infra hosts): @@ -28,23 +28,23 @@ case "$(uname -s)" in ;; esac -KIT="${RCLI_SDK_KIT:-$ROOT/kit}" -RCLI="${RA_RCLI_BIN:-$ROOT/build/rcli-cxx}" -if [[ ! -x "$RCLI" ]]; then - RCLI="${RA_RCLI_BIN:-$ROOT/build/rcli}" +KIT="${WALLY_SDK_KIT:-$ROOT/kit}" +WALLY="${RA_WALLY_BIN:-$ROOT/build/wally-cxx}" +if [[ ! -x "$WALLY" ]]; then + WALLY="${RA_WALLY_BIN:-$ROOT/build/wally}" fi if [[ "${RA_SKIP_BUILD:-0}" != "1" ]]; then if [[ ! -f "$KIT/include/runanywhere/proto/model_types.pb.h" ]]; then - bash "$ROOT/scripts/fetch-kit.sh" "$PLATFORM" "$KIT" + bash "$ROOT/scripts/build/fetch-kit.sh" "$PLATFORM" "$KIT" fi cmake -B "$ROOT/build" -G Ninja -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH="$KIT" cmake --build "$ROOT/build" -j "$JOBS" fi -[[ -x "$RCLI" ]] || { - echo "rcli not executable: $RCLI" >&2 +[[ -x "$WALLY" ]] || { + echo "wally not executable: $WALLY" >&2 exit 1 } @@ -61,12 +61,12 @@ export XDG_DATA_HOME="$TMP_HOME/data" export XDG_STATE_HOME="$TMP_HOME/state" export RUNANYWHERE_HOME="$TMP_HOME/home" -echo "[oss-keyless] rcli=$RCLI" +echo "[oss-keyless] wally=$WALLY" echo "[oss-keyless] base_url=$OSS_URL" echo "[oss-keyless] session_id=$SESSION" set +e -OUT="$("$RCLI" --environment development \ +OUT="$("$WALLY" --environment development \ --base-url "$OSS_URL" \ telemetry blast \ --processing-ms 42.5 \ @@ -78,7 +78,7 @@ set -e printf '%s\n' "$OUT" if [[ "$RC" -ne 0 ]]; then - echo "[oss-keyless] FAIL: rcli exited $RC" >&2 + echo "[oss-keyless] FAIL: wally exited $RC" >&2 exit "$RC" fi diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh new file mode 100644 index 0000000..f7bfc43 --- /dev/null +++ b/scripts/lib/common.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Shared helpers for wally's build/test/release scripts. Source it, don't run it: +# +# source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh" +# +# versions.toml is the single source of truth for every pin; nothing here or in +# a caller hardcodes a version, a sha, or a kit tag. Functions use `local` and +# print to stderr, so a caller's stdout stays its own. + +# Repo root, regardless of which subdirectory the sourcing script lives in. +wally_root() { + local here + here="$(cd "$(dirname "${BASH_SOURCE[1]}")" && pwd)" + # Walk up until we find versions.toml (the repo marker), so a script at any + # depth under scripts/ resolves the same root. + while [[ "${here}" != "/" && ! -f "${here}/versions.toml" ]]; do + here="$(dirname "${here}")" + done + printf '%s\n' "${here}" +} + +# One flat `key = "value"` line out of versions.toml. The file's own header +# guarantees the flat format, so one regex reads any pin. +wally_pin() { + local key="$1" root + root="$(wally_root)" + sed -n "s/^[[:space:]]*${key}[[:space:]]*=[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" \ + "${root}/versions.toml" | head -1 +} + +wally_version() { wally_pin version; } +wally_kit_version() { wally_pin kit_version; } +wally_kit_release_tag() { + local tag; tag="$(wally_pin kit_release_tag)" + printf '%s\n' "${tag:-v$(wally_kit_version)}" +} + +# Kit sha for a platform tag (macos-arm64|windows-x64|windows-arm64|linux-x64). +wally_kit_sha() { + case "$1" in + macos-arm64) wally_pin kit_sha256_macos_arm64 ;; + windows-x64) wally_pin kit_sha256_windows_x64 ;; + windows-arm64) wally_pin kit_sha256_windows_arm64 ;; + linux-x64) wally_pin kit_sha256_linux_x64 ;; + *) echo "error: unknown platform '$1'" >&2; return 2 ;; + esac +} + +# The host's own kit platform tag, for a local build. +wally_host_platform() { + case "$(uname -s)" in + Darwin) echo macos-arm64 ;; + Linux) echo linux-x64 ;; + *) echo windows-x64 ;; + esac +} + +log_step() { printf '== %s ==\n' "$*" >&2; } +log_ok() { printf ' ok %s\n' "$*" >&2; } +log_fail() { printf ' FAIL %s\n' "$*" >&2; } diff --git a/scripts/package-rcli.sh b/scripts/package-rcli.sh deleted file mode 100755 index 6bdc4aa..0000000 --- a/scripts/package-rcli.sh +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env bash -# package-rcli.sh -# -# Relocatable rcli bottle from a kit-linked CMake build. Does not compile the SDK. -# -# platform-tag: macos-arm64 | linux-x86_64 -# version: $RCLI_VERSION, else project(rcli VERSION …) -# macOS signing: $RCLI_CODESIGN_IDENTITY, optional $RCLI_CODESIGN_KEYCHAIN -# Set $RCLI_REQUIRE_DEVELOPER_ID=1 to reject ad-hoc signing. -# -# Layout: -# rcli-/bin/rcli -# rcli-/lib/ -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -BUILD="${1:?usage: package-rcli.sh }" -PLATFORM="${2:?usage: package-rcli.sh }" -[[ "${BUILD}" = /* ]] || BUILD="${ROOT}/${BUILD}" - -VERSION="${RCLI_VERSION:-}" -if [[ -z "${VERSION}" ]]; then - VERSION="$(sed -nE 's/^project\(rcli VERSION ([0-9.]+).*/\1/p' "${ROOT}/CMakeLists.txt" | head -1)" -fi -[[ -n "${VERSION}" ]] || { echo "error: cannot resolve RCLI version" >&2; exit 1; } - -KIT="${RCLI_SDK_KIT:-${CMAKE_PREFIX_PATH:-}}" -KIT="${KIT%%:*}" - -BIN="" -if [[ "$(uname -s)" == Darwin ]]; then - # The macOS bottle is the Swift MLX host. Never silently ship rcli-cxx. - if [[ ! -x "${BUILD}/rcli" ]]; then - echo "error: macOS bottle requires ${BUILD}/rcli (Swift MLX host)." >&2 - echo " cmake --build with RCLI_APPLE_MLX_HOST=ON, or scripts/build-mlx.sh" >&2 - exit 1 - fi - BIN="${BUILD}/rcli" -else - cands=("${BUILD}/rcli-cxx" "${BUILD}/rcli" "${BUILD}/rcli.exe" "${BUILD}/Release/rcli" "${BUILD}/Release/rcli.exe") - for cand in "${cands[@]}"; do - if [[ -x "${cand}" ]]; then BIN="${cand}"; break; fi - done -fi -[[ -n "${BIN}" ]] || { echo "error: rcli binary not found under ${BUILD}" >&2; exit 1; } - -DIST="${ROOT}/dist" -STAGE_ROOT="${DIST}/stage" -STAGE="${STAGE_ROOT}/rcli-${PLATFORM}" -TARBALL="${DIST}/rcli-${VERSION}-${PLATFORM}.tar.gz" - -rm -rf "${STAGE}" -mkdir -p "${STAGE}/bin" "${STAGE}/lib" -cp "${BIN}" "${STAGE}/bin/rcli" -chmod +x "${STAGE}/bin/rcli" -[[ -f "${ROOT}/README.md" ]] && cp "${ROOT}/README.md" "${STAGE}/README.md" -shopt -s nullglob -for bundle in "${BUILD}"/*.bundle; do - cp -R "${bundle}" "${STAGE}/bin/" -done -shopt -u nullglob -if [[ "$(uname -s)" == Darwin ]]; then - if [[ ! -d "${STAGE}/bin/mlx-swift_Cmlx.bundle" ]]; then - echo "error: macOS bottle requires mlx-swift_Cmlx.bundle next to rcli (Metal shaders)." >&2 - echo " cmake --build with RCLI_APPLE_MLX_HOST=ON, or scripts/build-mlx.sh" >&2 - exit 1 - fi -fi - -copy_kit_runtime() { - local src="$1" - [[ -n "${src}" && -e "${src}" ]] || return 0 - cp -R "${src}" "${STAGE}/lib/" -} - -if [[ -n "${KIT}" && -d "${KIT}/third_party" ]]; then - case "${PLATFORM}" in - macos-*) - copy_kit_runtime "${KIT}/third_party/libonnxruntime.dylib" - ;; - linux-*) - shopt -s nullglob - for so in "${KIT}/third_party"/libonnxruntime.so*; do - copy_kit_runtime "${so}" - done - shopt -u nullglob - ;; - esac -fi - -case "${PLATFORM}" in - macos-*) - if [[ -d "${STAGE}/lib" ]] && compgen -G "${STAGE}/lib/*.dylib" >/dev/null; then - install_name_tool -add_rpath "@loader_path/../lib" "${STAGE}/bin/rcli" 2>/dev/null || true - for lib in "${STAGE}/lib/"*.dylib; do - install_name_tool -id "@rpath/$(basename "${lib}")" "${lib}" 2>/dev/null || true - done - fi - sign_identity="${RCLI_CODESIGN_IDENTITY:--}" - if [[ "${RCLI_REQUIRE_DEVELOPER_ID:-0}" == 1 && "${sign_identity}" == - ]]; then - echo "error: production packaging requires RCLI_CODESIGN_IDENTITY" >&2 - exit 1 - fi - sign_args=(--force --sign "${sign_identity}") - if [[ "${sign_identity}" != - ]]; then - sign_args+=(--options runtime --timestamp) - fi - if [[ -n "${RCLI_CODESIGN_KEYCHAIN:-}" ]]; then - sign_args+=(--keychain "${RCLI_CODESIGN_KEYCHAIN}") - fi - shopt -s nullglob - for lib in "${STAGE}/lib/"*.dylib; do - codesign "${sign_args[@]}" "${lib}" - codesign --verify --strict "${lib}" - done - shopt -u nullglob - codesign "${sign_args[@]}" "${STAGE}/bin/rcli" - codesign --verify --strict "${STAGE}/bin/rcli" - ;; - linux-*) - if command -v patchelf >/dev/null; then - patchelf --set-rpath "\$ORIGIN/../lib" "${STAGE}/bin/rcli" - fi - ;; -esac - -"${STAGE}/bin/rcli" version >/dev/null - -mkdir -p "${DIST}" -rm -f "${TARBALL}" "${TARBALL}.sha256" -# macOS tar otherwise serializes Finder metadata as `._*` AppleDouble roots, -# breaking the single-root archive contract and surprising non-macOS clients. -COPYFILE_DISABLE=1 tar -czf "${TARBALL}" -C "${STAGE_ROOT}" "rcli-${PLATFORM}" -(cd "${DIST}" && shasum -a 256 "$(basename "${TARBALL}")" > "$(basename "${TARBALL}").sha256") -echo "Packaged ${TARBALL}" -tar -tzf "${TARBALL}" | head -20 diff --git a/scripts/stamp-formula.py b/scripts/release/stamp-formula.py similarity index 95% rename from scripts/stamp-formula.py rename to scripts/release/stamp-formula.py index 7229c4e..7600c4d 100755 --- a/scripts/stamp-formula.py +++ b/scripts/release/stamp-formula.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Point Formula/rcli.rb at a published release. +"""Point Formula/wally.rb at a published release. stamp-formula.py VERSION PLATFORM=SHA256 [PLATFORM=SHA256 ...] @@ -21,8 +21,8 @@ import re import sys -FORMULA = "Formula/rcli.rb" -RELEASES = "https://github.com/RunanywhereAI/RCLI/releases/download" +FORMULA = "Formula/wally.rb" +RELEASES = "https://github.com/RunanywhereAI/wally/releases/download" # The archive extension per platform, since Windows would be a zip if it were # ever served by Homebrew. Adding a platform here and to the formula is all a @@ -39,7 +39,7 @@ def stamp_platform(source: str, version: str, platform: str, digest: str) -> str """Rewrite the url naming `platform` and the sha256 line beneath it.""" if platform not in SUFFIX: raise SystemExit(f"unknown platform {platform!r}; known: {', '.join(SUFFIX)}") - asset = f"rcli-{version}-{platform}.{SUFFIX[platform]}" + asset = f"wally-{version}-{platform}.{SUFFIX[platform]}" url = f"{RELEASES}/v{version}/{asset}" # Any url mentioning this platform, whatever version it currently names. diff --git a/scripts/release/update-tap.sh b/scripts/release/update-tap.sh new file mode 100755 index 0000000..3145f44 --- /dev/null +++ b/scripts/release/update-tap.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# ============================================================================= +# update-tap.sh +# +# Stamps Formula/wally.rb from a PUBLISHED GitHub Release (reads .sha256 +# sidecars) and pushes Formula/wally.rb to the Homebrew tap. +# +# ./scripts/release/update-tap.sh 0.5.0 +# +# Environment: +# WALLY_TAP_REPO Tap git remote to update (required unless DRY_RUN=1) +# WALLY_TAP_DIR Existing tap checkout to reuse (default: fresh temp clone) +# DRY_RUN=1 Render + print, do not commit/push +# ============================================================================= + +set -euo pipefail + +VERSION="${1:?usage: update-tap.sh }" +VERSION="${VERSION#v}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CLI_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +FORMULA="${CLI_ROOT}/Formula/wally.rb" +RELEASE_BASE="https://github.com/RunanywhereAI/wally/releases/download/v${VERSION}" +# The tap is the wally repo itself: install.sh taps `RunanywhereAI/wally` at this +# repo's git URL, and the formula lives in-repo at Formula/wally.rb, so the +# stamped formula is pushed back here. Override with WALLY_TAP_REPO only to +# publish to a separate homebrew-tap repo. +TAP_REPO="${WALLY_TAP_REPO:-git@github.com:RunanywhereAI/wally.git}" + +fetch_sha() { + local asset="$1" + local line + line="$(curl -fsSL "${RELEASE_BASE}/${asset}.sha256")" || + { echo "ERROR: missing release asset ${asset}.sha256 — is v${VERSION} published?" >&2; exit 1; } + echo "${line}" | awk '{print $1}' +} + +echo "Fetching release checksums for v${VERSION}..." +SHA_MAC_ARM="$(fetch_sha "wally-${VERSION}-macos-arm64.tar.gz")" + +if [[ -f "${SCRIPT_DIR}/stamp-formula.py" ]]; then + python3 "${SCRIPT_DIR}/stamp-formula.py" "${VERSION}" \ + "macos-arm64=${SHA_MAC_ARM}" +else + echo "ERROR: scripts/release/stamp-formula.py missing" >&2 + exit 1 +fi + +echo "Stamped formula:" +echo "----------------------------------------" +cat "${FORMULA}" +echo "----------------------------------------" + +if [[ "${DRY_RUN:-0}" == "1" ]]; then + echo "DRY_RUN=1 — not pushing to the tap." + exit 0 +fi + +if [[ -z "${TAP_REPO}" ]]; then + echo "ERROR: WALLY_TAP_REPO resolved empty." >&2 + exit 1 +fi + +TAP_DIR="${WALLY_TAP_DIR:-}" +if [[ -z "${TAP_DIR}" ]]; then + TAP_DIR="$(mktemp -d)/homebrew-tap" + git clone --depth 1 "${TAP_REPO}" "${TAP_DIR}" +fi + +mkdir -p "${TAP_DIR}/Formula" +cp "${FORMULA}" "${TAP_DIR}/Formula/wally.rb" +git -C "${TAP_DIR}" add Formula/wally.rb +git -C "${TAP_DIR}" commit -m "wally ${VERSION}" +git -C "${TAP_DIR}" push + +echo "Tap formula updated in ${TAP_REPO}" diff --git a/scripts/verify-release-assets.py b/scripts/release/verify-release-assets.py similarity index 94% rename from scripts/verify-release-assets.py rename to scripts/release/verify-release-assets.py index 4d689b0..d2fae77 100755 --- a/scripts/verify-release-assets.py +++ b/scripts/release/verify-release-assets.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Fail-closed validation for an RCLI release archive and SHA-256 sidecar.""" +"""Fail-closed validation for an Wally release archive and SHA-256 sidecar.""" from __future__ import annotations @@ -13,7 +13,7 @@ ASSET = re.compile( - r"^rcli-(?P[0-9]+\.[0-9]+\.[0-9]+)-" + r"^wally-(?P[0-9]+\.[0-9]+\.[0-9]+)-" r"(?Pmacos-arm64|windows-x86_64)\.(?Ptar\.gz|zip)$" ) MAX_MEMBERS = 100_000 @@ -75,7 +75,7 @@ def validate_members( raise VerificationError(f"archive is missing non-empty {readme}") if sizes.get(binary, 0) <= 0: raise VerificationError(f"archive is missing non-empty {binary}") - if executable == "rcli" and not executable_bits.get(binary, False): + if executable == "wally" and not executable_bits.get(binary, False): raise VerificationError(f"{binary} has no executable mode bit") @@ -93,7 +93,7 @@ def verify_tar(archive: pathlib.Path, expected_root: str) -> None: ) except (tarfile.TarError, OSError) as exc: raise VerificationError(f"could not read tar archive: {exc}") from exc - validate_members(members, expected_root, "rcli") + validate_members(members, expected_root, "wally") def verify_zip(archive: pathlib.Path, expected_root: str) -> None: @@ -110,7 +110,7 @@ def verify_zip(archive: pathlib.Path, expected_root: str) -> None: ) except (zipfile.BadZipFile, OSError) as exc: raise VerificationError(f"could not read zip archive: {exc}") from exc - validate_members(members, expected_root, "rcli.exe") + validate_members(members, expected_root, "wally.exe") def verify_sidecar(archive: pathlib.Path, sidecar: pathlib.Path) -> None: @@ -146,7 +146,7 @@ def verify(archive: pathlib.Path, sidecar: pathlib.Path) -> None: raise VerificationError(f"release asset is missing or empty: {archive}") verify_sidecar(archive, sidecar) platform = match.group("platform") - expected_root = f"rcli-{platform}" + expected_root = f"wally-{platform}" if match.group("suffix") == "tar.gz": verify_tar(archive, expected_root) else: diff --git a/scripts/smoke.sh b/scripts/smoke.sh deleted file mode 100755 index b6d1684..0000000 --- a/scripts/smoke.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash -# Command-surface smoke: no model download. -# -# scripts/smoke.sh -set -euo pipefail - -RCLI="${1:?usage: smoke.sh }" -if [[ ! -e "${RCLI}" ]]; then - echo "not found: ${RCLI}" >&2 - exit 1 -fi -if [[ ! -x "${RCLI}" && "${RCLI}" != *.exe && "${RCLI}" != *.EXE ]]; then - echo "not executable: ${RCLI}" >&2 - exit 1 -fi - -fail=0 -check() { - local label="$1"; shift - if "$@" >/dev/null 2>&1; then - echo " ok ${label}" - else - echo " FAIL ${label}" - fail=1 - fi -} - -echo "smoke: ${RCLI}" -check "--help" "${RCLI}" --help -check "version" "${RCLI}" version -check "help" "${RCLI}" --help -# backends is the spec name; some builds still accept engines as an alias. -if "${RCLI}" backends --help >/dev/null 2>&1; then - check "backends" "${RCLI}" backends -elif "${RCLI}" engines --help >/dev/null 2>&1; then - check "engines" "${RCLI}" engines -else - echo " FAIL backends/engines" - fail=1 -fi - -if "${RCLI}" definitely-not-a-command >/dev/null 2>&1; then - echo " FAIL unknown command was accepted" - fail=1 -else - echo " ok unknown command rejected" -fi - -exit "${fail}" diff --git a/scripts/assert-backends.sh b/scripts/test/assert-backends.sh similarity index 60% rename from scripts/assert-backends.sh rename to scripts/test/assert-backends.sh index e74937b..92d80e0 100755 --- a/scripts/assert-backends.sh +++ b/scripts/test/assert-backends.sh @@ -1,29 +1,29 @@ #!/usr/bin/env bash -# Assert `rcli backends` lists the given engine names. +# Assert `wally backends` lists the given engine names. # -# scripts/assert-backends.sh [...] +# scripts/test/assert-backends.sh [...] set -euo pipefail -RCLI="${1:?usage: assert-backends.sh ...}" +WALLY="${1:?usage: assert-backends.sh ...}" shift if [[ $# -eq 0 ]]; then - echo "usage: assert-backends.sh ..." >&2 + echo "usage: assert-backends.sh ..." >&2 exit 2 fi -if [[ ! -e "${RCLI}" ]]; then - echo "not found: ${RCLI}" >&2 +if [[ ! -e "${WALLY}" ]]; then + echo "not found: ${WALLY}" >&2 exit 1 fi out="$(mktemp)" trap 'rm -f "${out}"' EXIT json="" -if "${RCLI}" --json backends >"${out}" 2>/dev/null; then +if "${WALLY}" --json backends >"${out}" 2>/dev/null; then json="$(cat "${out}")" -elif "${RCLI}" backends --json >"${out}" 2>/dev/null; then +elif "${WALLY}" backends --json >"${out}" 2>/dev/null; then json="$(cat "${out}")" else - json="$("${RCLI}" backends 2>/dev/null || true)" + json="$("${WALLY}" backends 2>/dev/null || true)" fi if [[ -z "${json}" ]]; then diff --git a/scripts/assert-binary-backends.sh b/scripts/test/assert-binary-backends.sh similarity index 80% rename from scripts/assert-binary-backends.sh rename to scripts/test/assert-binary-backends.sh index aba38e6..85c735a 100755 --- a/scripts/assert-binary-backends.sh +++ b/scripts/test/assert-binary-backends.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -# Assert the binary actually contains the engines `rcli backends` claims. +# Assert the binary actually contains the engines `wally backends` claims. # -# scripts/assert-binary-backends.sh [...] +# scripts/test/assert-binary-backends.sh [...] # # Release bottles (especially the Swift MLX host) strip global C symbols. # Stream nm/strings into grep. `grep -q` closes the pipe early; with @@ -9,14 +9,14 @@ # for the scan. set -euo pipefail -RCLI="${1:?usage: assert-binary-backends.sh ...}" +WALLY="${1:?usage: assert-binary-backends.sh ...}" shift if [[ $# -eq 0 ]]; then - echo "usage: assert-binary-backends.sh ..." >&2 + echo "usage: assert-binary-backends.sh ..." >&2 exit 2 fi -if [[ ! -e "${RCLI}" ]]; then - echo "not found: ${RCLI}" >&2 +if [[ ! -e "${WALLY}" ]]; then + echo "not found: ${WALLY}" >&2 exit 1 fi @@ -55,7 +55,7 @@ blob_match() { set +e set +o pipefail if [[ -n "${strings_bin}" ]]; then - "${strings_bin}" -a "${RCLI}" 2>/dev/null | grep -a -qiE "${pat}" + "${strings_bin}" -a "${WALLY}" 2>/dev/null | grep -a -qiE "${pat}" if [[ $? -eq 0 ]]; then set -e set -o pipefail @@ -63,7 +63,7 @@ blob_match() { fi fi if command -v nm >/dev/null 2>&1; then - nm -a "${RCLI}" 2>/dev/null | grep -a -qiE "${pat}" + nm -a "${WALLY}" 2>/dev/null | grep -a -qiE "${pat}" if [[ $? -eq 0 ]]; then set -e set -o pipefail @@ -71,7 +71,7 @@ blob_match() { fi fi if command -v dumpbin >/dev/null 2>&1; then - dumpbin /ALL "${RCLI}" 2>/dev/null | grep -a -qiE "${pat}" + dumpbin /ALL "${WALLY}" 2>/dev/null | grep -a -qiE "${pat}" if [[ $? -eq 0 ]]; then set -e set -o pipefail @@ -84,7 +84,7 @@ blob_match() { } fail=0 -exe_dir="$(cd "$(dirname "${RCLI}")" && pwd)" +exe_dir="$(cd "$(dirname "${WALLY}")" && pwd)" for name in "$@"; do if [[ "${name}" == mlx && -d "${exe_dir}/mlx-swift_Cmlx.bundle" ]]; then echo " ok binary has mlx (mlx-swift_Cmlx.bundle)" diff --git a/scripts/e2e-linux.sh b/scripts/test/e2e-linux.sh similarity index 53% rename from scripts/e2e-linux.sh rename to scripts/test/e2e-linux.sh index e78ff3e..c9136c9 100755 --- a/scripts/e2e-linux.sh +++ b/scripts/test/e2e-linux.sh @@ -3,41 +3,41 @@ # e2e-linux.sh # # Host-side port of runanywhere-sdks/core/tests/scripts/run-cli-e2e-linux.sh. -# That original built rcli inside the SDK Docker image (RAC_BUILD_CLI=ON). -# RCLI is a kit consumer, so this drives an already-built binary instead: +# That original built wally inside the SDK Docker image (RAC_BUILD_CLI=ON). +# WALLY is a kit consumer, so this drives an already-built binary instead: # -# scripts/e2e-linux.sh [path-to-rcli] +# scripts/test/e2e-linux.sh [path-to-wally] # # Always runs modelless contract checks. Inference + hermetic pull run when -# RCLI_TEST_MODEL_DIR is set (same layout as SDK download-test-models.sh). -# Set RCLI_E2E_REQUIRE_MODELS=1 to fail closed if that directory is missing. +# WALLY_TEST_MODEL_DIR is set (same layout as SDK download-test-models.sh). +# Set WALLY_E2E_REQUIRE_MODELS=1 to fail closed if that directory is missing. # ============================================================================= set -euo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -BIN="${1:-${RCLI_BIN:-}}" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +BIN="${1:-${WALLY_BIN:-}}" if [[ -z "$BIN" ]]; then - if [[ -x "$ROOT/build/rcli" ]]; then - BIN="$ROOT/build/rcli" - elif [[ -x "$ROOT/build/rcli-cxx" ]]; then - BIN="$ROOT/build/rcli-cxx" - elif [[ -x "$ROOT/build/rcli.exe" ]]; then - BIN="$ROOT/build/rcli.exe" + if [[ -x "$ROOT/build/wally" ]]; then + BIN="$ROOT/build/wally" + elif [[ -x "$ROOT/build/wally-cxx" ]]; then + BIN="$ROOT/build/wally-cxx" + elif [[ -x "$ROOT/build/wally.exe" ]]; then + BIN="$ROOT/build/wally.exe" fi fi if [[ -z "$BIN" || ! -e "$BIN" ]]; then - echo "usage: e2e-linux.sh " >&2 + echo "usage: e2e-linux.sh " >&2 exit 1 fi -MODEL_DIR="${RCLI_TEST_MODEL_DIR:-${RAC_TEST_MODEL_DIR:-}}" -LOG_DIR="${RCLI_TEST_LOG_DIR:-$ROOT/build/cli-e2e-logs}" -HOME_DIR="${RUNANYWHERE_HOME:-$(mktemp -d "${TMPDIR:-/tmp}/rcli-e2e.XXXXXX")}" -cleanup() { [[ -z "${RCLI_KEEP_HOME:-}" ]] && rm -rf "$HOME_DIR"; } +MODEL_DIR="${WALLY_TEST_MODEL_DIR:-${RAC_TEST_MODEL_DIR:-}}" +LOG_DIR="${WALLY_TEST_LOG_DIR:-$ROOT/build/cli-e2e-logs}" +HOME_DIR="${RUNANYWHERE_HOME:-$(mktemp -d "${TMPDIR:-/tmp}/wally-e2e.XXXXXX")}" +cleanup() { [[ -z "${WALLY_KEEP_HOME:-}" ]] && rm -rf "$HOME_DIR"; } trap cleanup EXIT mkdir -p "$LOG_DIR" "$HOME_DIR" -rcli() { "$BIN" --home "$HOME_DIR" "$@"; } +wally() { "$BIN" --home "$HOME_DIR" "$@"; } pass=0 fail=0 @@ -56,25 +56,25 @@ check() { fi } -smoke_version() { rcli version | grep -E 'rcli|[0-9]+\.[0-9]+'; } +smoke_version() { wally version | grep -E 'wally|[0-9]+\.[0-9]+'; } smoke_backends() { local out - out="$(rcli backends)" + out="$(wally backends)" echo "$out" echo "$out" | grep -qiE "llamacpp|llama" echo "$out" | grep -qiE "sherpa|onnx" } -smoke_list_all() { rcli list --all; } +smoke_list_all() { wally list --all; } smoke_info_json() { - rcli --json info | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d.get("rcli") or d.get("version")' + wally --json info | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d.get("wally") or d.get("version")' } smoke_unknown() { set +e - rcli definitely-not-a-command + wally definitely-not-a-command local code=$? set -e test "$code" -ne 0 @@ -92,10 +92,10 @@ hermetic_pull_rm() { curl -sf http://127.0.0.1:8077/ >/dev/null 2>&1 && break sleep 1 done - rcli --no-progress pull http://127.0.0.1:8077/silero_vad.onnx - rcli list | grep -q silero_vad - rcli rm silero_vad --force - ! rcli list | grep -q silero_vad + wally --no-progress pull http://127.0.0.1:8077/silero_vad.onnx + wally list | grep -q silero_vad + wally rm silero_vad --force + ! wally list | grep -q silero_vad pkill -f "http.server 8077" || true rm -rf "$stage" } @@ -109,39 +109,39 @@ stage_canonical() { llm_one_shot() { stage_canonical local out - out="$(rcli run qwen3-0.6b 'Reply with exactly: OK' --no-think --max-tokens 32)" + out="$(wally run qwen3-0.6b 'Reply with exactly: OK' --no-think --max-tokens 32)" echo "LLM said: $out" test -n "$out" } tts_stt_roundtrip() { - rcli --no-progress pull piper || rcli --no-progress pull piper-en - rcli tts --text "RunAnywhere runs models on device." --output /tmp/rcli-e2e-tts.wav - test -s /tmp/rcli-e2e-tts.wav - rcli --no-progress pull whisper-tiny + wally --no-progress pull piper || wally --no-progress pull piper-en + wally tts --text "RunAnywhere runs models on device." --output /tmp/wally-e2e-tts.wav + test -s /tmp/wally-e2e-tts.wav + wally --no-progress pull whisper-tiny local transcript - transcript="$(rcli stt --input /tmp/rcli-e2e-tts.wav)" + transcript="$(wally stt --input /tmp/wally-e2e-tts.wav)" echo "Transcript: $transcript" echo "$transcript" | grep -iE "run|anywhere|models|device" } vad_segments() { - rcli --no-progress pull piper || rcli --no-progress pull piper-en - rcli tts --text "Testing voice activity detection." --output /tmp/rcli-e2e-vad.wav - rcli --json vad --input /tmp/rcli-e2e-vad.wav | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d.get("segments") or d.get("speech") or isinstance(d, (dict, list))' + wally --no-progress pull piper || wally --no-progress pull piper-en + wally tts --text "Testing voice activity detection." --output /tmp/wally-e2e-vad.wav + wally --json vad --input /tmp/wally-e2e-vad.wav | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d.get("segments") or d.get("speech") or isinstance(d, (dict, list))' } voice_turn() { - rcli --no-progress pull piper || rcli --no-progress pull piper-en - rcli --no-progress pull whisper-tiny - rcli tts --text "Hello there." --output /tmp/rcli-e2e-turn.wav - rcli --json voice --input /tmp/rcli-e2e-turn.wav --output /tmp/rcli-e2e-reply.wav | python3 -c 'import json,sys; json.load(sys.stdin)' - test -s /tmp/rcli-e2e-reply.wav + wally --no-progress pull piper || wally --no-progress pull piper-en + wally --no-progress pull whisper-tiny + wally tts --text "Hello there." --output /tmp/wally-e2e-turn.wav + wally --json voice --input /tmp/wally-e2e-turn.wav --output /tmp/wally-e2e-reply.wav | python3 -c 'import json,sys; json.load(sys.stdin)' + test -s /tmp/wally-e2e-reply.wav } serve_health() { stage_canonical - rcli serve qwen3-0.6b --port 8090 >/tmp/rcli-e2e-serve.log 2>&1 /tmp/wally-e2e-serve.log 2>&1 }" echo "logs: $LOG_DIR" @@ -179,12 +179,12 @@ check smoke_info_json check smoke_unknown if [[ -z "$MODEL_DIR" ]]; then - if [[ "${RCLI_E2E_REQUIRE_MODELS:-0}" == "1" ]]; then - echo "RCLI_TEST_MODEL_DIR is required (RCLI_E2E_REQUIRE_MODELS=1)" >&2 + if [[ "${WALLY_E2E_REQUIRE_MODELS:-0}" == "1" ]]; then + echo "WALLY_TEST_MODEL_DIR is required (WALLY_E2E_REQUIRE_MODELS=1)" >&2 exit 1 fi echo - echo " skip inference (set RCLI_TEST_MODEL_DIR to enable)" + echo " skip inference (set WALLY_TEST_MODEL_DIR to enable)" else echo echo "==> Hermetic pull / rm (loopback HTTP, no WAN)" @@ -205,5 +205,5 @@ if [[ "$fail" -gt 0 ]]; then echo "Logs: $LOG_DIR" exit 1 fi -echo "All rcli e2e cases passed" +echo "All wally e2e cases passed" echo "Logs: $LOG_DIR" diff --git a/scripts/e2e-modalities.sh b/scripts/test/e2e-modalities.sh similarity index 75% rename from scripts/e2e-modalities.sh rename to scripts/test/e2e-modalities.sh index ccccb4f..b47afec 100755 --- a/scripts/e2e-modalities.sh +++ b/scripts/test/e2e-modalities.sh @@ -1,17 +1,17 @@ #!/usr/bin/env bash -# Engine-agnostic modality e2e. Invoked from scripts/e2e.sh. +# Engine-agnostic modality e2e. Invoked from scripts/test/e2e.sh. # # Tests are keyed by primitive (llm, stt, tts, vlm, embed, image, vad, rerank, -# segment), not by engine name. rcli routes via catalog framework / local path -# / plugin priority. --engine is never required; set RCLI_E2E_ENGINE only to +# segment), not by engine name. wally routes via catalog framework / local path +# / plugin priority. --engine is never required; set WALLY_E2E_ENGINE only to # force an overlay backend. # # Discovery (first hit wins per modality): -# 1. RCLI_E2E_ — catalog id or on-disk path -# 2. Legacy RCLI_E2E_{MODEL,MLX_MODEL,NEURT_MODEL,QHEXRT_MODEL} -# 3. Scan RCLI_E2E_MODEL_ROOTS + well-known dirs for *_HNPU / CoreML trees -# 4. `rcli models list --json` downloaded rows whose modality matches -# 5. If RCLI_E2E_AUTO=1, small OSS catalog defaults the binary can run +# 1. WALLY_E2E_ — catalog id or on-disk path +# 2. Legacy WALLY_E2E_{MODEL,MLX_MODEL,NEURT_MODEL,QHEXRT_MODEL} +# 3. Scan WALLY_E2E_MODEL_ROOTS + well-known dirs for *_HNPU / CoreML trees +# 4. `wally models list --json` downloaded rows whose modality matches +# 5. If WALLY_E2E_AUTO=1, small OSS catalog defaults the binary can run # # Skip = no model. Fail = a model was selected and the command failed. # Public CI leaves every knob unset and stays green. @@ -20,7 +20,7 @@ # under pipefail — SIGPIPE makes the whole script fail. set -euo pipefail -RCLI="${1:?usage: e2e-modalities.sh }" +WALLY="${1:?usage: e2e-modalities.sh }" pass=0 fail=0 @@ -79,11 +79,11 @@ set_mod() { } engine_args=() -if [[ -n "${RCLI_E2E_ENGINE:-}" ]]; then - engine_args=(--engine "${RCLI_E2E_ENGINE}") +if [[ -n "${WALLY_E2E_ENGINE:-}" ]]; then + engine_args=(--engine "${WALLY_E2E_ENGINE}") fi -workdir="$(mktemp -d "${TMPDIR:-/tmp}/rcli-e2e-mod.XXXXXX")" +workdir="$(mktemp -d "${TMPDIR:-/tmp}/wally-e2e-mod.XXXXXX")" cleanup() { rm -rf "${workdir}"; } trap cleanup EXIT @@ -163,7 +163,7 @@ write_wav write_ppm write_png || true -backend_json="$("${RCLI}" --json backends 2>/dev/null || true)" +backend_json="$("${WALLY}" --json backends 2>/dev/null || true)" has_backend() { local name="$1" printf '%s' "${backend_json}" | grep -F "\"${name}\"" >/dev/null 2>&1 || return 1 @@ -188,26 +188,26 @@ guess_mod() { } # 1. explicit per-modality env -set_mod llm "${RCLI_E2E_LLM:-${RCLI_E2E_MODEL:-}}" -set_mod stt "${RCLI_E2E_STT:-}" -set_mod tts "${RCLI_E2E_TTS:-}" -set_mod vlm "${RCLI_E2E_VLM:-}" -set_mod embed "${RCLI_E2E_EMBED:-}" -set_mod image "${RCLI_E2E_IMAGE:-}" -set_mod vad "${RCLI_E2E_VAD:-}" -set_mod rerank "${RCLI_E2E_RERANK:-}" -set_mod segment "${RCLI_E2E_SEGMENT:-}" -set_mod diarize "${RCLI_E2E_DIARIZE:-}" +set_mod llm "${WALLY_E2E_LLM:-${WALLY_E2E_MODEL:-}}" +set_mod stt "${WALLY_E2E_STT:-}" +set_mod tts "${WALLY_E2E_TTS:-}" +set_mod vlm "${WALLY_E2E_VLM:-}" +set_mod embed "${WALLY_E2E_EMBED:-}" +set_mod image "${WALLY_E2E_IMAGE:-}" +set_mod vad "${WALLY_E2E_VAD:-}" +set_mod rerank "${WALLY_E2E_RERANK:-}" +set_mod segment "${WALLY_E2E_SEGMENT:-}" +set_mod diarize "${WALLY_E2E_DIARIZE:-}" # 2. legacy engine knobs (still accepted; tests stay modality-keyed) -if [[ -n "${RCLI_E2E_MLX_MODEL:-}" ]]; then - set_mod "$(guess_mod "${RCLI_E2E_MLX_MODEL}")" "${RCLI_E2E_MLX_MODEL}" +if [[ -n "${WALLY_E2E_MLX_MODEL:-}" ]]; then + set_mod "$(guess_mod "${WALLY_E2E_MLX_MODEL}")" "${WALLY_E2E_MLX_MODEL}" fi -if [[ -n "${RCLI_E2E_NEURT_MODEL:-}" ]]; then - set_mod "$(guess_mod "${RCLI_E2E_NEURT_MODEL}")" "${RCLI_E2E_NEURT_MODEL}" +if [[ -n "${WALLY_E2E_NEURT_MODEL:-}" ]]; then + set_mod "$(guess_mod "${WALLY_E2E_NEURT_MODEL}")" "${WALLY_E2E_NEURT_MODEL}" fi -if [[ -n "${RCLI_E2E_QHEXRT_MODEL:-}" ]]; then - set_mod "$(guess_mod "${RCLI_E2E_QHEXRT_MODEL}")" "${RCLI_E2E_QHEXRT_MODEL}" +if [[ -n "${WALLY_E2E_QHEXRT_MODEL:-}" ]]; then + set_mod "$(guess_mod "${WALLY_E2E_QHEXRT_MODEL}")" "${WALLY_E2E_QHEXRT_MODEL}" fi # 3. scan well-known roots @@ -225,11 +225,11 @@ append_root() { fi } -if [[ -n "${RCLI_E2E_MODEL_ROOTS:-}" ]]; then +if [[ -n "${WALLY_E2E_MODEL_ROOTS:-}" ]]; then old_ifs="${IFS}" IFS=':;' # shellcheck disable=SC2086 - for r in ${RCLI_E2E_MODEL_ROOTS}; do + for r in ${WALLY_E2E_MODEL_ROOTS}; do append_root "${r}" done IFS="${old_ifs}" @@ -286,7 +286,7 @@ IFS="${old_ifs}" # 4. downloaded catalog rows list_json_file="${workdir}/models.json" -if "${RCLI}" --json models list >"${list_json_file}" 2>/dev/null; then +if "${WALLY}" --json models list >"${list_json_file}" 2>/dev/null; then if [[ -s "${list_json_file}" && -n "${python_bin}" ]]; then while IFS=$'\t' read -r mod ref; do [[ -n "${mod}" && -n "${ref}" ]] || continue @@ -320,7 +320,7 @@ PY fi # 5. AUTO: small OSS catalog ids the registered backends can serve. -if [[ "${RCLI_E2E_AUTO:-0}" == "1" ]]; then +if [[ "${WALLY_E2E_AUTO:-0}" == "1" ]]; then if has_backend llamacpp; then set_mod llm smollm2 set_mod vlm smolvlm2 @@ -345,9 +345,9 @@ if [[ "${RCLI_E2E_AUTO:-0}" == "1" ]]; then fi fi -echo "e2e (modalities, engine-agnostic): ${RCLI}" -if [[ -n "${RCLI_E2E_ENGINE:-}" ]]; then - echo " note RCLI_E2E_ENGINE=${RCLI_E2E_ENGINE} (override only)" +echo "e2e (modalities, engine-agnostic): ${WALLY}" +if [[ -n "${WALLY_E2E_ENGINE:-}" ]]; then + echo " note WALLY_E2E_ENGINE=${WALLY_E2E_ENGINE} (override only)" fi for mod in llm stt tts vlm embed image vad rerank segment diarize; do ref="$(get_mod "${mod}")" @@ -375,21 +375,21 @@ run_cmd() { bad "${label}${err:+ (${err})}" } -# --- runners (no --engine unless RCLI_E2E_ENGINE is set) -------------------- +# --- runners (no --engine unless WALLY_E2E_ENGINE is set) -------------------- if [[ -n "${MOD_LLM}" ]]; then run_cmd "llm ${MOD_LLM}" \ - "${RCLI}" llm generate -m "${MOD_LLM}" ${engine_args[@]+"${engine_args[@]}"} \ + "${WALLY}" llm generate -m "${MOD_LLM}" ${engine_args[@]+"${engine_args[@]}"} \ "Reply with exactly: ok" --max-output-tokens 16 else - skipm "llm (set RCLI_E2E_LLM or place a model under RCLI_E2E_MODEL_ROOTS)" + skipm "llm (set WALLY_E2E_LLM or place a model under WALLY_E2E_MODEL_ROOTS)" fi if [[ -n "${MOD_TTS}" ]]; then run_cmd "tts ${MOD_TTS}" \ - "${RCLI}" tts synthesize "hello from rcli" --output "${tts_wav}" \ + "${WALLY}" tts synthesize "hello from wally" --output "${tts_wav}" \ -m "${MOD_TTS}" else - skipm "tts (set RCLI_E2E_TTS)" + skipm "tts (set WALLY_E2E_TTS)" fi if [[ -n "${MOD_STT}" ]]; then @@ -398,61 +398,61 @@ if [[ -n "${MOD_STT}" ]]; then stt_in="${tts_wav}" fi run_cmd "stt ${MOD_STT}" \ - "${RCLI}" stt transcribe -m "${MOD_STT}" "${stt_in}" + "${WALLY}" stt transcribe -m "${MOD_STT}" "${stt_in}" else - skipm "stt (set RCLI_E2E_STT)" + skipm "stt (set WALLY_E2E_STT)" fi if [[ -n "${MOD_VLM}" && -s "${png}" ]]; then run_cmd "vlm ${MOD_VLM}" \ - "${RCLI}" vlm generate -m "${MOD_VLM}" ${engine_args[@]+"${engine_args[@]}"} \ + "${WALLY}" vlm generate -m "${MOD_VLM}" ${engine_args[@]+"${engine_args[@]}"} \ --image "${png}" "What color is this?" --max-output-tokens 16 else - skipm "vlm (set RCLI_E2E_VLM)" + skipm "vlm (set WALLY_E2E_VLM)" fi if [[ -n "${MOD_EMBED}" ]]; then run_cmd "embed ${MOD_EMBED}" \ - "${RCLI}" embed -m "${MOD_EMBED}" ${engine_args[@]+"${engine_args[@]}"} "hello" + "${WALLY}" embed -m "${MOD_EMBED}" ${engine_args[@]+"${engine_args[@]}"} "hello" else - skipm "embed (set RCLI_E2E_EMBED)" + skipm "embed (set WALLY_E2E_EMBED)" fi if [[ -n "${MOD_IMAGE}" ]]; then run_cmd "image ${MOD_IMAGE}" \ - "${RCLI}" image generate --model "${MOD_IMAGE}" \ + "${WALLY}" image generate --model "${MOD_IMAGE}" \ --prompt "a red square" --out "${img_out}" --steps 4 else - skipm "image (set RCLI_E2E_IMAGE to a compiled CoreML / HNPU diffusion tree)" + skipm "image (set WALLY_E2E_IMAGE to a compiled CoreML / HNPU diffusion tree)" fi if [[ -n "${MOD_VAD}" ]]; then run_cmd "vad ${MOD_VAD}" \ - "${RCLI}" vad detect -m "${MOD_VAD}" "${wav}" + "${WALLY}" vad detect -m "${MOD_VAD}" "${wav}" else - skipm "vad (set RCLI_E2E_VAD)" + skipm "vad (set WALLY_E2E_VAD)" fi if [[ -n "${MOD_RERANK}" ]]; then run_cmd "rerank ${MOD_RERANK}" \ - "${RCLI}" rerank -m "${MOD_RERANK}" "fruit" \ + "${WALLY}" rerank -m "${MOD_RERANK}" "fruit" \ --doc "bananas are yellow" --doc "steel is a metal" else - skipm "rerank (set RCLI_E2E_RERANK)" + skipm "rerank (set WALLY_E2E_RERANK)" fi if [[ -n "${MOD_SEGMENT}" ]]; then run_cmd "segment ${MOD_SEGMENT}" \ - "${RCLI}" segment -m "${MOD_SEGMENT}" "${ppm}" + "${WALLY}" segment -m "${MOD_SEGMENT}" "${ppm}" else - skipm "segment (set RCLI_E2E_SEGMENT; input is binary P6 PPM)" + skipm "segment (set WALLY_E2E_SEGMENT; input is binary P6 PPM)" fi if [[ -n "${MOD_DIARIZE}" ]]; then run_cmd "diarize ${MOD_DIARIZE}" \ - "${RCLI}" diarize -m "${MOD_DIARIZE}" "${wav}" + "${WALLY}" diarize -m "${MOD_DIARIZE}" "${wav}" else - skipm "diarize (set RCLI_E2E_DIARIZE)" + skipm "diarize (set WALLY_E2E_DIARIZE)" fi echo "modalities: ${pass} passed, ${fail} failed, ${skip} skipped" diff --git a/scripts/e2e.sh b/scripts/test/e2e.sh similarity index 68% rename from scripts/e2e.sh rename to scripts/test/e2e.sh index 2d52017..c96d047 100755 --- a/scripts/e2e.sh +++ b/scripts/test/e2e.sh @@ -1,31 +1,31 @@ #!/usr/bin/env bash -# Kit-consumer e2e for a built rcli binary. Does not compile the SDK. +# Kit-consumer e2e for a built wally binary. Does not compile the SDK. # -# scripts/e2e.sh +# scripts/test/e2e.sh # -# Always runs scripts/smoke.sh (no model download). Set RCLI_E2E_MODEL to also +# Always runs scripts/test/smoke.sh (no model download). Set WALLY_E2E_MODEL to also # pull a catalog model and run one generation — that needs network + disk. -# Overlay / device round-trips live in scripts/e2e-modalities.sh and are keyed +# Overlay / device round-trips live in scripts/test/e2e-modalities.sh and are keyed # by primitive, not engine. Public CI leaves those knobs unset (skip). -# CMAKE_PREFIX_PATH is optional; RCLI_SDK_KIT is preferred. Unset is fine. +# CMAKE_PREFIX_PATH is optional; WALLY_SDK_KIT is preferred. Unset is fine. set -euo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -RCLI="${1:?usage: e2e.sh }" -if [[ ! -e "${RCLI}" ]]; then - echo "not found: ${RCLI}" >&2 +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +WALLY="${1:?usage: e2e.sh }" +if [[ ! -e "${WALLY}" ]]; then + echo "not found: ${WALLY}" >&2 exit 1 fi -if [[ ! -x "${RCLI}" && "${RCLI}" != *.exe && "${RCLI}" != *.EXE ]]; then - echo "not executable: ${RCLI}" >&2 +if [[ ! -x "${WALLY}" && "${WALLY}" != *.exe && "${WALLY}" != *.EXE ]]; then + echo "not executable: ${WALLY}" >&2 exit 1 fi kit_cfg="" -kit_root="${RCLI_SDK_KIT:-${CMAKE_PREFIX_PATH-}}" +kit_root="${WALLY_SDK_KIT:-${CMAKE_PREFIX_PATH-}}" # CMAKE_PREFIX_PATH may be a list: ':' separated on POSIX, ';' on Windows. Take # the first entry, but never split a Windows path at its drive-letter colon -- -# doing that turned "D:/a/RCLI/kit" into "D", the kit Config was then never +# doing that turned "D:/a/WALLY/kit" into "D", the kit Config was then never # found, and the backend check silently fell back to expecting llamacpp + onnx # + sherpa. On x64 that default happens to be correct so the bug stayed hidden; # on ARM64, whose kit ships none of them, it failed the job. @@ -51,10 +51,10 @@ if [[ -n "${kit_root}" ]]; then fi # Win32 LoadLibrary searches the exe directory, then PATH. Stage kit DLLs -# next to rcli.exe and put third_party on PATH so onnx/sherpa can register +# next to wally.exe and put third_party on PATH so onnx/sherpa can register # even if CMake's configure-time GLOB missed a file. -if [[ "${RCLI}" == *.exe || "${RCLI}" == *.EXE ]] && [[ -n "${kit_root}" ]]; then - exe_dir="$(cd "$(dirname "${RCLI}")" && pwd)" +if [[ "${WALLY}" == *.exe || "${WALLY}" == *.EXE ]] && [[ -n "${kit_root}" ]]; then + exe_dir="$(cd "$(dirname "${WALLY}")" && pwd)" for d in "${kit_root}/third_party" "${kit_root}/bin" "${kit_root}/lib"; do [[ -d "${d}" ]] || continue if command -v cygpath >/dev/null 2>&1; then @@ -67,7 +67,7 @@ if [[ "${RCLI}" == *.exe || "${RCLI}" == *.EXE ]] && [[ -n "${kit_root}" ]]; the export PATH fi -bash "${ROOT}/scripts/smoke.sh" "${RCLI}" +bash "${ROOT}/scripts/test/smoke.sh" "${WALLY}" fail=0 check() { @@ -80,12 +80,12 @@ check() { fi } -echo "e2e (modelless extras): ${RCLI}" -check "info" "${RCLI}" info +echo "e2e (modelless extras): ${WALLY}" +check "info" "${WALLY}" info # Assert the engines this binary's kit actually ships. A 0.20.26 Windows kit # may only advertise llamacpp; 0.20.28+ kits advertise onnx + sherpa too. -# Product `rcli` on Apple Silicon also includes MLX. Intermediate `rcli-cxx` does not. +# Product `wally` on Apple Silicon also includes MLX. Intermediate `wally-cxx` does not. expected_backends=() kit_flag_true() { @@ -108,16 +108,16 @@ else expected_backends+=(sherpa) fi fi -base="$(basename "${RCLI}")" +base="$(basename "${WALLY}")" base="${base%.exe}" -if [[ "$(uname -s)" == Darwin && "$(uname -m)" == arm64 && "${base}" == "rcli" ]]; then +if [[ "$(uname -s)" == Darwin && "$(uname -m)" == arm64 && "${base}" == "wally" ]]; then expected_backends+=(mlx) fi # Overlay archives flip HAS_NEURT/HAS_QHEXRT at find_package time, but the # packaged Config.cmake still says FALSE. Presence of the backend library is -# the source of truth. Only RCLI_SDK_KIT counts — ambient CMAKE_PREFIX_PATH +# the source of truth. Only WALLY_SDK_KIT counts — ambient CMAKE_PREFIX_PATH # from an overlay rebuild must not fail a public-bottle e2e. -overlay_root="${RCLI_SDK_KIT-}" +overlay_root="${WALLY_SDK_KIT-}" if [[ -n "${overlay_root}" ]] && command -v cygpath >/dev/null 2>&1; then overlay_root="$(cygpath -u "${overlay_root}")" fi @@ -131,47 +131,47 @@ if [[ -n "${overlay_root}" ]]; then fi if [[ ${#expected_backends[@]} -eq 0 ]]; then echo " skip backends (kit has no engines)" -elif bash "${ROOT}/scripts/assert-backends.sh" "${RCLI}" "${expected_backends[@]}"; then +elif bash "${ROOT}/scripts/test/assert-backends.sh" "${WALLY}" "${expected_backends[@]}"; then echo " ok backends ${expected_backends[*]}" else echo " FAIL backends ${expected_backends[*]}" fail=1 fi if [[ ${#expected_backends[@]} -gt 0 ]]; then - if bash "${ROOT}/scripts/assert-binary-backends.sh" "${RCLI}" "${expected_backends[@]}"; then + if bash "${ROOT}/scripts/test/assert-binary-backends.sh" "${WALLY}" "${expected_backends[@]}"; then echo " ok binary ${expected_backends[*]}" else echo " FAIL binary ${expected_backends[*]}" fail=1 fi fi -if "${RCLI}" models list --help >/dev/null 2>&1; then - check "models list" "${RCLI}" models list +if "${WALLY}" models list --help >/dev/null 2>&1; then + check "models list" "${WALLY}" models list fi # Per-backend help surfaces that only compile in when the engine is linked. for name in "${expected_backends[@]}"; do case "${name}" in neurt) - check "image generate --help" "${RCLI}" image generate --help + check "image generate --help" "${WALLY}" image generate --help ;; mlx|qhexrt) - check "run --help" "${RCLI}" run --help + check "run --help" "${WALLY}" run --help ;; esac done # Modality round-trips (engine-agnostic). Skip when no model is discovered. -# RCLI_E2E_MODALITIES=0 disables this so public CI can stay modelless-only. -if [[ "${RCLI_E2E_MODALITIES:-1}" != "0" ]]; then - if bash "${ROOT}/scripts/e2e-modalities.sh" "${RCLI}"; then +# WALLY_E2E_MODALITIES=0 disables this so public CI can stay modelless-only. +if [[ "${WALLY_E2E_MODALITIES:-1}" != "0" ]]; then + if bash "${ROOT}/scripts/test/e2e-modalities.sh" "${WALLY}"; then echo " ok modalities" else echo " FAIL modalities" fail=1 fi else - echo " skip modalities (RCLI_E2E_MODALITIES=0)" + echo " skip modalities (WALLY_E2E_MODALITIES=0)" fi exit "${fail}" diff --git a/scripts/smoke-mlx-cli.sh b/scripts/test/smoke-mlx-cli.sh similarity index 55% rename from scripts/smoke-mlx-cli.sh rename to scripts/test/smoke-mlx-cli.sh index 922083d..52b268a 100755 --- a/scripts/smoke-mlx-cli.sh +++ b/scripts/test/smoke-mlx-cli.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -# Compatibility name for the SDK-era rcli/scripts/smoke-mlx-cli.sh. +# Compatibility name for the SDK-era wally/scripts/test/smoke-mlx-cli.sh. exec "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/smoke-mlx.sh" "$@" diff --git a/scripts/smoke-mlx.sh b/scripts/test/smoke-mlx.sh similarity index 53% rename from scripts/smoke-mlx.sh rename to scripts/test/smoke-mlx.sh index 9777688..2ee23c1 100755 --- a/scripts/smoke-mlx.sh +++ b/scripts/test/smoke-mlx.sh @@ -1,19 +1,19 @@ #!/usr/bin/env bash -# MLX smoke — port of runanywhere-sdks/rcli/scripts/smoke-mlx-cli.sh +# MLX smoke — port of runanywhere-sdks/wally/scripts/test/smoke-mlx-cli.sh # # Builds the Swift MLX host, then exercises backends, LLM, TTS, STT, and VLM -# against the product `rcli` binary (not the SDK DevTools playground). +# against the product `wally` binary (not the SDK DevTools playground). set -euo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -BIN="${RCLI_BIN:-$ROOT/build/rcli}" -HOME_DIR="${RCLI_HOME:-${RUNANYWHERE_MLX_SMOKE_HOME:-/tmp/rcli-mlx-smoke}}" -PULL="${RCLI_SMOKE_PULL:-${RUNANYWHERE_MLX_SMOKE_PULL:-1}}" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +BIN="${WALLY_BIN:-$ROOT/build/wally}" +HOME_DIR="${WALLY_HOME:-${RUNANYWHERE_MLX_SMOKE_HOME:-/tmp/wally-mlx-smoke}}" +PULL="${WALLY_SMOKE_PULL:-${RUNANYWHERE_MLX_SMOKE_PULL:-1}}" -LLM_MODEL="${RCLI_SMOKE_LLM:-${RUNANYWHERE_MLX_SMOKE_LLM:-mlx-qwen3-0.6b-4bit}}" -VLM_MODEL="${RCLI_SMOKE_VLM:-${RUNANYWHERE_MLX_SMOKE_VLM:-mlx-fastvlm-0.5b-bf16}}" -STT_MODEL="${RCLI_SMOKE_STT:-${RUNANYWHERE_MLX_SMOKE_STT:-mlx-qwen3-asr-0.6b-8bit}}" -TTS_MODEL="${RCLI_SMOKE_TTS:-${RUNANYWHERE_MLX_SMOKE_TTS:-mlx-soprano-1.1-80m-5bit}}" +LLM_MODEL="${WALLY_SMOKE_LLM:-${RUNANYWHERE_MLX_SMOKE_LLM:-mlx-qwen3-0.6b-4bit}}" +VLM_MODEL="${WALLY_SMOKE_VLM:-${RUNANYWHERE_MLX_SMOKE_VLM:-mlx-fastvlm-0.5b-bf16}}" +STT_MODEL="${WALLY_SMOKE_STT:-${RUNANYWHERE_MLX_SMOKE_STT:-mlx-qwen3-asr-0.6b-8bit}}" +TTS_MODEL="${WALLY_SMOKE_TTS:-${RUNANYWHERE_MLX_SMOKE_TTS:-mlx-soprano-1.1-80m-5bit}}" if [[ "$(uname -s)" != "Darwin" ]]; then echo "smoke-mlx: Darwin only" >&2 @@ -24,19 +24,19 @@ if [[ "$(uname -m)" != "arm64" ]]; then exit 0 fi -bash "$ROOT/scripts/build-mlx.sh" +bash "$ROOT/scripts/build/build-mlx.sh" if [[ ! -x "$BIN" ]]; then - echo "error: missing $BIN (scripts/build-mlx.sh should install it as build/rcli)" >&2 + echo "error: missing $BIN (scripts/build/build-mlx.sh should install it as build/wally)" >&2 exit 1 fi mkdir -p "$HOME_DIR" -rcli() { "$BIN" --home "$HOME_DIR" "$@"; } +wally() { "$BIN" --home "$HOME_DIR" "$@"; } pull_if_enabled() { local model="$1" if [[ "$PULL" == "1" ]]; then - rcli pull "$model" + wally pull "$model" fi } @@ -65,12 +65,12 @@ PNG } echo "MLX backend smoke" -rcli --json backends | grep -q '"name":"mlx"' -rcli --json backends | grep -q '"name":"llamacpp"' +wally --json backends | grep -q '"name":"mlx"' +wally --json backends | grep -q '"name":"llamacpp"' echo "LLM: $LLM_MODEL" pull_if_enabled "$LLM_MODEL" -llm_out="$(rcli run "$LLM_MODEL" "Say OK in one short sentence." --max-tokens 16 --temp 0.1)" +llm_out="$(wally run "$LLM_MODEL" "Say OK in one short sentence." --max-tokens 16 --temp 0.1)" require_text "LLM" "$llm_out" printf '%s\n' "$llm_out" @@ -78,22 +78,22 @@ echo "TTS: $TTS_MODEL" pull_if_enabled "$TTS_MODEL" tts_wav="$HOME_DIR/mlx-smoke-tts.wav" rm -f "$tts_wav" -rcli tts --model "$TTS_MODEL" --text "Hello from MLX text to speech." --output "$tts_wav" +wally tts --model "$TTS_MODEL" --text "Hello from MLX text to speech." --output "$tts_wav" require_file "$tts_wav" echo "STT: $STT_MODEL" pull_if_enabled "$STT_MODEL" -stt_out="$(rcli stt --model "$STT_MODEL" --input "$tts_wav")" +stt_out="$(wally stt --model "$STT_MODEL" --input "$tts_wav")" require_text "STT" "$stt_out" printf '%s\n' "$stt_out" echo "VLM: $VLM_MODEL" pull_if_enabled "$VLM_MODEL" -image_path="${RCLI_SMOKE_IMAGE:-${RUNANYWHERE_MLX_SMOKE_IMAGE:-$HOME_DIR/mlx-smoke-image.png}}" -if [[ -z "${RCLI_SMOKE_IMAGE:-${RUNANYWHERE_MLX_SMOKE_IMAGE:-}}" ]]; then +image_path="${WALLY_SMOKE_IMAGE:-${RUNANYWHERE_MLX_SMOKE_IMAGE:-$HOME_DIR/mlx-smoke-image.png}}" +if [[ -z "${WALLY_SMOKE_IMAGE:-${RUNANYWHERE_MLX_SMOKE_IMAGE:-}}" ]]; then make_default_image "$image_path" fi -vlm_out="$(rcli run "$VLM_MODEL" --image "$image_path" \ +vlm_out="$(wally run "$VLM_MODEL" --image "$image_path" \ "Describe the image in one short sentence." --max-tokens 32 --temp 0.1)" require_text "VLM" "$vlm_out" printf '%s\n' "$vlm_out" diff --git a/scripts/test/smoke.sh b/scripts/test/smoke.sh new file mode 100755 index 0000000..531e043 --- /dev/null +++ b/scripts/test/smoke.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Command-surface smoke: no model download. +# +# scripts/test/smoke.sh +set -euo pipefail + +WALLY="${1:?usage: smoke.sh }" +if [[ ! -e "${WALLY}" ]]; then + echo "not found: ${WALLY}" >&2 + exit 1 +fi +if [[ ! -x "${WALLY}" && "${WALLY}" != *.exe && "${WALLY}" != *.EXE ]]; then + echo "not executable: ${WALLY}" >&2 + exit 1 +fi + +fail=0 +check() { + local label="$1"; shift + if "$@" >/dev/null 2>&1; then + echo " ok ${label}" + else + echo " FAIL ${label}" + fail=1 + fi +} + +echo "smoke: ${WALLY}" +check "--help" "${WALLY}" --help +check "version" "${WALLY}" version +check "help" "${WALLY}" --help +# backends is the spec name; some builds still accept engines as an alias. +if "${WALLY}" backends --help >/dev/null 2>&1; then + check "backends" "${WALLY}" backends +elif "${WALLY}" engines --help >/dev/null 2>&1; then + check "engines" "${WALLY}" engines +else + echo " FAIL backends/engines" + fail=1 +fi + +if "${WALLY}" definitely-not-a-command >/dev/null 2>&1; then + echo " FAIL unknown command was accepted" + fail=1 +else + echo " ok unknown command rejected" +fi + +exit "${fail}" diff --git a/scripts/test-e2e.sh b/scripts/test/test-e2e.sh similarity index 51% rename from scripts/test-e2e.sh rename to scripts/test/test-e2e.sh index b341014..966efa3 100755 --- a/scripts/test-e2e.sh +++ b/scripts/test/test-e2e.sh @@ -1,21 +1,21 @@ #!/usr/bin/env bash -# Kit-consumer rewrite of runanywhere-sdks/rcli/scripts/test-e2e.sh +# Kit-consumer rewrite of runanywhere-sdks/wally/scripts/test/test-e2e.sh # # Original configured the SDK with RAC_BUILD_CLI=ON. This repo consumes a # published C++ desktop kit: # -# 1. Configure + build against CMAKE_PREFIX_PATH / RCLI_SDK_KIT +# 1. Configure + build against CMAKE_PREFIX_PATH / WALLY_SDK_KIT # 2. Offline unit/segment tests (ctest) -# 3. CLI contract smoke (scripts/smoke.sh + scripts/e2e.sh) -# 4. Optional model round-trip when RCLI_E2E_MODEL is set +# 3. CLI contract smoke (scripts/test/smoke.sh + scripts/test/e2e.sh) +# 4. Optional model round-trip when WALLY_E2E_MODEL is set set -euo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" cd "$ROOT" -KIT="${RCLI_SDK_KIT:-${CMAKE_PREFIX_PATH:-$ROOT/kit}}" -BUILD="${RCLI_BUILD_DIR:-$ROOT/build}" -JOBS="${RCLI_JOBS:-}" +KIT="${WALLY_SDK_KIT:-${CMAKE_PREFIX_PATH:-$ROOT/kit}}" +BUILD="${WALLY_BUILD_DIR:-$ROOT/build}" +JOBS="${WALLY_JOBS:-}" if [[ -z "$JOBS" ]]; then case "$(uname -s)" in Darwin) JOBS="$(sysctl -n hw.logicalcpu)" ;; @@ -32,15 +32,15 @@ bad() { printf ' FAIL %s\n' "$1"; fail=$((fail + 1)); } echo "==> Kit prefix: $KIT" if [[ ! -d "$KIT/include" ]]; then case "$(uname -s)-$(uname -m)" in - Darwin-arm64) bash "$ROOT/scripts/fetch-kit.sh" macos-arm64 "$KIT" ;; + Darwin-arm64) bash "$ROOT/scripts/build/fetch-kit.sh" macos-arm64 "$KIT" ;; *) - echo "error: no kit at $KIT — set RCLI_SDK_KIT or CMAKE_PREFIX_PATH" >&2 + echo "error: no kit at $KIT — set WALLY_SDK_KIT or CMAKE_PREFIX_PATH" >&2 exit 1 ;; esac fi -if [[ "${RCLI_E2E_KEEP_BUILD:-0}" != "1" || ! -f "$BUILD/CMakeCache.txt" ]]; then +if [[ "${WALLY_E2E_KEEP_BUILD:-0}" != "1" || ! -f "$BUILD/CMakeCache.txt" ]]; then echo "==> Configuring" cmake -B "$BUILD" -DCMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE:-Release}" \ -DCMAKE_PREFIX_PATH="$KIT" @@ -50,11 +50,11 @@ echo "==> Building" cmake --build "$BUILD" -j "$JOBS" BIN="" -for cand in "$BUILD/rcli" "$BUILD/rcli-cxx" "$BUILD/rcli.exe" "$BUILD/Release/rcli.exe"; do +for cand in "$BUILD/wally" "$BUILD/wally-cxx" "$BUILD/wally.exe" "$BUILD/Release/wally.exe"; do if [[ -e "$cand" ]]; then BIN="$cand"; break; fi done -[[ -n "$BIN" ]] || { echo "rcli binary not found under $BUILD" >&2; exit 1; } -echo "rcli: $BIN" +[[ -n "$BIN" ]] || { echo "wally binary not found under $BUILD" >&2; exit 1; } +echo "wally: $BIN" echo "==> Offline tests (ctest)" if ctest --test-dir "$BUILD" --output-on-failure ${CMAKE_BUILD_TYPE:+-C "$CMAKE_BUILD_TYPE"}; then @@ -64,8 +64,8 @@ else fi echo "==> CLI contract" -if bash "$ROOT/scripts/smoke.sh" "$BIN"; then ok "smoke.sh"; else bad "smoke.sh"; fi -if bash "$ROOT/scripts/e2e.sh" "$BIN"; then ok "e2e.sh"; else bad "e2e.sh"; fi +if bash "$ROOT/scripts/test/smoke.sh" "$BIN"; then ok "smoke.sh"; else bad "smoke.sh"; fi +if bash "$ROOT/scripts/test/e2e.sh" "$BIN"; then ok "e2e.sh"; else bad "e2e.sh"; fi echo "Summary: $pass passed, $fail failed" [[ "$fail" -eq 0 ]] diff --git a/scripts/test/test-install-skill-dirs.sh b/scripts/test/test-install-skill-dirs.sh new file mode 100755 index 0000000..68f87eb --- /dev/null +++ b/scripts/test/test-install-skill-dirs.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Proves install.sh installs the RunAnywhere skill into the agent homes that +# exist: ~/.claude (Claude Code), ~/.agents (Cursor / Codex), both, or a +# ~/.claude default on a fresh machine with neither. Drives the installer's +# --print-skill-dirs seam against throwaway HOMEs; no network, no brew. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INSTALL="${SCRIPT_DIR}/../../install.sh" + +fails=0 +run() { HOME="$1" sh "$INSTALL" --print-skill-dirs; } +check() { + name="$1"; expected="$2"; actual="$3" + if [ "$expected" = "$actual" ]; then + printf 'ok %s\n' "$name" + else + printf 'FAIL %s\n expected: %s\n actual: %s\n' "$name" "$expected" "$actual" + fails=$((fails + 1)) + fi +} + +t="$(mktemp -d)"; mkdir -p "$t/.claude" +check "claude-only" "$t/.claude/skills/runanywhere" "$(run "$t")" + +t="$(mktemp -d)"; mkdir -p "$t/.agents" +check "agents-only" "$t/.agents/skills/runanywhere" "$(run "$t")" + +t="$(mktemp -d)"; mkdir -p "$t/.claude" "$t/.agents" +check "both" "$(printf '%s\n%s' "$t/.claude/skills/runanywhere" "$t/.agents/skills/runanywhere")" "$(run "$t")" + +t="$(mktemp -d)" +check "neither-defaults-claude" "$t/.claude/skills/runanywhere" "$(run "$t")" + +[ "$fails" -eq 0 ] || { printf '%d test(s) failed\n' "$fails" >&2; exit 1; } +printf 'all skill-dir cases pass\n' diff --git a/scripts/update-tap.sh b/scripts/update-tap.sh deleted file mode 100755 index 6aa7bf3..0000000 --- a/scripts/update-tap.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================= -# update-tap.sh -# -# Stamps Formula/rcli.rb from a PUBLISHED GitHub Release (reads .sha256 -# sidecars) and pushes Formula/rcli.rb to the Homebrew tap. -# -# ./scripts/update-tap.sh 0.5.0 -# -# Environment: -# RCLI_TAP_REPO Tap git remote to update (required unless DRY_RUN=1) -# RCLI_TAP_DIR Existing tap checkout to reuse (default: fresh temp clone) -# DRY_RUN=1 Render + print, do not commit/push -# ============================================================================= - -set -euo pipefail - -VERSION="${1:?usage: update-tap.sh }" -VERSION="${VERSION#v}" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CLI_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -FORMULA="${CLI_ROOT}/Formula/rcli.rb" -RELEASE_BASE="https://github.com/RunanywhereAI/RCLI/releases/download/v${VERSION}" -TAP_REPO="${RCLI_TAP_REPO:-}" - -fetch_sha() { - local asset="$1" - local line - line="$(curl -fsSL "${RELEASE_BASE}/${asset}.sha256")" || - { echo "ERROR: missing release asset ${asset}.sha256 — is v${VERSION} published?" >&2; exit 1; } - echo "${line}" | awk '{print $1}' -} - -echo "Fetching release checksums for v${VERSION}..." -SHA_MAC_ARM="$(fetch_sha "rcli-${VERSION}-macos-arm64.tar.gz")" - -if [[ -f "${CLI_ROOT}/scripts/stamp-formula.py" ]]; then - python3 "${CLI_ROOT}/scripts/stamp-formula.py" "${VERSION}" \ - "macos-arm64=${SHA_MAC_ARM}" -else - echo "ERROR: scripts/stamp-formula.py missing" >&2 - exit 1 -fi - -echo "Stamped formula:" -echo "----------------------------------------" -cat "${FORMULA}" -echo "----------------------------------------" - -if [[ "${DRY_RUN:-0}" == "1" ]]; then - echo "DRY_RUN=1 — not pushing to the tap." - exit 0 -fi - -if [[ -z "${TAP_REPO}" ]]; then - echo "ERROR: set RCLI_TAP_REPO explicitly; the canonical RCLI vs homebrew-tap repository has not been decided." >&2 - exit 1 -fi - -TAP_DIR="${RCLI_TAP_DIR:-}" -if [[ -z "${TAP_DIR}" ]]; then - TAP_DIR="$(mktemp -d)/homebrew-tap" - git clone --depth 1 "${TAP_REPO}" "${TAP_DIR}" -fi - -mkdir -p "${TAP_DIR}/Formula" -cp "${FORMULA}" "${TAP_DIR}/Formula/rcli.rb" -git -C "${TAP_DIR}" add Formula/rcli.rb -git -C "${TAP_DIR}" commit -m "rcli ${VERSION}" -git -C "${TAP_DIR}" push - -echo "Tap formula updated in ${TAP_REPO}" diff --git a/skills/runanywhere/SKILL.md b/skills/runanywhere/SKILL.md index 61485f4..acb30c0 100644 --- a/skills/runanywhere/SKILL.md +++ b/skills/runanywhere/SKILL.md @@ -1,11 +1,11 @@ --- name: runanywhere -description: Set up and use RunAnywhere from the terminal — install rcli, sign in, pick a coding harness, run a model, check spend. Use when the user wants to get started with RunAnywhere, run a harness like opencode against a hosted or on-device model, or asks what their usage is. +description: Set up and use RunAnywhere from the terminal — install wally, sign in, pick a coding harness, run a model, check spend. Use when the user wants to get started with RunAnywhere, run a harness like opencode against a hosted or on-device model, or asks what their usage is. --- # RunAnywhere -`rcli` is one CLI for two things: models running **on this machine**, and models +`wally` is one CLI for two things: models running **on this machine**, and models served from **RunAnywhere Cloud**. The same commands cover both — if a model is on the machine it is served locally, otherwise the request goes to the console the user is signed in to and is metered against their balance. @@ -13,35 +13,35 @@ the user is signed in to and is metered against their balance. ## First check what is already true ```bash -rcli whoami # signed in? which console? -rcli backends # which engines this build linked +wally whoami # signed in? which console? +wally backends # which engines this build linked ``` -`rcli whoami` failing with "not signed in" is the only thing that needs fixing +`wally whoami` failing with "not signed in" is the only thing that needs fixing before anything else works against the cloud. On-device models need no account. ## Signing in ```bash -rcli login +wally login ``` Opens the console in a browser. The person signs in with Google or GitHub, -approves the terminal, and the CLI stores a key in `~/.config/rcli`. There is no -password and no organization step. If a browser cannot open, `rcli login +approves the terminal, and the CLI stores a key in `~/.config/wally`. There is no +password and no organization step. If a browser cannot open, `wally login --no-browser` prints the URL to visit. ## Coding harnesses -A harness is an existing coding tool that `rcli` wires to a model. Today that is +A harness is an existing coding tool that `wally` wires to a model. Today that is **opencode**. ```bash -rcli opencode --cloud -m glm-5.3 # hosted, metered -rcli opencode -m qwen3-0.6b # a model on this machine +wally opencode --cloud -m glm-5.3 # hosted, metered +wally opencode -m qwen3-0.6b # a model on this machine ``` -If opencode is not installed, `rcli` says so and prints the install command +If opencode is not installed, `wally` says so and prints the install command (`npm i -g opencode-ai`) rather than failing. Install it, then run the same line again. @@ -52,9 +52,9 @@ opencode harness works?" is a better second message than a launched TUI. ## Running a model directly ```bash -rcli pull qwen3-0.6b # download it -rcli list # what is downloaded -rcli run qwen3-0.6b # talk to it +wally pull qwen3-0.6b # download it +wally list # what is downloaded +wally run qwen3-0.6b # talk to it ``` Models land in `~/.local/share/runanywhere`. Nothing is downloaded until asked. @@ -62,21 +62,21 @@ Models land in `~/.local/share/runanywhere`. Nothing is downloaded until asked. ## Spend ```bash -rcli usage # credit left, then input/output/cache tokens and spend -rcli usage --json +wally usage # credit left, then input/output/cache tokens and spend +wally usage --json ``` Read-only, and scoped to the signed-in account. ## When something is wrong -- **"not signed in"** — `rcli login`. -- **"that key is not valid"** — the key was revoked or expired; `rcli login` again. +- **"not signed in"** — `wally login`. +- **"that key is not valid"** — the key was revoked or expired; `wally login` again. - **opencode not installed** — `npm i -g opencode-ai`. -- **a model is slow or unavailable** — `rcli backends` shows which engines this +- **a model is slow or unavailable** — `wally backends` shows which engines this build actually linked; a model needing an engine that is not there will not run. ## What not to do -Do not print, log or echo the contents of `~/.config/rcli/credentials.json`. +Do not print, log or echo the contents of `~/.config/wally/credentials.json`. It holds a key with the person's credit behind it. diff --git a/src/account/baked_endpoints.h.in b/src/account/baked_endpoints.h.in new file mode 100644 index 0000000..f660760 --- /dev/null +++ b/src/account/baked_endpoints.h.in @@ -0,0 +1,17 @@ +// Configure-time endpoint bake — TEMPLATE. The real header is generated into +// the build tree (never committed) by CMake from WALLY_BAKED_CONSOLE_API_URL / +// WALLY_BAKED_CONSOLE_WEB_ORIGIN in the *environment* of the configure step. +// Values never appear on the cmake command line, in the cache, or in logs. +// +// Both macros are empty strings in a normal build, and every consumer treats +// empty as "no bake" — so a release build is byte-for-byte the production +// resolution order it always had. A URL baked here is NOT a secret (anyone can +// run `strings` on the binary); the discipline is about the pipeline: no +// endpoint in committed source, none echoed by the build. +#ifndef WALLY_ACCOUNT_BAKED_ENDPOINTS_H +#define WALLY_ACCOUNT_BAKED_ENDPOINTS_H + +#define WALLY_BAKED_CONSOLE_API_URL "@WALLY_BAKED_CONSOLE_API_URL@" +#define WALLY_BAKED_CONSOLE_WEB_ORIGIN "@WALLY_BAKED_CONSOLE_WEB_ORIGIN@" + +#endif // WALLY_ACCOUNT_BAKED_ENDPOINTS_H diff --git a/src/account/console.cpp b/src/account/console.cpp index a917072..8d00c6e 100644 --- a/src/account/console.cpp +++ b/src/account/console.cpp @@ -2,9 +2,12 @@ #include #include +#include +#include #include #include #include +#include #include #include #include @@ -17,9 +20,10 @@ #include #endif +#include "account/console_contract.h" #include "account/credentials.h" -namespace rcli::account { +namespace wally::account { namespace { using Json = nlohmann::json; @@ -137,7 +141,7 @@ bool WinHttpTransport(const HttpRequest& input, HttpResponse* output, std::strin _wcsicmp(host.c_str(), L"localhost") == 0 || host == L"127.0.0.1" || host == L"::1"; const DWORD access_type = loopback ? WINHTTP_ACCESS_TYPE_NO_PROXY : WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY; - const WinHttpHandle session(WinHttpOpen(L"rcli-cloud-auth/1", access_type, + const WinHttpHandle session(WinHttpOpen(L"wally-cloud-auth/1", access_type, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0)); if (!session || !WinHttpSetTimeouts(session.get(), kConnectTimeoutMs, kConnectTimeoutMs, kTotalTimeoutMs, kTotalTimeoutMs)) { @@ -225,6 +229,31 @@ bool WinHttpTransport(const HttpRequest& input, HttpResponse* output, std::strin output->status = 0; output->body.clear(); + output->headers.clear(); + // Only Retry-After is read back on this path: it is the one response header + // a caller acts on (a 429 with a wait hint), and querying a named header is + // cheaper than enumerating them all. + { + wchar_t retry_after_name[] = L"Retry-After"; + DWORD retry_size = 0; + WinHttpQueryHeaders(request.get(), WINHTTP_QUERY_CUSTOM, retry_after_name, + WINHTTP_NO_OUTPUT_BUFFER, &retry_size, WINHTTP_NO_HEADER_INDEX); + if (retry_size > 0 && retry_size < 256) { + std::wstring wide(retry_size / sizeof(wchar_t), L'\0'); + if (WinHttpQueryHeaders(request.get(), WINHTTP_QUERY_CUSTOM, retry_after_name, + wide.data(), &retry_size, WINHTTP_NO_HEADER_INDEX)) { + std::string value; + for (const wchar_t wc : wide) { + if (wc != L'\0' && wc < 128) { + value.push_back(static_cast(wc)); + } + } + if (!value.empty()) { + output->headers["retry-after"] = value; + } + } + } + } std::array buffer{}; while (true) { DWORD received = 0; @@ -277,6 +306,37 @@ std::size_t CollectBody(char* bytes, std::size_t size, std::size_t count, void* return length; } +// Collects one header line into the response map, keyed by a lowercased name. +// curl hands the status line and a trailing blank line here too; both lack a +// colon and are skipped. +std::size_t CollectHeader(char* bytes, std::size_t size, std::size_t count, void* userdata) { + auto* headers = static_cast*>(userdata); + const std::size_t length = size * count; + if (headers == nullptr || count != 0 && size > std::numeric_limits::max() / count) { + return 0; + } + const std::string line(bytes, length); + const std::size_t colon = line.find(':'); + if (colon == std::string::npos) { + return length; + } + std::string name = line.substr(0, colon); + std::transform(name.begin(), name.end(), name.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + std::string value = line.substr(colon + 1); + const auto trim = [](std::string& s) { + const auto not_space = [](unsigned char c) { return std::isspace(c) == 0; }; + s.erase(s.begin(), std::find_if(s.begin(), s.end(), not_space)); + s.erase(std::find_if(s.rbegin(), s.rend(), not_space).base(), s.end()); + }; + trim(name); + trim(value); + if (!name.empty() && headers->size() < 100) { + (*headers)[name] = value; + } + return length; +} + #endif bool DefaultTransport(const HttpRequest& input, HttpResponse* output, std::string* error) { @@ -338,6 +398,7 @@ bool DefaultTransport(const HttpRequest& input, HttpResponse* output, std::strin output->status = 0; output->body.clear(); + output->headers.clear(); ResponseBuffer response{&output->body, false}; configured = configured && curl_easy_setopt(request, CURLOPT_URL, input.url.c_str()) == CURLE_OK && @@ -350,9 +411,11 @@ bool DefaultTransport(const HttpRequest& input, HttpResponse* output, std::strin curl_easy_setopt(request, CURLOPT_FOLLOWLOCATION, 0L) == CURLE_OK && curl_easy_setopt(request, CURLOPT_SSL_VERIFYPEER, 1L) == CURLE_OK && curl_easy_setopt(request, CURLOPT_SSL_VERIFYHOST, 2L) == CURLE_OK && - curl_easy_setopt(request, CURLOPT_USERAGENT, "rcli-cloud-auth/1") == CURLE_OK && + curl_easy_setopt(request, CURLOPT_USERAGENT, "wally-cloud-auth/1") == CURLE_OK && curl_easy_setopt(request, CURLOPT_WRITEFUNCTION, CollectBody) == CURLE_OK && - curl_easy_setopt(request, CURLOPT_WRITEDATA, &response) == CURLE_OK; + curl_easy_setopt(request, CURLOPT_WRITEDATA, &response) == CURLE_OK && + curl_easy_setopt(request, CURLOPT_HEADERFUNCTION, CollectHeader) == CURLE_OK && + curl_easy_setopt(request, CURLOPT_HEADERDATA, &output->headers) == CURLE_OK; if (configured && !input.body.empty()) { configured = input.body.size() <= static_cast(std::numeric_limits::max()) && @@ -370,8 +433,9 @@ bool DefaultTransport(const HttpRequest& input, HttpResponse* output, std::strin if (!received || response.too_large || status < 100 || status > 599) { output->status = 0; output->body.clear(); + output->headers.clear(); if (error != nullptr) { - // Names the origin actually contacted: with RCLI_CONSOLE_URL unset + // Names the origin actually contacted: with WALLY_CONSOLE_URL unset // that is the production console, and a bare "could not reach the // console" reads as a local dev server nobody pointed us at. *error = response.too_large ? "console response exceeded the safety limit" @@ -384,13 +448,24 @@ bool DefaultTransport(const HttpRequest& input, HttpResponse* output, std::strin #endif } -void HttpError(const char* operation, const std::string& origin, int status, std::string* error) { - if (error != nullptr) { - // Names which console answered: with RCLI_CONSOLE_URL unset that is - // production, and a bare "failed with HTTP 404" reads as a bug rather - // than as the wrong console having been asked. - *error = std::string("console ") + operation + " (" + origin + ") failed with HTTP " + - std::to_string(status); +void HttpError(const char* operation, const std::string& origin, const HttpResponse& response, + std::string* error) { + if (error == nullptr) { + return; + } + // Names which console answered: with WALLY_CONSOLE_URL unset that is + // production, and a bare "failed with HTTP 404" reads as a bug rather + // than as the wrong console having been asked. + *error = std::string("console ") + operation + " (" + origin + ") failed with HTTP " + + std::to_string(response.status); + // A 429 means overload, not a broken request. Tell the person how long the + // server asked them to wait, so "try again" is actionable rather than a + // guess. + if (response.status == 429) { + const int wait = response.retry_after_seconds(); + *error += wait >= 0 ? "; the console is rate limiting, retry after " + std::to_string(wait) + + "s" + : "; the console is rate limiting, wait a moment and retry"; } } @@ -420,35 +495,25 @@ bool ParseObject(const HttpResponse& response, Json* object, std::string* error) } } -bool RequiredString(const Json& object, const char* key, std::string* value, std::string* error) { - const auto found = object.find(key); - if (found == object.end() || !found->is_string() || - found->get_ref().empty()) { - if (error != nullptr) { - *error = std::string("console response is missing ") + key; - } +// Parse a response body straight into its generated contract type. The typed +// `get` throws when a required field is missing or the wrong shape or an enum +// value is unknown, so a response that does not match the pinned contract is a +// clean failure here rather than a wrong value read field by name downstream. +template +bool ParseContract(const HttpResponse& response, T* value, std::string* error) { + Json object; + if (!ParseObject(response, &object, error)) { return false; } - *value = found->get(); - return true; -} - -std::string OptionalString(const Json& object, const char* key) { - const auto found = object.find(key); - return found != object.end() && found->is_string() ? found->get() : std::string(); -} - -// int64_t, not long: `long` is 32 bits on MSVC, and cost_micros passes 2^31 -// at about $2,147 of spend, so a Windows build silently truncated it. -std::int64_t Number(const Json& object, const char* key, std::int64_t fallback = 0) { - const auto found = object.find(key); - if (found == object.end() || !found->is_number_integer()) { - return fallback; - } try { - return found->get(); + *value = object.get(); + return true; } catch (const Json::exception&) { - return fallback; + // Never the body: it can carry a token on an error path. + if (error != nullptr) { + *error = "console returned a response that did not match the contract"; + } + return false; } } @@ -456,7 +521,7 @@ bool Send(const Transport& transport, HttpRequest request, HttpResponse* respons std::string* error) { if (!transport(request, response, error)) { if (error != nullptr && error->empty()) { - // Names the origin actually contacted: with RCLI_CONSOLE_URL unset + // Names the origin actually contacted: with WALLY_CONSOLE_URL unset // that is the production console, and a plain "could not reach the // console" reads as a local server that was never told about. *error = "could not reach the RunAnywhere console at " + request.url; @@ -483,16 +548,31 @@ bool RequestCodeIsSafe(const std::string& value) { }); } -bool ReadGrant(const Json& object, Grant* grant, std::string* error) { - grant->access_token = OptionalString(object, "access_token"); - grant->refresh_token = OptionalString(object, "refresh_token"); - grant->email = OptionalString(object, "email"); - grant->plan = OptionalString(object, "plan"); - grant->expires_in = std::max(0, Number(object, "expires_in")); +// The plan is a closed enum in the contract, so its only text is a known-safe +// literal; render it back for the domain Grant, which carries a plain string. +std::string PlanText(const std::optional& plan) { + if (!plan.has_value()) { + return std::string(); + } + return Json(*plan).get(); +} + +// Map the wire grant fields onto the domain Grant, still sanitizing every +// string that reaches the terminal or the credential file. The contract typing +// removes the field-name guesswork; the safety checks stay. +bool MapGrant(const std::optional& access_token, + const std::optional& refresh_token, + const std::optional& email, + const std::optional& plan, std::int64_t expires_in, Grant* grant, + std::string* error) { + grant->access_token = access_token.value_or(""); + grant->refresh_token = refresh_token.value_or(""); + grant->email = email.value_or(""); + grant->plan = PlanText(plan); + grant->expires_in = std::max(0, expires_in); if ((!grant->access_token.empty() && !SessionTokenIsSafe(grant->access_token)) || (!grant->refresh_token.empty() && !SessionTokenIsSafe(grant->refresh_token)) || - (!grant->email.empty() && !DisplayTextIsSafe(grant->email, 320)) || - (!grant->plan.empty() && !DisplayTextIsSafe(grant->plan, 80))) { + (!grant->email.empty() && !DisplayTextIsSafe(grant->email, 320))) { if (error != nullptr) { *error = "console returned an invalid cloud session"; } @@ -527,6 +607,26 @@ bool ConsoleOrigin(const std::string& input, std::string* origin, std::string* e } // namespace +int HttpResponse::retry_after_seconds() const { + const auto it = headers.find("retry-after"); + if (it == headers.end()) { + return -1; + } + const std::string& value = it->second; + if (value.empty() || + !std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c); })) { + return -1; + } + int seconds = 0; + const auto result = std::from_chars(value.data(), value.data() + value.size(), seconds); + if (result.ec != std::errc{} || result.ptr != value.data() + value.size()) { + return -1; + } + // A day is the ceiling: anything larger is a misconfiguration, and honoring + // it would hang a terminal for hours. + return seconds > 86400 ? 86400 : seconds; +} + ConsoleClient::ConsoleClient(Transport transport) : transport_(transport ? std::move(transport) : Transport(DefaultTransport)) {} @@ -542,24 +642,31 @@ bool ConsoleClient::BeginAuthorization(const std::string& console_url, const std if (!ConsoleOrigin(console_url, &origin, error)) { return false; } - const Json payload = {{"hostname", hostname}, {"client", "rcli"}}; + // The client value is the contract enum, whose only member serializes to + // "rcli" -- InferenceInfra's CliClient StrEnum recognizes exactly that, and + // sending anything else 422s /auth/cli/start. Renaming the wire value needs + // a coordinated InferenceInfra change (add "wally" to the enum, re-vendor + // this contract), which is why it is pinned here rather than free text. + contract::CliStartRequest request; + request.client = contract::CliClient::kRcli; + request.hostname = hostname; HttpResponse response; - if (!Send(transport_, {"POST", origin + "/auth/cli/start", payload.dump(), {}}, &response, + if (!Send(transport_, {"POST", origin + "/auth/cli/start", Json(request).dump(), {}}, &response, error)) { return false; } if (response.status != 200) { - HttpError("authorization", origin, response.status, error); + HttpError("authorization", origin, response, error); return false; } - Json object; - if (!ParseObject(response, &object, error) || - !RequiredString(object, "request_code", &authorization->request_code, error) || - !RequiredString(object, "poll_secret", &authorization->poll_secret, error) || - !RequiredString(object, "verification_url", &authorization->verification_url, error)) { + contract::CliStartResponse parsed; + if (!ParseContract(response, &parsed, error)) { return false; } + authorization->request_code = parsed.request_code; + authorization->poll_secret = parsed.poll_secret; + authorization->verification_url = parsed.verification_url; if (!RequestCodeIsSafe(authorization->request_code) || !SessionTokenIsSafe(authorization->poll_secret)) { if (error != nullptr) { @@ -567,10 +674,9 @@ bool ConsoleClient::BeginAuthorization(const std::string& console_url, const std } return false; } - const std::int64_t expires = Number(object, "expires_in", 600); - const std::int64_t interval = Number(object, "interval", 2); - authorization->expires_in = static_cast(std::clamp(expires, 30, 1800)); - authorization->interval = static_cast(std::clamp(interval, 1, 30)); + authorization->expires_in = + static_cast(std::clamp(parsed.expires_in, 30, 1800)); + authorization->interval = static_cast(std::clamp(parsed.interval, 1, 30)); return true; } @@ -586,43 +692,36 @@ PollResult ConsoleClient::Poll(const std::string& console_url, const Authorizati if (!ConsoleOrigin(console_url, &origin, error)) { return PollResult::Failed; } - const Json payload = {{"request_code", authorization.request_code}, - {"poll_secret", authorization.poll_secret}}; + contract::CliPollRequest request; + request.request_code = authorization.request_code; + request.poll_secret = authorization.poll_secret; HttpResponse response; - if (!Send(transport_, {"POST", origin + "/auth/cli/poll", payload.dump(), {}}, &response, + if (!Send(transport_, {"POST", origin + "/auth/cli/poll", Json(request).dump(), {}}, &response, error)) { return PollResult::Failed; } if (response.status != 200) { - HttpError("poll", origin, response.status, error); + HttpError("poll", origin, response, error); return PollResult::Failed; } - Json object; - if (!ParseObject(response, &object, error)) { + contract::PollResponse parsed; + if (!ParseContract(response, &parsed, error)) { return PollResult::Failed; } - std::string state; - if (!RequiredString(object, "status", &state, error)) { - return PollResult::Failed; - } - if (state == "pending") { - return PollResult::Pending; - } - if (state == "denied") { - return PollResult::Denied; - } - if (state == "expired") { - return PollResult::Expired; - } - if (state != "approved") { - if (error != nullptr) { - *error = "console returned an unknown authorization state"; - } - return PollResult::Failed; + switch (parsed.status) { + case contract::PollStatus::kPending: + return PollResult::Pending; + case contract::PollStatus::kDenied: + return PollResult::Denied; + case contract::PollStatus::kExpired: + return PollResult::Expired; + case contract::PollStatus::kApproved: + break; } - if (!ReadGrant(object, grant, error)) { + if (!MapGrant(parsed.access_token, parsed.refresh_token, parsed.email, parsed.plan, + parsed.expires_in.value_or(0), grant, error)) { return PollResult::Failed; } if (grant->access_token.empty() || grant->refresh_token.empty()) { @@ -646,21 +745,23 @@ bool ConsoleClient::Refresh(const std::string& console_url, const std::string& r if (!ConsoleOrigin(console_url, &origin, error)) { return false; } - const Json payload = {{"refresh_token", refresh_token}}; + contract::CliRefreshRequest request; + request.refresh_token = refresh_token; HttpResponse response; - if (!Send(transport_, {"POST", origin + "/auth/cli/refresh", payload.dump(), {}}, &response, - error)) { + if (!Send(transport_, {"POST", origin + "/auth/cli/refresh", Json(request).dump(), {}}, + &response, error)) { return false; } if (response.status != 200) { - HttpError("refresh", origin, response.status, error); + HttpError("refresh", origin, response, error); return false; } - Json object; - if (!ParseObject(response, &object, error)) { + contract::GrantResponse parsed; + if (!ParseContract(response, &parsed, error)) { return false; } - if (!ReadGrant(object, grant, error)) { + if (!MapGrant(parsed.access_token, parsed.refresh_token, parsed.email, parsed.plan, + parsed.expires_in, grant, error)) { return false; } if (grant->access_token.empty()) { @@ -696,26 +797,25 @@ IdentityResult ConsoleClient::WhoAmI(const std::string& console_url, return IdentityResult::Unauthorized; } if (response.status != 200) { - HttpError("identity request", origin, response.status, error); + HttpError("identity request", origin, response, error); return IdentityResult::Failed; } - Json object; - if (!ParseObject(response, &object, error) || - !RequiredString(object, "email", &identity->email, error)) { + contract::IdentityResponse parsed; + if (!ParseContract(response, &parsed, error)) { return IdentityResult::Failed; } + identity->email = parsed.email; if (!DisplayTextIsSafe(identity->email, 320)) { if (error != nullptr) { *error = "console returned an invalid account identity"; } return IdentityResult::Failed; } - identity->plan = OptionalString(object, "plan"); - identity->tokens_this_month = Number(object, "tokens_this_month"); - identity->monthly_token_limit = Number(object, "monthly_token_limit"); - if ((!identity->plan.empty() && !DisplayTextIsSafe(identity->plan, 80)) || - identity->tokens_this_month < 0 || identity->monthly_token_limit < 0) { + identity->plan = PlanText(parsed.plan); + identity->tokens_this_month = parsed.tokens_this_month; + identity->monthly_token_limit = parsed.monthly_token_limit; + if (identity->tokens_this_month < 0 || identity->monthly_token_limit < 0) { if (error != nullptr) { *error = "console returned invalid account usage"; } @@ -767,108 +867,79 @@ IdentityResult ConsoleClient::FetchUsage(const std::string& console_url, return IdentityResult::Unauthorized; } if (response.status != 200) { - HttpError("usage request", origin, response.status, error); + HttpError("usage request", origin, response, error); return IdentityResult::Failed; } - Json object; - if (!ParseObject(response, &object, error)) { + contract::CliUsageResponse parsed; + if (!ParseContract(response, &parsed, error)) { return IdentityResult::Failed; } - const auto credit = object.find("credit"); - if (credit != object.end() && credit->is_object()) { - usage->credit.balance_micros = Number(*credit, "balance_micros"); - usage->credit.granted_micros = Number(*credit, "granted_micros"); - usage->credit.spent_micros = Number(*credit, "spent_micros"); - } - - const auto totals = object.find("totals"); - if (totals != object.end() && totals->is_object()) { - usage->totals.requests = Number(*totals, "requests"); - usage->totals.prompt_tokens = Number(*totals, "prompt_tokens"); - usage->totals.completion_tokens = Number(*totals, "completion_tokens"); - usage->totals.cached_tokens = Number(*totals, "cached_tokens"); - usage->totals.cost_micros = Number(*totals, "cost_micros"); - } - - // Absent on every console deployed before windowed totals shipped. Left - // empty rather than filled from `totals`, which covers `days` and would - // read as an hour's spend while describing a month's. - const auto windows = object.find("windows"); - if (windows != object.end() && windows->is_array()) { - for (const Json& entry : *windows) { - if (!entry.is_object()) { - continue; - } - UsageWindow window; - window.window = DisplaySafe(OptionalString(entry, "window"), 16); - window.seconds = Number(entry, "seconds"); - const auto window_totals = entry.find("totals"); - if (window_totals != entry.end() && window_totals->is_object()) { - window.totals.requests = Number(*window_totals, "requests"); - window.totals.prompt_tokens = Number(*window_totals, "prompt_tokens"); - window.totals.completion_tokens = Number(*window_totals, "completion_tokens"); - window.totals.cached_tokens = Number(*window_totals, "cached_tokens"); - window.totals.cost_micros = Number(*window_totals, "cost_micros"); - } - usage->windows.push_back(window); - } - } - - const auto timeline = object.find("timeline"); - if (timeline != object.end() && timeline->is_array()) { - for (const Json& point : *timeline) { - if (!point.is_object()) { - continue; - } - UsageDay day; - day.date = DisplaySafe(OptionalString(point, "date"), 32); - day.requests = Number(point, "requests"); - day.prompt_tokens = Number(point, "prompt_tokens"); - day.completion_tokens = Number(point, "completion_tokens"); - day.cost_micros = Number(point, "cost_micros"); - usage->timeline.push_back(day); - } - } + // Map the typed response onto the domain Usage, sanitizing every string the + // server chose before it reaches the terminal. The numbers are already + // typed; the strings still pass through DisplaySafe because a hostile + // console must not be able to write escape sequences to the user's screen. + const auto copy_totals = [](const contract::UsageTotals& from, UsageTotals* to) { + to->requests = from.requests; + to->prompt_tokens = from.prompt_tokens; + to->completion_tokens = from.completion_tokens; + to->cached_tokens = from.cached_tokens; + to->cost_micros = from.cost_micros; + }; - const auto models = object.find("models"); - if (models != object.end() && models->is_array()) { - for (const Json& entry : *models) { - if (!entry.is_object()) { - continue; - } - UsageModel row; - row.model = DisplaySafe(OptionalString(entry, "model"), 128); - row.requests = Number(entry, "requests"); - row.prompt_tokens = Number(entry, "prompt_tokens"); - row.completion_tokens = Number(entry, "completion_tokens"); - row.cached_tokens = Number(entry, "cached_tokens"); - row.cost_micros = Number(entry, "cost_micros"); - usage->models.push_back(row); - } - } - - const auto events = object.find("recent"); - if (events != object.end() && events->is_array()) { - for (const Json& entry : *events) { - if (!entry.is_object()) { - continue; - } - UsageEvent event; - event.request_id = DisplaySafe(OptionalString(entry, "request_id"), 128); - event.model = DisplaySafe(OptionalString(entry, "model"), 128); - event.harness = DisplaySafe(OptionalString(entry, "harness"), 64); - event.started_at = DisplaySafe(OptionalString(entry, "ts_start"), 64); - event.error_code = DisplaySafe(OptionalString(entry, "error_code"), 64); - event.prompt_tokens = Number(entry, "prompt_tokens"); - event.completion_tokens = Number(entry, "completion_tokens"); - event.cached_tokens = Number(entry, "cached_tokens"); - event.cost_micros = Number(entry, "cost_micros"); - event.ttft_ms = Number(entry, "ttft_ms"); - event.status_code = static_cast(Number(entry, "status_code")); - usage->events.push_back(event); - } + usage->credit.balance_micros = parsed.credit.balance_micros; + usage->credit.granted_micros = parsed.credit.granted_micros; + usage->credit.spent_micros = parsed.credit.spent_micros; + copy_totals(parsed.totals, &usage->totals); + + // Absent on every console deployed before windowed totals shipped. The + // window label is a closed enum, so its text is a known-safe literal. + for (const contract::CliUsageWindow& entry : parsed.windows) { + UsageWindow window; + window.window = Json(entry.window).get(); + window.seconds = entry.seconds; + copy_totals(entry.totals, &window.totals); + usage->windows.push_back(window); + } + + for (const contract::UsageTimelinePoint& point : parsed.timeline) { + UsageDay day; + day.date = DisplaySafe(point.date, 32); + day.requests = point.requests; + day.prompt_tokens = point.prompt_tokens; + day.completion_tokens = point.completion_tokens; + day.cost_micros = point.cost_micros; + usage->timeline.push_back(day); + } + + for (const contract::UsageModelRollup& entry : parsed.models) { + UsageModel row; + row.model = DisplaySafe(entry.model, 128); + row.requests = entry.requests; + row.prompt_tokens = entry.prompt_tokens; + row.completion_tokens = entry.completion_tokens; + row.cached_tokens = entry.cached_tokens; + row.cost_micros = entry.cost_micros; + usage->models.push_back(row); + } + + for (const contract::CliUsageEvent& entry : parsed.recent) { + UsageEvent event; + event.request_id = DisplaySafe(entry.request_id, 128); + event.model = DisplaySafe(entry.model, 128); + event.harness = + entry.harness.has_value() ? DisplaySafe(Json(*entry.harness).get(), 64) + : std::string(); + event.started_at = DisplaySafe(entry.ts_start, 64); + event.error_code = DisplaySafe(entry.error_code.value_or(""), 64); + event.prompt_tokens = entry.prompt_tokens; + event.completion_tokens = entry.completion_tokens; + event.cached_tokens = entry.cached_tokens; + event.cost_micros = entry.cost_micros; + event.ttft_ms = entry.ttft_ms.value_or(0); + event.status_code = static_cast(entry.status_code); + usage->events.push_back(event); } return IdentityResult::Ok; } @@ -889,22 +960,23 @@ bool ConsoleClient::Revoke(const std::string& console_url, const std::string& ac if (!ConsoleOrigin(console_url, &origin, error)) { return false; } - const Json payload = {{"refresh_token", refresh_token}}; + contract::CliRefreshRequest request; + request.refresh_token = refresh_token; HttpResponse response; - if (!Send(transport_, {"POST", origin + "/auth/cli/revoke", payload.dump(), access_token}, + if (!Send(transport_, {"POST", origin + "/auth/cli/revoke", Json(request).dump(), access_token}, &response, error)) { return false; } if (response.status != 200 && response.status != 204) { - HttpError("revoke", origin, response.status, error); + HttpError("revoke", origin, response, error); return false; } return true; } -} // namespace rcli::account +} // namespace wally::account -namespace rcli::account { +namespace wally::account { bool BeginAuthorization(const std::string& console_url, const std::string& hostname, Authorization* authorization, std::string* error) { @@ -926,4 +998,4 @@ bool WhoAmI(const std::string& console_url, const std::string& token, Identity* return ConsoleClient().WhoAmI(console_url, token, identity, error) == IdentityResult::Ok; } -} // namespace rcli::account +} // namespace wally::account diff --git a/src/account/console.h b/src/account/console.h index ce38961..77e7311 100644 --- a/src/account/console.h +++ b/src/account/console.h @@ -1,13 +1,14 @@ -#ifndef RCLI_ACCOUNT_CONSOLE_H -#define RCLI_ACCOUNT_CONSOLE_H +#ifndef WALLY_ACCOUNT_CONSOLE_H +#define WALLY_ACCOUNT_CONSOLE_H #include #include +#include #include #include -namespace rcli::account { +namespace wally::account { struct HttpRequest { std::string method; @@ -19,6 +20,17 @@ struct HttpRequest { struct HttpResponse { int status = 0; std::string body; + // Response headers, keys lowercased so a lookup does not have to guess the + // server's casing. Populated by the real transport; a mock may leave it + // empty. + std::map headers; + + // The value of a `Retry-After` header as whole seconds, or -1 when the + // header is absent or not a plain delay. The hosted API answers overload + // with 429 and this header; a client is expected to wait rather than retry + // at once. Only the delta-seconds form is honored: an HTTP-date Retry-After + // is valid but never sent by this API, so parsing one would be dead code. + int retry_after_seconds() const; }; using Transport = std::function; @@ -165,6 +177,6 @@ bool Refresh(const std::string& console_url, const std::string& refresh_token, G bool WhoAmI(const std::string& console_url, const std::string& token, Identity* identity, std::string* error); -} // namespace rcli::account +} // namespace wally::account -#endif // RCLI_ACCOUNT_CONSOLE_H +#endif // WALLY_ACCOUNT_CONSOLE_H diff --git a/src/account/console_contract.h b/src/account/console_contract.h new file mode 100644 index 0000000..9cbaee0 --- /dev/null +++ b/src/account/console_contract.h @@ -0,0 +1,901 @@ +// Generated by contracts/generate_console_binding.py from +// contracts/wally-cli-v1.openapi.json. DO NOT EDIT. +// +// Typed request and response models for the CLI's control-plane calls, so +// console.cpp neither builds a request body by hand nor reads a response +// field by name. Regenerate and commit whenever the pinned contract moves. +#ifndef WALLY_ACCOUNT_CONSOLE_CONTRACT_H +#define WALLY_ACCOUNT_CONSOLE_CONTRACT_H + +#include +#include +#include +#include + +#include + +namespace wally::account::contract { + +// SHA-256 of contracts/wally-cli-v1.openapi.json this header was built from. +inline constexpr char kContractSha256[] = "5069ea05fa10339c0ec316f269e1aa2ed486114ec1e248abe82c2ed702fdc20a"; + +enum class ApiErrorCode { + kInvalidRequest, + kUnauthorized, + kForbidden, + kNotFound, + kConflict, + kGone, + kPayloadTooLarge, + kRateLimited, + kInternalError, + kUpstreamUnavailable, + kServiceUnavailable, + kTimeout, + kBadRequest, + kExpiredApiKey, + kGatewayUnavailable, + kIdempotencyKeyReused, + kInsufficientCredit, + kInvalidApiKey, + kModelNotEntitled, + kNotInCohort, + kProvisioningUnavailable, + kProxyRetired, + kRevokedApiKey, + kSignupMethodNotAllowed, + kUpstreamContractViolation, + kUserRateLimited, + kGatewayNotConfigured, + kGatewayTimeout, + kGatewayProtocolError, + kGatewayOperationPending, + kReconciliationUnverified, +}; + +inline void from_json(const nlohmann::json& j, ApiErrorCode& value) { + const std::string raw = j.get(); + if (raw == "invalid_request") { value = ApiErrorCode::kInvalidRequest; return; } + if (raw == "unauthorized") { value = ApiErrorCode::kUnauthorized; return; } + if (raw == "forbidden") { value = ApiErrorCode::kForbidden; return; } + if (raw == "not_found") { value = ApiErrorCode::kNotFound; return; } + if (raw == "conflict") { value = ApiErrorCode::kConflict; return; } + if (raw == "gone") { value = ApiErrorCode::kGone; return; } + if (raw == "payload_too_large") { value = ApiErrorCode::kPayloadTooLarge; return; } + if (raw == "rate_limited") { value = ApiErrorCode::kRateLimited; return; } + if (raw == "internal_error") { value = ApiErrorCode::kInternalError; return; } + if (raw == "upstream_unavailable") { value = ApiErrorCode::kUpstreamUnavailable; return; } + if (raw == "service_unavailable") { value = ApiErrorCode::kServiceUnavailable; return; } + if (raw == "timeout") { value = ApiErrorCode::kTimeout; return; } + if (raw == "bad_request") { value = ApiErrorCode::kBadRequest; return; } + if (raw == "expired_api_key") { value = ApiErrorCode::kExpiredApiKey; return; } + if (raw == "gateway_unavailable") { value = ApiErrorCode::kGatewayUnavailable; return; } + if (raw == "idempotency_key_reused") { value = ApiErrorCode::kIdempotencyKeyReused; return; } + if (raw == "insufficient_credit") { value = ApiErrorCode::kInsufficientCredit; return; } + if (raw == "invalid_api_key") { value = ApiErrorCode::kInvalidApiKey; return; } + if (raw == "model_not_entitled") { value = ApiErrorCode::kModelNotEntitled; return; } + if (raw == "not_in_cohort") { value = ApiErrorCode::kNotInCohort; return; } + if (raw == "provisioning_unavailable") { value = ApiErrorCode::kProvisioningUnavailable; return; } + if (raw == "proxy_retired") { value = ApiErrorCode::kProxyRetired; return; } + if (raw == "revoked_api_key") { value = ApiErrorCode::kRevokedApiKey; return; } + if (raw == "signup_method_not_allowed") { value = ApiErrorCode::kSignupMethodNotAllowed; return; } + if (raw == "upstream_contract_violation") { value = ApiErrorCode::kUpstreamContractViolation; return; } + if (raw == "user_rate_limited") { value = ApiErrorCode::kUserRateLimited; return; } + if (raw == "gateway_not_configured") { value = ApiErrorCode::kGatewayNotConfigured; return; } + if (raw == "gateway_timeout") { value = ApiErrorCode::kGatewayTimeout; return; } + if (raw == "gateway_protocol_error") { value = ApiErrorCode::kGatewayProtocolError; return; } + if (raw == "gateway_operation_pending") { value = ApiErrorCode::kGatewayOperationPending; return; } + if (raw == "reconciliation_unverified") { value = ApiErrorCode::kReconciliationUnverified; return; } + throw nlohmann::json::type_error::create(302, "unknown ApiErrorCode: " + raw, &j); +} + +inline void to_json(nlohmann::json& j, const ApiErrorCode& value) { + switch (value) { + case ApiErrorCode::kInvalidRequest: j = "invalid_request"; return; + case ApiErrorCode::kUnauthorized: j = "unauthorized"; return; + case ApiErrorCode::kForbidden: j = "forbidden"; return; + case ApiErrorCode::kNotFound: j = "not_found"; return; + case ApiErrorCode::kConflict: j = "conflict"; return; + case ApiErrorCode::kGone: j = "gone"; return; + case ApiErrorCode::kPayloadTooLarge: j = "payload_too_large"; return; + case ApiErrorCode::kRateLimited: j = "rate_limited"; return; + case ApiErrorCode::kInternalError: j = "internal_error"; return; + case ApiErrorCode::kUpstreamUnavailable: j = "upstream_unavailable"; return; + case ApiErrorCode::kServiceUnavailable: j = "service_unavailable"; return; + case ApiErrorCode::kTimeout: j = "timeout"; return; + case ApiErrorCode::kBadRequest: j = "bad_request"; return; + case ApiErrorCode::kExpiredApiKey: j = "expired_api_key"; return; + case ApiErrorCode::kGatewayUnavailable: j = "gateway_unavailable"; return; + case ApiErrorCode::kIdempotencyKeyReused: j = "idempotency_key_reused"; return; + case ApiErrorCode::kInsufficientCredit: j = "insufficient_credit"; return; + case ApiErrorCode::kInvalidApiKey: j = "invalid_api_key"; return; + case ApiErrorCode::kModelNotEntitled: j = "model_not_entitled"; return; + case ApiErrorCode::kNotInCohort: j = "not_in_cohort"; return; + case ApiErrorCode::kProvisioningUnavailable: j = "provisioning_unavailable"; return; + case ApiErrorCode::kProxyRetired: j = "proxy_retired"; return; + case ApiErrorCode::kRevokedApiKey: j = "revoked_api_key"; return; + case ApiErrorCode::kSignupMethodNotAllowed: j = "signup_method_not_allowed"; return; + case ApiErrorCode::kUpstreamContractViolation: j = "upstream_contract_violation"; return; + case ApiErrorCode::kUserRateLimited: j = "user_rate_limited"; return; + case ApiErrorCode::kGatewayNotConfigured: j = "gateway_not_configured"; return; + case ApiErrorCode::kGatewayTimeout: j = "gateway_timeout"; return; + case ApiErrorCode::kGatewayProtocolError: j = "gateway_protocol_error"; return; + case ApiErrorCode::kGatewayOperationPending: j = "gateway_operation_pending"; return; + case ApiErrorCode::kReconciliationUnverified: j = "reconciliation_unverified"; return; + } +} + +enum class CliClient { + kRcli, +}; + +inline void from_json(const nlohmann::json& j, CliClient& value) { + const std::string raw = j.get(); + if (raw == "rcli") { value = CliClient::kRcli; return; } + throw nlohmann::json::type_error::create(302, "unknown CliClient: " + raw, &j); +} + +inline void to_json(nlohmann::json& j, const CliClient& value) { + switch (value) { + case CliClient::kRcli: j = "rcli"; return; + } +} + +enum class CliPlan { + kBeta, +}; + +inline void from_json(const nlohmann::json& j, CliPlan& value) { + const std::string raw = j.get(); + if (raw == "beta") { value = CliPlan::kBeta; return; } + throw nlohmann::json::type_error::create(302, "unknown CliPlan: " + raw, &j); +} + +inline void to_json(nlohmann::json& j, const CliPlan& value) { + switch (value) { + case CliPlan::kBeta: j = "beta"; return; + } +} + +enum class CliUsageWindowLabel { + k1h, + k24h, +}; + +inline void from_json(const nlohmann::json& j, CliUsageWindowLabel& value) { + const std::string raw = j.get(); + if (raw == "1h") { value = CliUsageWindowLabel::k1h; return; } + if (raw == "24h") { value = CliUsageWindowLabel::k24h; return; } + throw nlohmann::json::type_error::create(302, "unknown CliUsageWindowLabel: " + raw, &j); +} + +inline void to_json(nlohmann::json& j, const CliUsageWindowLabel& value) { + switch (value) { + case CliUsageWindowLabel::k1h: j = "1h"; return; + case CliUsageWindowLabel::k24h: j = "24h"; return; + } +} + +enum class Harness { + kClaudeCode, + kOpencode, + kHermes, + kCursor, + kContinue, + kAider, + kCline, + kRoo, + kZed, + kOpenaiSdk, + kLangchain, + kLlamaindex, + kCurl, + kConsole, + kPlayground, + kRcli, + kSdk, + kUnknown, +}; + +inline void from_json(const nlohmann::json& j, Harness& value) { + const std::string raw = j.get(); + if (raw == "claude_code") { value = Harness::kClaudeCode; return; } + if (raw == "opencode") { value = Harness::kOpencode; return; } + if (raw == "hermes") { value = Harness::kHermes; return; } + if (raw == "cursor") { value = Harness::kCursor; return; } + if (raw == "continue") { value = Harness::kContinue; return; } + if (raw == "aider") { value = Harness::kAider; return; } + if (raw == "cline") { value = Harness::kCline; return; } + if (raw == "roo") { value = Harness::kRoo; return; } + if (raw == "zed") { value = Harness::kZed; return; } + if (raw == "openai_sdk") { value = Harness::kOpenaiSdk; return; } + if (raw == "langchain") { value = Harness::kLangchain; return; } + if (raw == "llamaindex") { value = Harness::kLlamaindex; return; } + if (raw == "curl") { value = Harness::kCurl; return; } + if (raw == "console") { value = Harness::kConsole; return; } + if (raw == "playground") { value = Harness::kPlayground; return; } + if (raw == "rcli") { value = Harness::kRcli; return; } + if (raw == "sdk") { value = Harness::kSdk; return; } + if (raw == "unknown") { value = Harness::kUnknown; return; } + throw nlohmann::json::type_error::create(302, "unknown Harness: " + raw, &j); +} + +inline void to_json(nlohmann::json& j, const Harness& value) { + switch (value) { + case Harness::kClaudeCode: j = "claude_code"; return; + case Harness::kOpencode: j = "opencode"; return; + case Harness::kHermes: j = "hermes"; return; + case Harness::kCursor: j = "cursor"; return; + case Harness::kContinue: j = "continue"; return; + case Harness::kAider: j = "aider"; return; + case Harness::kCline: j = "cline"; return; + case Harness::kRoo: j = "roo"; return; + case Harness::kZed: j = "zed"; return; + case Harness::kOpenaiSdk: j = "openai_sdk"; return; + case Harness::kLangchain: j = "langchain"; return; + case Harness::kLlamaindex: j = "llamaindex"; return; + case Harness::kCurl: j = "curl"; return; + case Harness::kConsole: j = "console"; return; + case Harness::kPlayground: j = "playground"; return; + case Harness::kRcli: j = "rcli"; return; + case Harness::kSdk: j = "sdk"; return; + case Harness::kUnknown: j = "unknown"; return; + } +} + +enum class PollStatus { + kPending, + kApproved, + kDenied, + kExpired, +}; + +inline void from_json(const nlohmann::json& j, PollStatus& value) { + const std::string raw = j.get(); + if (raw == "pending") { value = PollStatus::kPending; return; } + if (raw == "approved") { value = PollStatus::kApproved; return; } + if (raw == "denied") { value = PollStatus::kDenied; return; } + if (raw == "expired") { value = PollStatus::kExpired; return; } + throw nlohmann::json::type_error::create(302, "unknown PollStatus: " + raw, &j); +} + +inline void to_json(nlohmann::json& j, const PollStatus& value) { + switch (value) { + case PollStatus::kPending: j = "pending"; return; + case PollStatus::kApproved: j = "approved"; return; + case PollStatus::kDenied: j = "denied"; return; + case PollStatus::kExpired: j = "expired"; return; + } +} + +struct ApiError { + ApiErrorCode code; + std::string message; +}; + +inline void from_json(const nlohmann::json& j, ApiError& value) { + if (j.contains("code") && !j.at("code").is_null()) { + value.code = j.at("code").get(); + } else { + value.code = ApiErrorCode{}; + } + if (j.contains("message") && !j.at("message").is_null()) { + value.message = j.at("message").get(); + } else { + value.message = std::string{}; + } +} + +inline void to_json(nlohmann::json& j, const ApiError& value) { + j = nlohmann::json::object(); + j["code"] = value.code; + j["message"] = value.message; +} + +struct CliPollRequest { + std::string poll_secret; + std::string request_code; +}; + +inline void from_json(const nlohmann::json& j, CliPollRequest& value) { + if (j.contains("poll_secret") && !j.at("poll_secret").is_null()) { + value.poll_secret = j.at("poll_secret").get(); + } else { + value.poll_secret = std::string{}; + } + if (j.contains("request_code") && !j.at("request_code").is_null()) { + value.request_code = j.at("request_code").get(); + } else { + value.request_code = std::string{}; + } +} + +inline void to_json(nlohmann::json& j, const CliPollRequest& value) { + j = nlohmann::json::object(); + j["poll_secret"] = value.poll_secret; + j["request_code"] = value.request_code; +} + +struct CliRefreshRequest { + std::string refresh_token; +}; + +inline void from_json(const nlohmann::json& j, CliRefreshRequest& value) { + if (j.contains("refresh_token") && !j.at("refresh_token").is_null()) { + value.refresh_token = j.at("refresh_token").get(); + } else { + value.refresh_token = std::string{}; + } +} + +inline void to_json(nlohmann::json& j, const CliRefreshRequest& value) { + j = nlohmann::json::object(); + j["refresh_token"] = value.refresh_token; +} + +struct CliStartRequest { + CliClient client; + std::string hostname; +}; + +inline void from_json(const nlohmann::json& j, CliStartRequest& value) { + if (j.contains("client") && !j.at("client").is_null()) { + value.client = j.at("client").get(); + } else { + value.client = CliClient{}; + } + if (j.contains("hostname") && !j.at("hostname").is_null()) { + value.hostname = j.at("hostname").get(); + } else { + value.hostname = std::string{}; + } +} + +inline void to_json(nlohmann::json& j, const CliStartRequest& value) { + j = nlohmann::json::object(); + j["client"] = value.client; + j["hostname"] = value.hostname; +} + +struct CliStartResponse { + std::int64_t expires_in; + std::int64_t interval; + std::string poll_secret; + std::string request_code; + std::string verification_url; +}; + +inline void from_json(const nlohmann::json& j, CliStartResponse& value) { + if (j.contains("expires_in") && !j.at("expires_in").is_null()) { + value.expires_in = j.at("expires_in").get(); + } else { + value.expires_in = std::int64_t{}; + } + if (j.contains("interval") && !j.at("interval").is_null()) { + value.interval = j.at("interval").get(); + } else { + value.interval = std::int64_t{}; + } + if (j.contains("poll_secret") && !j.at("poll_secret").is_null()) { + value.poll_secret = j.at("poll_secret").get(); + } else { + value.poll_secret = std::string{}; + } + if (j.contains("request_code") && !j.at("request_code").is_null()) { + value.request_code = j.at("request_code").get(); + } else { + value.request_code = std::string{}; + } + if (j.contains("verification_url") && !j.at("verification_url").is_null()) { + value.verification_url = j.at("verification_url").get(); + } else { + value.verification_url = std::string{}; + } +} + +inline void to_json(nlohmann::json& j, const CliStartResponse& value) { + j = nlohmann::json::object(); + j["expires_in"] = value.expires_in; + j["interval"] = value.interval; + j["poll_secret"] = value.poll_secret; + j["request_code"] = value.request_code; + j["verification_url"] = value.verification_url; +} + +struct CliUsageEvent { + std::int64_t cached_tokens; + std::int64_t completion_tokens; + std::int64_t cost_micros; + std::optional error_code; + std::optional harness; + std::string model; + std::int64_t prompt_tokens; + std::string request_id; + std::int64_t status_code; + std::string ts_start; + std::optional ttft_ms; +}; + +inline void from_json(const nlohmann::json& j, CliUsageEvent& value) { + if (j.contains("cached_tokens") && !j.at("cached_tokens").is_null()) { + value.cached_tokens = j.at("cached_tokens").get(); + } else { + value.cached_tokens = std::int64_t{}; + } + if (j.contains("completion_tokens") && !j.at("completion_tokens").is_null()) { + value.completion_tokens = j.at("completion_tokens").get(); + } else { + value.completion_tokens = std::int64_t{}; + } + if (j.contains("cost_micros") && !j.at("cost_micros").is_null()) { + value.cost_micros = j.at("cost_micros").get(); + } else { + value.cost_micros = std::int64_t{}; + } + if (j.contains("error_code") && !j.at("error_code").is_null()) { + value.error_code = j.at("error_code").get(); + } else { + value.error_code = std::nullopt; + } + if (j.contains("harness") && !j.at("harness").is_null()) { + value.harness = j.at("harness").get(); + } else { + value.harness = std::nullopt; + } + if (j.contains("model") && !j.at("model").is_null()) { + value.model = j.at("model").get(); + } else { + value.model = std::string{}; + } + if (j.contains("prompt_tokens") && !j.at("prompt_tokens").is_null()) { + value.prompt_tokens = j.at("prompt_tokens").get(); + } else { + value.prompt_tokens = std::int64_t{}; + } + if (j.contains("request_id") && !j.at("request_id").is_null()) { + value.request_id = j.at("request_id").get(); + } else { + value.request_id = std::string{}; + } + if (j.contains("status_code") && !j.at("status_code").is_null()) { + value.status_code = j.at("status_code").get(); + } else { + value.status_code = std::int64_t{}; + } + if (j.contains("ts_start") && !j.at("ts_start").is_null()) { + value.ts_start = j.at("ts_start").get(); + } else { + value.ts_start = std::string{}; + } + if (j.contains("ttft_ms") && !j.at("ttft_ms").is_null()) { + value.ttft_ms = j.at("ttft_ms").get(); + } else { + value.ttft_ms = std::nullopt; + } +} + +inline void to_json(nlohmann::json& j, const CliUsageEvent& value) { + j = nlohmann::json::object(); + j["cached_tokens"] = value.cached_tokens; + j["completion_tokens"] = value.completion_tokens; + j["cost_micros"] = value.cost_micros; + if (value.error_code.has_value()) { + j["error_code"] = *value.error_code; + } + if (value.harness.has_value()) { + j["harness"] = *value.harness; + } + j["model"] = value.model; + j["prompt_tokens"] = value.prompt_tokens; + j["request_id"] = value.request_id; + j["status_code"] = value.status_code; + j["ts_start"] = value.ts_start; + if (value.ttft_ms.has_value()) { + j["ttft_ms"] = *value.ttft_ms; + } +} + +struct CreditResponse { + std::int64_t balance_micros; + std::int64_t granted_micros; + std::int64_t spent_micros; +}; + +inline void from_json(const nlohmann::json& j, CreditResponse& value) { + if (j.contains("balance_micros") && !j.at("balance_micros").is_null()) { + value.balance_micros = j.at("balance_micros").get(); + } else { + value.balance_micros = std::int64_t{}; + } + if (j.contains("granted_micros") && !j.at("granted_micros").is_null()) { + value.granted_micros = j.at("granted_micros").get(); + } else { + value.granted_micros = std::int64_t{}; + } + if (j.contains("spent_micros") && !j.at("spent_micros").is_null()) { + value.spent_micros = j.at("spent_micros").get(); + } else { + value.spent_micros = std::int64_t{}; + } +} + +inline void to_json(nlohmann::json& j, const CreditResponse& value) { + j = nlohmann::json::object(); + j["balance_micros"] = value.balance_micros; + j["granted_micros"] = value.granted_micros; + j["spent_micros"] = value.spent_micros; +} + +struct UsageModelRollup { + std::int64_t cached_tokens; + std::int64_t completion_tokens; + std::int64_t cost_micros; + std::string model; + std::int64_t prompt_tokens; + std::int64_t requests; +}; + +inline void from_json(const nlohmann::json& j, UsageModelRollup& value) { + if (j.contains("cached_tokens") && !j.at("cached_tokens").is_null()) { + value.cached_tokens = j.at("cached_tokens").get(); + } else { + value.cached_tokens = std::int64_t{}; + } + if (j.contains("completion_tokens") && !j.at("completion_tokens").is_null()) { + value.completion_tokens = j.at("completion_tokens").get(); + } else { + value.completion_tokens = std::int64_t{}; + } + if (j.contains("cost_micros") && !j.at("cost_micros").is_null()) { + value.cost_micros = j.at("cost_micros").get(); + } else { + value.cost_micros = std::int64_t{}; + } + if (j.contains("model") && !j.at("model").is_null()) { + value.model = j.at("model").get(); + } else { + value.model = std::string{}; + } + if (j.contains("prompt_tokens") && !j.at("prompt_tokens").is_null()) { + value.prompt_tokens = j.at("prompt_tokens").get(); + } else { + value.prompt_tokens = std::int64_t{}; + } + if (j.contains("requests") && !j.at("requests").is_null()) { + value.requests = j.at("requests").get(); + } else { + value.requests = std::int64_t{}; + } +} + +inline void to_json(nlohmann::json& j, const UsageModelRollup& value) { + j = nlohmann::json::object(); + j["cached_tokens"] = value.cached_tokens; + j["completion_tokens"] = value.completion_tokens; + j["cost_micros"] = value.cost_micros; + j["model"] = value.model; + j["prompt_tokens"] = value.prompt_tokens; + j["requests"] = value.requests; +} + +struct UsageTimelinePoint { + std::int64_t completion_tokens; + std::int64_t cost_micros; + std::string date; + std::int64_t prompt_tokens; + std::int64_t requests; +}; + +inline void from_json(const nlohmann::json& j, UsageTimelinePoint& value) { + if (j.contains("completion_tokens") && !j.at("completion_tokens").is_null()) { + value.completion_tokens = j.at("completion_tokens").get(); + } else { + value.completion_tokens = std::int64_t{}; + } + if (j.contains("cost_micros") && !j.at("cost_micros").is_null()) { + value.cost_micros = j.at("cost_micros").get(); + } else { + value.cost_micros = std::int64_t{}; + } + if (j.contains("date") && !j.at("date").is_null()) { + value.date = j.at("date").get(); + } else { + value.date = std::string{}; + } + if (j.contains("prompt_tokens") && !j.at("prompt_tokens").is_null()) { + value.prompt_tokens = j.at("prompt_tokens").get(); + } else { + value.prompt_tokens = std::int64_t{}; + } + if (j.contains("requests") && !j.at("requests").is_null()) { + value.requests = j.at("requests").get(); + } else { + value.requests = std::int64_t{}; + } +} + +inline void to_json(nlohmann::json& j, const UsageTimelinePoint& value) { + j = nlohmann::json::object(); + j["completion_tokens"] = value.completion_tokens; + j["cost_micros"] = value.cost_micros; + j["date"] = value.date; + j["prompt_tokens"] = value.prompt_tokens; + j["requests"] = value.requests; +} + +struct UsageTotals { + std::int64_t cached_tokens; + std::int64_t completion_tokens; + std::int64_t cost_micros; + std::int64_t prompt_tokens; + std::int64_t requests; +}; + +inline void from_json(const nlohmann::json& j, UsageTotals& value) { + if (j.contains("cached_tokens") && !j.at("cached_tokens").is_null()) { + value.cached_tokens = j.at("cached_tokens").get(); + } else { + value.cached_tokens = std::int64_t{}; + } + if (j.contains("completion_tokens") && !j.at("completion_tokens").is_null()) { + value.completion_tokens = j.at("completion_tokens").get(); + } else { + value.completion_tokens = std::int64_t{}; + } + if (j.contains("cost_micros") && !j.at("cost_micros").is_null()) { + value.cost_micros = j.at("cost_micros").get(); + } else { + value.cost_micros = std::int64_t{}; + } + if (j.contains("prompt_tokens") && !j.at("prompt_tokens").is_null()) { + value.prompt_tokens = j.at("prompt_tokens").get(); + } else { + value.prompt_tokens = std::int64_t{}; + } + if (j.contains("requests") && !j.at("requests").is_null()) { + value.requests = j.at("requests").get(); + } else { + value.requests = std::int64_t{}; + } +} + +inline void to_json(nlohmann::json& j, const UsageTotals& value) { + j = nlohmann::json::object(); + j["cached_tokens"] = value.cached_tokens; + j["completion_tokens"] = value.completion_tokens; + j["cost_micros"] = value.cost_micros; + j["prompt_tokens"] = value.prompt_tokens; + j["requests"] = value.requests; +} + +struct CliUsageWindow { + std::int64_t seconds; + UsageTotals totals; + CliUsageWindowLabel window; +}; + +inline void from_json(const nlohmann::json& j, CliUsageWindow& value) { + if (j.contains("seconds") && !j.at("seconds").is_null()) { + value.seconds = j.at("seconds").get(); + } else { + value.seconds = std::int64_t{}; + } + if (j.contains("totals") && !j.at("totals").is_null()) { + value.totals = j.at("totals").get(); + } else { + value.totals = UsageTotals{}; + } + if (j.contains("window") && !j.at("window").is_null()) { + value.window = j.at("window").get(); + } else { + value.window = CliUsageWindowLabel{}; + } +} + +inline void to_json(nlohmann::json& j, const CliUsageWindow& value) { + j = nlohmann::json::object(); + j["seconds"] = value.seconds; + j["totals"] = value.totals; + j["window"] = value.window; +} + +struct CliUsageResponse { + CreditResponse credit; + std::vector models; + std::vector recent; + std::vector timeline; + UsageTotals totals; + std::vector windows; +}; + +inline void from_json(const nlohmann::json& j, CliUsageResponse& value) { + if (j.contains("credit") && !j.at("credit").is_null()) { + value.credit = j.at("credit").get(); + } else { + value.credit = CreditResponse{}; + } + if (j.contains("models") && !j.at("models").is_null()) { + value.models = j.at("models").get>(); + } else { + value.models = std::vector{}; + } + if (j.contains("recent") && !j.at("recent").is_null()) { + value.recent = j.at("recent").get>(); + } else { + value.recent = std::vector{}; + } + if (j.contains("timeline") && !j.at("timeline").is_null()) { + value.timeline = j.at("timeline").get>(); + } else { + value.timeline = std::vector{}; + } + if (j.contains("totals") && !j.at("totals").is_null()) { + value.totals = j.at("totals").get(); + } else { + value.totals = UsageTotals{}; + } + if (j.contains("windows") && !j.at("windows").is_null()) { + value.windows = j.at("windows").get>(); + } else { + value.windows = std::vector{}; + } +} + +inline void to_json(nlohmann::json& j, const CliUsageResponse& value) { + j = nlohmann::json::object(); + j["credit"] = value.credit; + j["models"] = value.models; + j["recent"] = value.recent; + j["timeline"] = value.timeline; + j["totals"] = value.totals; + j["windows"] = value.windows; +} + +struct GrantResponse { + std::string access_token; + std::string email; + std::int64_t expires_in; + CliPlan plan; + std::string refresh_token; +}; + +inline void from_json(const nlohmann::json& j, GrantResponse& value) { + if (j.contains("access_token") && !j.at("access_token").is_null()) { + value.access_token = j.at("access_token").get(); + } else { + value.access_token = std::string{}; + } + if (j.contains("email") && !j.at("email").is_null()) { + value.email = j.at("email").get(); + } else { + value.email = std::string{}; + } + if (j.contains("expires_in") && !j.at("expires_in").is_null()) { + value.expires_in = j.at("expires_in").get(); + } else { + value.expires_in = std::int64_t{}; + } + if (j.contains("plan") && !j.at("plan").is_null()) { + value.plan = j.at("plan").get(); + } else { + value.plan = CliPlan{}; + } + if (j.contains("refresh_token") && !j.at("refresh_token").is_null()) { + value.refresh_token = j.at("refresh_token").get(); + } else { + value.refresh_token = std::string{}; + } +} + +inline void to_json(nlohmann::json& j, const GrantResponse& value) { + j = nlohmann::json::object(); + j["access_token"] = value.access_token; + j["email"] = value.email; + j["expires_in"] = value.expires_in; + j["plan"] = value.plan; + j["refresh_token"] = value.refresh_token; +} + +struct IdentityResponse { + std::string email; + std::int64_t monthly_token_limit; + CliPlan plan; + std::int64_t tokens_this_month; +}; + +inline void from_json(const nlohmann::json& j, IdentityResponse& value) { + if (j.contains("email") && !j.at("email").is_null()) { + value.email = j.at("email").get(); + } else { + value.email = std::string{}; + } + if (j.contains("monthly_token_limit") && !j.at("monthly_token_limit").is_null()) { + value.monthly_token_limit = j.at("monthly_token_limit").get(); + } else { + value.monthly_token_limit = std::int64_t{}; + } + if (j.contains("plan") && !j.at("plan").is_null()) { + value.plan = j.at("plan").get(); + } else { + value.plan = CliPlan{}; + } + if (j.contains("tokens_this_month") && !j.at("tokens_this_month").is_null()) { + value.tokens_this_month = j.at("tokens_this_month").get(); + } else { + value.tokens_this_month = std::int64_t{}; + } +} + +inline void to_json(nlohmann::json& j, const IdentityResponse& value) { + j = nlohmann::json::object(); + j["email"] = value.email; + j["monthly_token_limit"] = value.monthly_token_limit; + j["plan"] = value.plan; + j["tokens_this_month"] = value.tokens_this_month; +} + +struct PollResponse { + std::optional access_token; + std::optional email; + std::optional expires_in; + std::optional plan; + std::optional refresh_token; + PollStatus status; +}; + +inline void from_json(const nlohmann::json& j, PollResponse& value) { + if (j.contains("access_token") && !j.at("access_token").is_null()) { + value.access_token = j.at("access_token").get(); + } else { + value.access_token = std::nullopt; + } + if (j.contains("email") && !j.at("email").is_null()) { + value.email = j.at("email").get(); + } else { + value.email = std::nullopt; + } + if (j.contains("expires_in") && !j.at("expires_in").is_null()) { + value.expires_in = j.at("expires_in").get(); + } else { + value.expires_in = std::nullopt; + } + if (j.contains("plan") && !j.at("plan").is_null()) { + value.plan = j.at("plan").get(); + } else { + value.plan = std::nullopt; + } + if (j.contains("refresh_token") && !j.at("refresh_token").is_null()) { + value.refresh_token = j.at("refresh_token").get(); + } else { + value.refresh_token = std::nullopt; + } + if (j.contains("status") && !j.at("status").is_null()) { + value.status = j.at("status").get(); + } else { + value.status = PollStatus{}; + } +} + +inline void to_json(nlohmann::json& j, const PollResponse& value) { + j = nlohmann::json::object(); + if (value.access_token.has_value()) { + j["access_token"] = *value.access_token; + } + if (value.email.has_value()) { + j["email"] = *value.email; + } + if (value.expires_in.has_value()) { + j["expires_in"] = *value.expires_in; + } + if (value.plan.has_value()) { + j["plan"] = *value.plan; + } + if (value.refresh_token.has_value()) { + j["refresh_token"] = *value.refresh_token; + } + j["status"] = value.status; +} + +} // namespace wally::account::contract + +#endif // WALLY_ACCOUNT_CONSOLE_CONTRACT_H diff --git a/src/account/credentials.cpp b/src/account/credentials.cpp index 5927c53..2db66c3 100644 --- a/src/account/credentials.cpp +++ b/src/account/credentials.cpp @@ -1,5 +1,7 @@ #include "account/credentials.h" +#include "account/baked_endpoints.h" + #include #include #include @@ -30,7 +32,7 @@ #include #endif -namespace rcli::account { +namespace wally::account { namespace { namespace fs = std::filesystem; @@ -91,6 +93,22 @@ std::string Env(const char* name) { return value != nullptr ? std::string(value) : std::string(); } +/// `name` (the current, documented variable) wins whenever it's set. `legacy` +/// is read only when `name` is unset, so an installed rcli-era override still +/// works after the wally rename — with a one-line notice, since it's a name +/// nobody should be setting a year from now. +std::string EnvWithLegacyFallback(const char* name, const char* legacy) { + std::string value = Env(name); + if (!value.empty()) { + return value; + } + std::string legacy_value = Env(legacy); + if (!legacy_value.empty()) { + std::fprintf(stderr, "warning: %s is deprecated, use %s instead\n", legacy, name); + } + return legacy_value; +} + std::string Lower(std::string value) { std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); @@ -110,6 +128,33 @@ struct ParsedUrl { bool bracketed = false; }; +/// Reduces a console URL's path to the prefix every endpoint hangs off, or +/// fails if it is not one. +/// +/// A console is not always at the root of its host. Development is reached at +/// `https://inference.runanywhere.ai/api-dev`, where the load balancer strips +/// the prefix and forwards to the dev control plane; the same host without it +/// is production. Refusing the path -- which this did until it was found -- +/// leaves no way to name the dev console at all, so `--console-url` had to be +/// given the backend's own Cloud Run hostname, which bypasses the load balancer +/// and therefore reaches different code than any real client does. +/// +/// A query or fragment is still refused. Every caller builds an endpoint by +/// appending to this string, and `?a=1` + `/v1/me` is not a URL. +bool NormalizeBasePath(const std::string& suffix, std::string* base_path) { + if (suffix.empty() || suffix == "/") { + base_path->clear(); + return true; + } + if (suffix.front() != '/' || suffix.find_first_of("?#") != std::string::npos || + suffix.find("//") != std::string::npos || suffix.find("/.") != std::string::npos || + suffix.size() > 256) { + return false; + } + *base_path = suffix.back() == '/' ? suffix.substr(0, suffix.size() - 1) : suffix; + return true; +} + bool ValidPort(const std::string& port) { if (port.empty()) { return true; @@ -281,7 +326,7 @@ bool Protect(const std::string& plaintext, std::vector* protected DATA_BLOB input{static_cast(plaintext.size()), reinterpret_cast(const_cast(plaintext.data()))}; DATA_BLOB output{}; - if (!CryptProtectData(&input, L"RunAnywhere RCLI cloud session", nullptr, nullptr, nullptr, + if (!CryptProtectData(&input, L"RunAnywhere Wally cloud session", nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output)) { if (error != nullptr) { *error = "Windows could not protect the cloud session"; @@ -515,14 +560,23 @@ bool WriteDocument(const std::string& path, const std::string& document, std::st } // namespace std::string DefaultConsoleUrl() { - const std::string configured = Env("RCLI_CONSOLE_URL"); - return configured.empty() ? kProductionConsoleApi : configured; + const std::string configured = EnvWithLegacyFallback("WALLY_CONSOLE_URL", "RCLI_CONSOLE_URL"); + if (!configured.empty()) { + return configured; + } + // A dev build carries its control plane compiled in (see + // baked_endpoints.h.in): the env override above still wins, production + // builds generate an empty macro and fall through unchanged. + if (WALLY_BAKED_CONSOLE_API_URL[0] != '\0') { + return WALLY_BAKED_CONSOLE_API_URL; + } + return kProductionConsoleApi; } std::vector TrustedBrowserOrigins(const std::string& console_url) { // An operator who declared one has said which console they trust, and that // is then the only one. - const std::string declared = Env("RCLI_CONSOLE_WEB_URL"); + const std::string declared = EnvWithLegacyFallback("WALLY_CONSOLE_WEB_URL", "RCLI_CONSOLE_WEB_URL"); std::string normalized; if (!declared.empty() && NormalizeConsoleUrl(declared, &normalized, nullptr)) { return {normalized}; @@ -530,6 +584,17 @@ std::vector TrustedBrowserOrigins(const std::string& console_url) { if (console_url == kProductionConsoleApi) { return {std::begin(kProductionConsoleWeb), std::end(kProductionConsoleWeb)}; } + // A dev build that baked its control plane also baked which browser + // console may approve sign-ins for it (a local console on loopback, + // typically). Trust holds pairwise: the baked web origin is honored only + // while talking to the baked API, never for an arbitrary --base-url. + if (WALLY_BAKED_CONSOLE_WEB_ORIGIN[0] != '\0' && + console_url == std::string(WALLY_BAKED_CONSOLE_API_URL)) { + std::string baked_web; + if (NormalizeConsoleUrl(WALLY_BAKED_CONSOLE_WEB_ORIGIN, &baked_web, nullptr)) { + return {baked_web, console_url}; + } + } // Anything else — a dev console, a loopback stub — is trusted only at its // own origin. That is the rule that held before, and it is the safe answer // for a console whose approval page we know nothing about. @@ -547,14 +612,17 @@ bool BrowserUrlIsTrusted(const std::string& url, const std::vector& bool NormalizeConsoleUrl(const std::string& input, std::string* normalized, std::string* error) { ParsedUrl parsed; - if (!ParseUrl(input, false, &parsed)) { + std::string base_path; + if (!ParseUrl(input, true, &parsed) || !NormalizeBasePath(parsed.suffix, &base_path)) { if (error != nullptr) { - *error = "console URL must be an HTTPS origin, or HTTP on exact loopback"; + *error = + "console URL must be an HTTPS origin with an optional path, " + "or HTTP on exact loopback"; } return false; } if (normalized != nullptr) { - *normalized = RenderOrigin(parsed); + *normalized = RenderOrigin(parsed) + base_path; } return true; } @@ -567,7 +635,9 @@ bool BrowserUrlIsSafe(const std::string& url) { bool BrowserUrlMatchesConsole(const std::string& url, const std::string& console_url) { ParsedUrl browser; ParsedUrl console; - return ParseUrl(url, true, &browser) && ParseUrl(console_url, false, &console) && + // Origins, not full URLs: a console's base path says where its API lives, + // and says nothing about which pages on that host may approve a sign-in. + return ParseUrl(url, true, &browser) && ParseUrl(console_url, true, &console) && RenderOrigin(browser) == RenderOrigin(console); } @@ -582,20 +652,20 @@ bool SessionTokenIsSafe(const std::string& token) { } std::string ProfileDirectory() { - const std::string override_dir = Env("RCLI_PROFILE_DIR"); + const std::string override_dir = EnvWithLegacyFallback("WALLY_PROFILE_DIR", "RCLI_PROFILE_DIR"); if (!override_dir.empty()) { return override_dir; } #if defined(_WIN32) const std::string home = HomeDirectory(); - return home.empty() ? std::string() : home + "/RunAnywhere/RCLI"; + return home.empty() ? std::string() : home + "/RunAnywhere/Wally"; #else const std::string xdg = Env("XDG_CONFIG_HOME"); if (!xdg.empty()) { - return xdg + "/rcli"; + return xdg + "/wally"; } const std::string home = HomeDirectory(); - return home.empty() ? std::string() : home + "/.config/rcli"; + return home.empty() ? std::string() : home + "/.config/wally"; #endif } @@ -736,4 +806,4 @@ bool Clear(std::string* error) { return true; } -} // namespace rcli::account +} // namespace wally::account diff --git a/src/account/credentials.h b/src/account/credentials.h index 3b67ef6..efb7a0e 100644 --- a/src/account/credentials.h +++ b/src/account/credentials.h @@ -1,10 +1,10 @@ -#ifndef RCLI_ACCOUNT_CREDENTIALS_H -#define RCLI_ACCOUNT_CREDENTIALS_H +#ifndef WALLY_ACCOUNT_CREDENTIALS_H +#define WALLY_ACCOUNT_CREDENTIALS_H #include #include -namespace rcli::account { +namespace wally::account { struct Credentials { std::string console_url; @@ -19,7 +19,7 @@ struct Credentials { } }; -/// The console API used when neither a login flag nor RCLI_CONSOLE_URL is set. +/// The console API used when neither a login flag nor WALLY_CONSOLE_URL is set. /// This is the control plane — `/auth/cli/*`, `/v1/me`, `/v1/cli/*` — and it is /// not the host a person approves a sign-in on. See `TrustedBrowserOrigin`. std::string DefaultConsoleUrl(); @@ -31,7 +31,7 @@ std::string DefaultConsoleUrl(); /// shipped case — so the origin check ran against nothing and the approval URL /// the server sent was taken on trust. /// -/// Three answers, in order. `RCLI_CONSOLE_WEB_URL` alone when an operator +/// Three answers, in order. `WALLY_CONSOLE_WEB_URL` alone when an operator /// declared one. The deployed console's origins when `console_url` is the /// deployed API, because those are different hosts from the API and demanding /// the API's own origin refuses every real sign-in. Otherwise `console_url` @@ -70,6 +70,6 @@ Credentials Load(); bool Save(const Credentials& credentials, std::string* error); bool Clear(std::string* error); -} // namespace rcli::account +} // namespace wally::account -#endif // RCLI_ACCOUNT_CREDENTIALS_H +#endif // WALLY_ACCOUNT_CREDENTIALS_H diff --git a/src/anthropic/messages.cpp b/src/anthropic/messages.cpp index c325f3e..ebebf9e 100644 --- a/src/anthropic/messages.cpp +++ b/src/anthropic/messages.cpp @@ -10,8 +10,9 @@ #include "anthropic/translate.h" #include "io/output.h" +#include "net/loopback_auth.h" -namespace rcli::anthropic { +namespace wally::anthropic { namespace { using Json = nlohmann::json; @@ -43,9 +44,28 @@ struct Runtime { std::string api_key; std::string model; std::string advertised; + // The secret handed to the wrapped tool, and required back on every request. + // Binding to 127.0.0.1 keeps the network out; this keeps other local + // processes out. + std::string local_token; bool verbose = false; }; +// The token the wrapped tool presents, read from either header Claude Code may +// send it in: Authorization: Bearer (from ANTHROPIC_AUTH_TOKEN) or +// x-api-key: (from ANTHROPIC_API_KEY). Both carry the same value. +std::string PresentedToken(const httplib::Request& request) { + if (request.has_header("x-api-key")) { + return request.get_header_value("x-api-key"); + } + const std::string authorization = request.get_header_value("Authorization"); + constexpr const char* kBearer = "Bearer "; + if (authorization.rfind(kBearer, 0) == 0) { + return authorization.substr(std::string(kBearer).size()); + } + return std::string(); +} + std::unique_ptr g_runtime; void ApplyAuth(httplib::Client& client, const std::string& api_key) { @@ -64,6 +84,11 @@ void HandleNonStreaming(Runtime& runtime, const Json& request, httplib::Response client.Post(runtime.prefix + "/chat/completions", upstream.dump(), "application/json"); if (!reply || reply->status < 200 || reply->status >= 300) { response.status = reply ? reply->status : 502; + // A 429 from the hosted API carries a Retry-After the wrapped tool + // should honor; httplib drops upstream headers unless we copy them. + if (reply && reply->status == 429 && reply->has_header("Retry-After")) { + response.set_header("Retry-After", reply->get_header_value("Retry-After")); + } response.set_content( translate::ErrorBody("api_error", reply ? reply->body : std::string("the model endpoint did not answer")), @@ -82,6 +107,11 @@ void HandleNonStreaming(Runtime& runtime, const Json& request, httplib::Response std::string failure; if (translate::PayloadError(parsed, &failure_type, &failure)) { response.status = failure_type == "rate_limit_error" ? 429 : 502; + // A rate-limit error can arrive as a 200 body rather than a 429 status; + // forward the upstream Retry-After either way so the tool backs off. + if (response.status == 429 && reply->has_header("Retry-After")) { + response.set_header("Retry-After", reply->get_header_value("Retry-After")); + } response.set_content(translate::ErrorBody(failure_type, failure), "application/json"); return; } @@ -192,11 +222,20 @@ bool Start(const harness::Endpoint& upstream, const std::string& model, Shim* sh runtime->api_key = upstream.api_key; runtime->model = model; runtime->advertised = advertised.empty() ? model : advertised; + runtime->local_token = wally::net::GenerateLoopbackToken(); runtime->verbose = verbose; Runtime* raw = runtime.get(); raw->server.Post("/v1/messages", [raw](const httplib::Request& request, httplib::Response& response) { + if (!wally::net::ConstantTimeEquals(PresentedToken(request), raw->local_token)) { + response.status = 401; + response.set_content( + translate::ErrorBody("authentication_error", + "this local endpoint only serves the tool wally launched"), + "application/json"); + return; + } if (raw->verbose) { out::status_line("anthropic: POST /v1/messages, " + std::to_string(request.body.size()) + " bytes"); @@ -218,7 +257,7 @@ bool Start(const harness::Endpoint& upstream, const std::string& model, Shim* sh } } catch (const std::exception& error) { // httplib does not catch, and an exception leaving here reaches - // std::terminate: the editor's model call would abort rcli. + // std::terminate: the editor's model call would abort wally. if (raw->verbose) { out::status_line(std::string("anthropic: request failed: ") + error.what()); } @@ -278,7 +317,7 @@ bool Start(const harness::Endpoint& upstream, const std::string& model, Shim* sh response.set_content( translate::ErrorBody("not_found_error", request.method + " " + request.path + - " is not something rcli translates"), + " is not something wally translates"), "application/json"); } }); @@ -294,10 +333,11 @@ bool Start(const harness::Endpoint& upstream, const std::string& model, Shim* sh started->thread = std::thread([started] { started->server.listen_after_bind(); }); shim->base_url = "http://127.0.0.1:" + std::to_string(port); - // Never the upstream key: the client only has to send something, and - // handing it a real console token would put it in that process's - // environment where it does not belong. - shim->auth_token = "rcli-local"; + // A per-session secret, never the upstream key: handing the tool a real + // console token would put it in that process's environment where it does + // not belong, and a fixed value would let any local process spend the + // signed-in user's credit. The server checks this back on every request. + shim->auth_token = started->local_token; shim->running = true; return true; } @@ -317,4 +357,4 @@ void Stop(Shim* shim) { } } -} // namespace rcli::anthropic +} // namespace wally::anthropic diff --git a/src/anthropic/messages.h b/src/anthropic/messages.h index e4cb226..5f76083 100644 --- a/src/anthropic/messages.h +++ b/src/anthropic/messages.h @@ -1,5 +1,5 @@ -#ifndef RCLI_ANTHROPIC_MESSAGES_H -#define RCLI_ANTHROPIC_MESSAGES_H +#ifndef WALLY_ANTHROPIC_MESSAGES_H +#define WALLY_ANTHROPIC_MESSAGES_H #include @@ -10,7 +10,7 @@ /// Claude Code, Claude Desktop and Cowork all talk the Anthropic Messages API /// and are pointed elsewhere with ANTHROPIC_BASE_URL. Our server speaks /// OpenAI: /v1/models, /v1/chat/completions, /health, and nothing else. The two -/// never meet, which is why `rcli opencode` works today and `rcli claude-code` +/// never meet, which is why `wally opencode` works today and `wally claude-code` /// could not. /// /// This is the translator between them. It serves POST /v1/messages on @@ -23,7 +23,7 @@ /// vendor's wire format, which is an integration detail of this CLI, not /// inference logic every SDK consumer needs. If a second consumer ever wants /// it, that is the moment to move it down a layer. -namespace rcli::anthropic { +namespace wally::anthropic { /// A running translator. struct Shim { @@ -57,6 +57,6 @@ bool Start(const harness::Endpoint& upstream, const std::string& model, Shim* sh /// Stops the translator and waits for its thread. Safe on a stopped shim. void Stop(Shim* shim); -} // namespace rcli::anthropic +} // namespace wally::anthropic -#endif // RCLI_ANTHROPIC_MESSAGES_H +#endif // WALLY_ANTHROPIC_MESSAGES_H diff --git a/src/anthropic/translate.cpp b/src/anthropic/translate.cpp index b80d783..245170a 100644 --- a/src/anthropic/translate.cpp +++ b/src/anthropic/translate.cpp @@ -3,7 +3,7 @@ #include #include -namespace rcli::anthropic::translate { +namespace wally::anthropic::translate { namespace { /// A string field, or empty when it is absent or is something else. @@ -293,7 +293,7 @@ Json ResponseToAnthropic(const Json& openai, const std::string& model) { Json out; const std::string reply_id = Field(openai, "id"); - out["id"] = reply_id.empty() ? std::string("msg_rcli") : reply_id; + out["id"] = reply_id.empty() ? std::string("msg_wally") : reply_id; out["type"] = "message"; out["role"] = "assistant"; out["model"] = model; @@ -366,7 +366,7 @@ std::string StreamChunkToAnthropic(const Json& chunk, StreamState* state) { // of null is exactly what some servers send. state->message_id = chunk.contains("id") && chunk["id"].is_string() ? chunk["id"].get() - : std::string("msg_rcli"); + : std::string("msg_wally"); Json start; start["type"] = "message_start"; start["message"] = Json{{"id", state->message_id}, @@ -566,4 +566,4 @@ std::string ErrorBody(const std::string& type, const std::string& message) { return Json{{"type", "error"}, {"error", Json{{"type", type}, {"message", message}}}}.dump(); } -} // namespace rcli::anthropic::translate +} // namespace wally::anthropic::translate diff --git a/src/anthropic/translate.h b/src/anthropic/translate.h index 5c30dac..4cd1079 100644 --- a/src/anthropic/translate.h +++ b/src/anthropic/translate.h @@ -1,5 +1,5 @@ -#ifndef RCLI_ANTHROPIC_TRANSLATE_H -#define RCLI_ANTHROPIC_TRANSLATE_H +#ifndef WALLY_ANTHROPIC_TRANSLATE_H +#define WALLY_ANTHROPIC_TRANSLATE_H #include #include @@ -13,7 +13,7 @@ /// and reading JSON back, which is the only part of this worth testing: the /// HTTP plumbing is cpp-httplib's, and the interesting bugs are all in the /// shapes. -namespace rcli::anthropic::translate { +namespace wally::anthropic::translate { using Json = nlohmann::json; @@ -82,6 +82,6 @@ std::string ErrorBody(const std::string& type, const std::string& message); /// Returns false when `payload` carries no error, which is the ordinary case. bool PayloadError(const Json& payload, std::string* type, std::string* message); -} // namespace rcli::anthropic::translate +} // namespace wally::anthropic::translate -#endif // RCLI_ANTHROPIC_TRANSLATE_H +#endif // WALLY_ANTHROPIC_TRANSLATE_H diff --git a/src/app.cpp b/src/app.cpp index c93d21f..d57f1c6 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1,24 +1,28 @@ #include "app.h" #include +#include #include +#include +#include #include #include "bootstrap.h" +#include "cli_formatter.h" #include "commands/commands.h" #include "io/output.h" #include "rac/core/rac_logger.h" -#ifndef RCLI_VERSION -#define RCLI_VERSION "0.0.0-dev" +#ifndef WALLY_VERSION +#define WALLY_VERSION "0.0.0-dev" #endif -namespace rcli { +namespace wally { void configure_app(CLI::App& app, GlobalOptions& options) { - app.set_version_flag("--version,-V", std::string("rcli ") + RCLI_VERSION); + app.set_version_flag("--version,-V", std::string("wally ") + WALLY_VERSION); app.require_subcommand(0, 1); app.fallthrough(true); @@ -26,24 +30,32 @@ void configure_app(CLI::App& app, GlobalOptions& options) { app.add_flag("-v,--verbose", options.verbose, "Debug logging on stderr"); app.add_flag("-q,--quiet", options.quiet, "Errors only on stderr"); app.add_flag("--no-progress", options.no_progress, "Disable progress rendering"); + app.add_flag("--no-color", options.no_color, "Disable colored --help output"); app.add_option("--home", options.home_override, "RunAnywhere home directory (default: $RUNANYWHERE_HOME or " "~/.local/share/runanywhere; models live under /Models)"); // Control-plane connection. validation happens in resolve_connection(). + // Developer/SDK-facing, not something a person reaches for day to day -- + // group("") drops them out of the default --help listing the same way + // `telemetry` is hidden below, while leaving them fully parseable + // (flags and RUNANYWHERE_* env fallbacks both still resolve). app.add_option("--environment", options.environment, "SDK environment: development (default, keyless OSS → baked staging " "backend) or production (API key + https URL).") ->envname("RUNANYWHERE_ENVIRONMENT") - ->check(CLI::IsMember({"dev", "development", "prod", "production"})); + ->check(CLI::IsMember({"dev", "development", "prod", "production"})) + ->group(""); app.add_option("--base-url", options.base_url, "Backend base URL. Optional in development (baked staging URL). " "Required https for production.") - ->envname("RUNANYWHERE_BASE_URL"); + ->envname("RUNANYWHERE_BASE_URL") + ->group(""); app.add_option("--api-key", options.api_key, "Control-plane API key (required for production; omit for " "keyless development)") - ->envname("RUNANYWHERE_API_KEY"); + ->envname("RUNANYWHERE_API_KEY") + ->group(""); // Namespaces first (the spec grammar), then the terminal aliases, then the // infrastructure commands — that is the order `--help` lists them in. @@ -70,6 +82,7 @@ void configure_app(CLI::App& app, GlobalOptions& options) { commands::register_bench(app, options); commands::register_backends(app, options); commands::register_info(app, options); + commands::register_about(app, options); commands::register_version(app, options); commands::register_auth(app, options); commands::register_account(app, options); @@ -77,20 +90,88 @@ void configure_app(CLI::App& app, GlobalOptions& options) { commands::register_editors(app, options); commands::register_harness(app, options); commands::register_telemetry(app, options); + + // `--help` groups: CLI11 prints one heading per distinct group string, in + // the order each group is first seen (Formatter::make_subcommands), so + // this order is the print order. Centralized here rather than one + // ->group() call per register_* file: 36 top-level commands with no + // grouping at all used to land in a single default SUBCOMMANDS: bucket. + const std::vector> help_groups = { + {"llm", "Generate"}, {"vlm", "Generate"}, {"stt", "Generate"}, + {"tts", "Generate"}, {"vad", "Generate"}, {"embed", "Generate"}, + {"rerank", "Generate"}, {"image", "Generate"}, {"diarize", "Generate"}, + {"segment", "Generate"}, {"voice", "Generate"}, {"rag", "Generate"}, + {"run", "Shortcuts"}, {"chat", "Shortcuts"}, {"ls", "Shortcuts"}, + {"show", "Shortcuts"}, {"pull", "Shortcuts"}, {"rm", "Shortcuts"}, + {"models", "Models"}, {"lora", "Models"}, + {"serve", "Serve & measure"}, {"bench", "Serve & measure"}, + {"backends", "Serve & measure"}, {"info", "Serve & measure"}, + {"about", "Serve & measure"}, {"version", "Serve & measure"}, + {"auth", "Account"}, {"login", "Account"}, {"logout", "Account"}, + {"whoami", "Account"}, {"usage", "Account"}, + {"opencode", "Editors & agents"}, {"claude-code", "Editors & agents"}, + {"claude-desktop", "Editors & agents"}, {"clion", "Editors & agents"}, + {"rustrover", "Editors & agents"}, + }; + // configure_app() runs ahead of run()'s own try/catch (and tests call it + // directly with none at all), so a typo here must never propagate as an + // uncaught exception -- that crashed the Windows CI binaries outright + // (0xC0000409, no diagnostic) the one time a name here didn't match. + // Report it and keep going with the default flat listing rather than + // taking the whole CLI down over a --help cosmetic. + for (const auto& [name, group] : help_groups) { + try { + app.get_subcommand(name)->group(group); + } catch (const CLI::OptionNotFound&) { + out::error_line(std::string("internal: --help grouping named an unknown " + "subcommand '") + + name + "', skipping it"); + } + } + // Internal debug tool, not a command a user reaches for. An empty group + // string drops a subcommand out of the default listing entirely + // (Formatter::make_subcommands) while it stays fully callable — + // `wally telemetry --help` still works. + try { + app.get_subcommand("telemetry")->group(""); + } catch (const CLI::OptionNotFound&) { + // Nothing to hide if it isn't there. + } } int run(int argc, char** argv) { GlobalOptions options; + // Decided ahead of CLI11's own parse: a subcommand inherits its parent's + // formatter_ at construction time (App::App), which configure_app() + // triggers below, so the color decision has to already be settled before + // that call. Plain argv scan rather than parsing --no-color for real. + bool no_color_requested = false; + for (int i = 1; i < argc; ++i) { + if (std::string(argv[i]) == "--no-color") { + no_color_requested = true; + break; + } + } + CLI::App app{"RunAnywhere on-device AI CLI — llm, vlm, stt, tts, vad, embed, rerank, " "image, rag, voice and the models that back them"}; + app.formatter(std::make_shared(color_output_enabled(no_color_requested))); configure_app(app, options); + // Every subcommand here loads a model on this machine; a hosted console + // model (glm-5.3-flash, ...) has no path through `run`/`llm generate` at + // all, and that dead end used to be the only place someone learned the + // cloud path exists. + app.footer( + "A model your account has on the hosted console (not this machine) runs through " + "`wally claude-code -m ` or `wally opencode --cloud -m `, not `run`/`llm " + "generate`."); int exit_code = 0; try { app.parse(argc, argv); if (app.get_subcommands().empty()) { - // Bare `rcli` prints help like `ollama` does. + // Bare `wally` prints help like `ollama` does. out::status_line(app.help()); } } catch (const CLI::CallForHelp& e) { @@ -111,23 +192,31 @@ int run(int argc, char** argv) { return exit_code; } -} // namespace rcli +} // namespace wally + +// Called from Swift, before MLX.register() — measured, not inferred: the +// Swift host logs 3 more INFO lines during that call (Swift callbacks +// registered, MLX backend registered, RunAnywhereMLX backend registered +// successfully), all before wally_run_main ever runs, so muting only inside +// wally_run_main left 5 RAC lines on `wally --version` instead of the 2 the +// old comment here assumed. Splitting the mute into its own entry point, +// called from WallyMLX.swift ahead of MLX.register(), is what actually gets +// there. +extern "C" void wally_quiet_sdk_logging() { + rac_logger_set_min_level(RAC_LOG_ERROR); +} -extern "C" int rcli_run_main(int argc, char** argv) { - // Here, not in main(). The shipped Apple binary is the Swift MLX host, - // which registers its callbacks and enters at this symbol; it never runs - // main.cpp, so the quieting that used to live there covered `rcli-cxx` and - // left the product binary noisy. Measured on `info`: `rcli` 4 RAC lines, - // `rcli-cxx` 2. Moving it here is what puts the product binary on the same - // footing; the 2 it should land on is inferred from rcli-cxx, not measured, - // because the Swift host does not build without the SDK Swift tree. +extern "C" int wally_run_main(int argc, char** argv) { + // Covers `wally-cxx` and any other entry that skips the Swift host, where + // wally_quiet_sdk_logging() above is never called. Idempotent with it. // - // Those 2 are backend registration WARNs emitted during static - // initialisation, which completes before any entry point runs. No call from - // inside the process can catch them; silencing them needs a pre-registration - // hook in the kit, and the kit owns backend registration. + // The 2 RAC lines still on stderr on every entry point are backend + // registration WARNs emitted during static initialisation, which + // completes before any entry point runs. No call from inside the process + // can catch them; silencing them needs a pre-registration hook in the + // kit, and the kit owns backend registration. // // `--verbose` raises the level again in bootstrap(). rac_logger_set_min_level(RAC_LOG_ERROR); - return rcli::run(argc, argv); + return wally::run(argc, argv); } diff --git a/src/app.h b/src/app.h index 8bd8b53..2f8f500 100644 --- a/src/app.h +++ b/src/app.h @@ -1,25 +1,25 @@ /** * @file app.h - * @brief Shared rcli app wiring for the binary and in-process tests. + * @brief Shared wally app wiring for the binary and in-process tests. */ -#ifndef RCLI_APP_H -#define RCLI_APP_H +#ifndef WALLY_APP_H +#define WALLY_APP_H #include #include "bootstrap.h" -namespace rcli { +namespace wally { void configure_app(CLI::App& app, GlobalOptions& options); int run(int argc, char** argv); -} // namespace rcli +} // namespace wally /// The one entry point both binaries use: `main()` here, and the Swift MLX host -/// that ships as `rcli` on Apple. Anything that must happen before a command +/// that ships as `wally` on Apple. Anything that must happen before a command /// runs belongs behind this, not in `main()`, which the product binary skips. -extern "C" int rcli_run_main(int argc, char** argv); +extern "C" int wally_run_main(int argc, char** argv); -#endif // RCLI_APP_H +#endif // WALLY_APP_H diff --git a/src/bootstrap.cpp b/src/bootstrap.cpp index fd54985..fd93036 100644 --- a/src/bootstrap.cpp +++ b/src/bootstrap.cpp @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include #include #if !defined(_WIN32) #include @@ -35,19 +37,19 @@ #include "device_info.h" #include "io/output.h" -#if defined(RCLI_HAS_LLAMACPP) +#if defined(WALLY_HAS_LLAMACPP) #include "rac/backends/rac_llm_llamacpp.h" #endif -#if defined(RCLI_HAS_ONNX) +#if defined(WALLY_HAS_ONNX) #include "rac/plugin/rac_plugin_entry_onnx.h" #endif -#if defined(RCLI_HAS_SHERPA) +#if defined(WALLY_HAS_SHERPA) #include "rac/plugin/rac_plugin_entry_sherpa.h" #endif -#if defined(RCLI_HAS_MLX) +#if defined(WALLY_HAS_MLX) #include "rac/backends/rac_mlx.h" #endif -#if defined(RCLI_HAS_NEURT) +#if defined(WALLY_HAS_NEURT) // The neurt engine (Apple-only: ANE LLM + CoreML diffusion) has no dedicated // rac_backend_neurt_register() fn; register its plugin entry directly. This // call also keeps the static rac_backend_neurt archive linked (references @@ -55,11 +57,11 @@ #include "rac/plugin/rac_plugin_entry.h" #include "rac/plugin/rac_plugin_entry_neurt.h" #endif -#if defined(RCLI_HAS_QHEXRT) +#if defined(WALLY_HAS_QHEXRT) extern "C" rac_result_t rac_backend_qhexrt_register(void); #endif -namespace rcli { +namespace wally { namespace { @@ -76,10 +78,24 @@ rac_log_level_t log_level_for(const GlobalOptions &options) { return RAC_LOG_DEBUG; } // Quiet by default (like ollama): SDK internals only surface at ERROR. - // rcli prints its own user-facing status/progress lines on stderr. + // wally prints its own user-facing status/progress lines on stderr. return RAC_LOG_ERROR; } +#if defined(WALLY_HAS_LLAMACPP) +// ggml/llama.cpp prints its backend and Metal init straight to stderr, outside +// rac_logger's gate, so --quiet never reached it. Route each line back through +// the logger at ggml's own level: the default ERROR floor and --quiet then +// drop it like any other SDK log, and --verbose still shows it. Only compiled +// where llama.cpp is linked -- windows-arm64's kit ships no llamacpp backend, +// so rac_llamacpp_set_log_callback isn't a symbol there. +void route_ggml_log(rac_log_level_t level, const char *message, void *) { + if (message != nullptr && level >= rac_logger_get_min_level()) { + rac_logger_logf(level, "LLM.LlamaCpp.GGML", nullptr, "%s", message); + } +} +#endif + std::string first_env_value(const char *first, const char *second, const char *third) { const char *keys[] = {first, second, third}; @@ -238,11 +254,11 @@ void initialize_sdk_metadata(const Connection &connection) { sdk_config.base_url = effective_base_url.c_str(); sdk_config.device_id = device_id[0] != '\0' ? device_id : ""; sdk_config.platform = desktop_platform(); - sdk_config.sdk_version = RCLI_VERSION; + sdk_config.sdk_version = WALLY_VERSION; sdk_config.client_info.sdk_binding = "cli"; - sdk_config.client_info.app_identifier = "ai.runanywhere.rcli"; + sdk_config.client_info.app_identifier = "ai.runanywhere.wally"; sdk_config.client_info.app_name = "RunAnywhere CLI"; - sdk_config.client_info.app_version = RCLI_VERSION; + sdk_config.client_info.app_version = WALLY_VERSION; sdk_config.client_info.app_build = nullptr; sdk_config.client_info.locale = locale.empty() ? nullptr : locale.c_str(); sdk_config.client_info.timezone = timezone.empty() ? nullptr : timezone.c_str(); @@ -258,7 +274,7 @@ void initialize_sdk_metadata(const Connection &connection) { // rac_telemetry_manager_set_http_callback (user_data = the manager) so the // outcome is reported back through rac_telemetry_manager_http_complete. Mirrors // the control-plane POST performed by commons' auth path. -void rcli_telemetry_http_callback(void *user_data, const char *endpoint, +void wally_telemetry_http_callback(void *user_data, const char *endpoint, const char *json_body, size_t json_length, rac_bool_t requires_auth) { auto *manager = static_cast(user_data); @@ -329,13 +345,13 @@ void rcli_telemetry_http_callback(void *user_data, const char *endpoint, } if (!ok) { // Surface the exact backend rejection (status + response body) so schema - // mismatches (e.g. strict extra_forbidden 422s) are diagnosable from rcli. + // mismatches (e.g. strict extra_forbidden 422s) are diagnosable from wally. out::status_line(std::string("telemetry POST ") + (endpoint ? endpoint : "?") + " -> rc=" + out::describe_result(rc) + " http=" + std::to_string(response.status) + " body=" + (body.empty() ? "(empty)" : body)); // DEBUG: dump the exact request JSON so a malformed offset can be inspected. - if (const char *dump = std::getenv("RCLI_TELEMETRY_DUMP"); + if (const char *dump = std::getenv("WALLY_TELEMETRY_DUMP"); dump != nullptr && dump[0] != '\0' && json_body != nullptr) { if (FILE *fp = std::fopen(dump, "ab")) { std::fwrite(json_body, 1, json_length, fp); @@ -388,14 +404,14 @@ void initialize_telemetry_auth(const Connection &connection) { // Create + register the telemetry sink BEFORE Phase 2 so its flush has a sink // and events emitted during subsequent commands are tracked. Delivery runs - // through rcli_telemetry_http_callback over the desktop HTTP transport; the + // through wally_telemetry_http_callback over the desktop HTTP transport; the // terminal batch flushes in rac_shutdown() during teardown. g_telemetry_manager = rac_telemetry_manager_create( connection.environment, device_id[0] != '\0' ? device_id : "", - desktop_platform(), RCLI_VERSION); + desktop_platform(), WALLY_VERSION); if (g_telemetry_manager != nullptr) { rac_telemetry_manager_set_http_callback( - g_telemetry_manager, rcli_telemetry_http_callback, g_telemetry_manager); + g_telemetry_manager, wally_telemetry_http_callback, g_telemetry_manager); rac_events_set_telemetry_sink(g_telemetry_manager); } @@ -407,7 +423,7 @@ void initialize_telemetry_auth(const Connection &connection) { phase1.set_device_id(device_id); } phase1.set_platform(desktop_platform()); - phase1.set_sdk_version(RCLI_VERSION); + phase1.set_sdk_version(WALLY_VERSION); std::string phase1_bytes; if (!phase1.SerializeToString(&phase1_bytes)) { @@ -549,6 +565,20 @@ rac_result_t bootstrap(const GlobalOptions &options, Bootstrapped *out) { } if (!g_bootstrapped) { + // rac_model_paths_set_base_dir happily creates /Models on first use, + // which is right for the real default — but an explicit --home that + // doesn't exist is far more often a typo than a fresh directory someone + // wants populated from nothing, and the only symptom otherwise is a + // catalog that looks empty with no explanation at all. + if (!options.home_override.empty()) { + std::error_code exists_ec; + if (!std::filesystem::exists(home, exists_ec)) { + out::status_line("warning: --home '" + home + + "' does not exist yet; it will be created empty " + "(pass the right path, or `wally pull` into this one)"); + } + } + rac_result_t rc = rac_desktop_adapter_init(nullptr, &g_adapter); if (rc != RAC_SUCCESS) { out::error_line("desktop adapter init failed: " + @@ -570,11 +600,14 @@ rac_result_t bootstrap(const GlobalOptions &options, Bootstrapped *out) { const rac_log_level_t log_level = log_level_for(options); rac_logger_set_stderr_always(RAC_FALSE); rac_logger_set_min_level(log_level); +#if defined(WALLY_HAS_LLAMACPP) + rac_llamacpp_set_log_callback(route_ggml_log, nullptr); +#endif rac_config_t config = {}; config.platform_adapter = &g_adapter; config.log_level = log_level; - config.log_tag = "rcli"; + config.log_tag = "wally"; rc = rac_init(&config); if (rc != RAC_SUCCESS) { out::error_line("rac_init failed: " + out::describe_result(rc)); @@ -599,36 +632,41 @@ rac_result_t bootstrap(const GlobalOptions &options, Bootstrapped *out) { initialize_telemetry_auth(connection); -#if defined(RCLI_HAS_LLAMACPP) +#if defined(WALLY_HAS_LLAMACPP) if (rac_backend_llamacpp_register() != RAC_SUCCESS) { out::status_line("warning: llamacpp backend failed to register"); } #endif -#if defined(RCLI_HAS_ONNX) +#if defined(WALLY_HAS_ONNX) if (rac_backend_onnx_register() != RAC_SUCCESS) { out::status_line("warning: onnx backend failed to register"); } #endif -#if defined(RCLI_HAS_SHERPA) +#if defined(WALLY_HAS_SHERPA) if (rac_backend_sherpa_register() != RAC_SUCCESS) { out::status_line("warning: sherpa backend failed to register"); } #endif -#if defined(RCLI_HAS_MLX) +#if defined(WALLY_HAS_MLX) + // A C++-only host (wally-cxx) links the MLX plugin from the kit but provides + // no MLX runtime callbacks, so availability is false by design. That is the + // normal state for that build, not a warning — note it only under --verbose. + // A host that DOES provide the runtime and still fails to register is a real + // problem and always warns. if (rac_mlx_is_available() != RAC_TRUE) { - out::status_line( - "warning: mlx backend requires MLX runtime callbacks; skipping registration"); + if (options.verbose) { + out::status_line("mlx runtime callbacks not provided; skipping MLX backend"); + } } else if (rac_backend_mlx_register() != RAC_SUCCESS) { - out::status_line( - "warning: mlx backend requires MLX runtime callbacks; backend failed to register"); + out::status_line("warning: mlx backend failed to register"); } #endif -#if defined(RCLI_HAS_NEURT) +#if defined(WALLY_HAS_NEURT) if (rac_plugin_register(rac_plugin_entry_neurt()) != RAC_SUCCESS) { out::status_line("warning: neurt (Apple Neural Engine) backend failed to register"); } #endif -#if defined(RCLI_HAS_QHEXRT) +#if defined(WALLY_HAS_QHEXRT) if (rac_backend_qhexrt_register() != RAC_SUCCESS) { out::status_line("warning: qhexrt (Qualcomm Hexagon NPU) backend failed to register"); } @@ -670,4 +708,4 @@ void shutdown() { rac_telemetry_manager_t *active_telemetry_manager() { return g_telemetry_manager; } -} // namespace rcli +} // namespace wally diff --git a/src/bootstrap.h b/src/bootstrap.h index fc6202c..ac2a5f5 100644 --- a/src/bootstrap.h +++ b/src/bootstrap.h @@ -1,6 +1,6 @@ /** * @file bootstrap.h - * @brief One-call SDK bring-up for every rcli command. + * @brief One-call SDK bring-up for every wally command. * * Mirrors the canonical bootstrap proven by the commons real-inference tests * (tests/test_voice_agent.cpp) with real desktop I/O: @@ -11,8 +11,8 @@ * Commands call bootstrap() exactly once; it is idempotent within a process. */ -#ifndef RCLI_BOOTSTRAP_H -#define RCLI_BOOTSTRAP_H +#ifndef WALLY_BOOTSTRAP_H +#define WALLY_BOOTSTRAP_H #include @@ -21,7 +21,7 @@ typedef struct rac_telemetry_manager rac_telemetry_manager_t; -namespace rcli { +namespace wally { /** Global flags shared by all subcommands (parsed in main.cpp). */ struct GlobalOptions { @@ -29,6 +29,8 @@ struct GlobalOptions { bool verbose = false; bool quiet = false; bool no_progress = false; + bool no_color = false; // --no-color flag; the actual ANSI decision is made + // in run() before parsing, see cli_formatter.h std::string home_override; // --home flag // Control-plane connection. CLI11 fills these from @@ -90,6 +92,6 @@ void shutdown(); */ rac_telemetry_manager_t* active_telemetry_manager(); -} // namespace rcli +} // namespace wally -#endif // RCLI_BOOTSTRAP_H +#endif // WALLY_BOOTSTRAP_H diff --git a/src/catalog/catalog.cpp b/src/catalog/catalog.cpp index 11664d7..53d12ce 100644 --- a/src/catalog/catalog.cpp +++ b/src/catalog/catalog.cpp @@ -8,7 +8,7 @@ #include "io/output.h" #include "io/proto.h" -namespace rcli::catalog { +namespace wally::catalog { namespace { @@ -1654,7 +1654,7 @@ constexpr CatalogEntry kCatalog[] = { v1::MODEL_CATEGORY_EMBEDDING, v1::INFERENCE_FRAMEWORK_ONNX, v1::MODEL_FORMAT_ONNX, nullptr, kMiniLmFiles, 2, 90 * MB, 0, false}, - // --- Reranking (llama.cpp cross-encoder; `rcli rerank -m `) --- + // --- Reranking (llama.cpp cross-encoder; `wally rerank -m `) --- {"bge-reranker-v2-m3-q4_k_m", "bge-reranker", "BGE Reranker v2-m3 Q4_K_M (Reranking)", v1::MODEL_CATEGORY_EMBEDDING, v1::INFERENCE_FRAMEWORK_LLAMA_CPP, v1::MODEL_FORMAT_GGUF, @@ -1665,8 +1665,8 @@ constexpr CatalogEntry kCatalog[] = { // --- Image generation (CoreML diffusion; Apple only) --- // Apple-optimized Stable Diffusion 1.5. Id matches the built-in diffusion // model registry (diffusion_model_registry.cpp) and the Swift facade's - // canonical `.imageGeneration` model, so `rcli image generate` resolves it - // and `rcli list` shows it. The palettized CoreML bundle is a directory of + // canonical `.imageGeneration` model, so `wally image generate` resolves it + // and `wally list` shows it. The palettized CoreML bundle is a directory of // compiled .mlmodelc sub-models served by the `coreml` engine; a // pre-fetched bundle can also be passed to `--model` as a local path. // The Hugging Face *repo page* is HTML (~160 KB) and is not a model. @@ -1679,7 +1679,7 @@ constexpr CatalogEntry kCatalog[] = { "coreml-stable-diffusion-v1-5-palettized_split_einsum_v2_compiled.zip", nullptr, 0, 1500 * MB, 0, false}, // NeuRT advertises LLM + STT + EMBED + RERANK + VLM + EMBED_IMAGE + DIFFUSION; folder refs (same ModelInfo - // path as sd15). Pass a local compiled tree to `--model` — `rcli pull` of a + // path as sd15). Pass a local compiled tree to `--model` — `wally pull` of a // Hugging Face repo page is HTML, not a bundle. {"lfm2_5_230m_ane", "lfm2-230m-ane", "LFM2.5 230M (Apple Neural Engine)", v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_COREML, @@ -1695,7 +1695,7 @@ constexpr CatalogEntry kCatalog[] = { // that "loads, undrivable" — its manifest parsed and its encoder graph bound, but the SDK's // neurt engine filled no embedding_ops, so nothing could drive it. Gate B on an M4 Max: // cosine vs the fp32 gold min 0.9588 / mean 0.9890, relevant ranked above irrelevant in 4/4 - // gold records. `rcli embed --engine ane --model `. + // gold records. `wally embed --engine ane --model `. // // ASYMMETRIC: the bundle declares "query: " / "passage: " prefixes and returns a materially // different vector per role. Measured here, prefixing correctly moves query-vs-relevant @@ -1709,9 +1709,9 @@ constexpr CatalogEntry kCatalog[] = { 0, 0, 0, false}, // The first ANE RERANK row. Its `score` graph role was outside NeuRT's manifest vocabulary, so // the published bundle was rejected before a graph was touched. Gate on an M4 Max: positive - // beats negative on 5/5 gold triples, matching the reference. `rcli rerank --engine ane`. + // beats negative on 5/5 gold triples, matching the reference. `wally rerank --engine ane`. // - // Category note: MODEL_CATEGORY_RERANK (value 12) exists and is the right one. RCLI's other + // Category note: MODEL_CATEGORY_RERANK (value 12) exists and is the right one. WALLY's other // reranker (bge-reranker-v2-m3, above) uses MODEL_CATEGORY_EMBEDDING — a pre-existing // inconsistency left alone here rather than changed as a drive-by. {"nv_rerankqa_1b_v2_ane", "nv-rerank-ane", @@ -1723,7 +1723,7 @@ constexpr CatalogEntry kCatalog[] = { // The first ANE VLM row. Image + prompt -> text: the runtime runs the vision tower, splices its // 256 visual tokens over the prompt's positions, then drives the ordinary chunked // text decode. Gate on an M4 Max: reproduced all 3 gold generations EXACTLY, word for word, - // including prompt-token counts (274/273/274). `rcli vlm generate --engine ane`. + // including prompt-token counts (274/273/274). `wally vlm generate --engine ane`. {"internvl3_5_1b_ane", "internvl-1b-ane", "InternVL3.5 1B (Apple Neural Engine)", v1::MODEL_CATEGORY_MULTIMODAL, v1::INFERENCE_FRAMEWORK_COREML, @@ -1754,7 +1754,7 @@ constexpr CatalogEntry kCatalog[] = { v1::MODEL_CATEGORY_SPEECH_SYNTHESIS, v1::INFERENCE_FRAMEWORK_COREML, v1::MODEL_FORMAT_MLPACKAGE, // The .zip, NOT the repo root. A bare huggingface.co// URL makes - // `rcli pull` fetch the repo's HTML PAGE -- 120 KB of markup written to disk + // `wally pull` fetch the repo's HTML PAGE -- 120 KB of markup written to disk // under the model id, with a cheerful "done 100%". Every other ANE row here // still has that shape and is therefore listable but not pullable. "https://huggingface.co/runanywhere/Kokoro-82M_ANE/resolve/main/" @@ -1950,7 +1950,7 @@ constexpr CatalogEntry kCatalog[] = { // resolve the same native catalog the Android/Flutter apps use. Folder // URLs are registered as ModelInfo (same path as CoreML diffusion) — // the QNN context tree is fetched by the QHexRT bundle policy or passed - // as a local `*_HNPU` directory to `rcli run`. + // as a local `*_HNPU` directory to `wally run`. {"lfm2_5_230m", "lfm2-230m-npu", "LFM2.5 230M (Hexagon NPU)", v1::MODEL_CATEGORY_LANGUAGE, v1::INFERENCE_FRAMEWORK_QHEXRT, v1::MODEL_FORMAT_QNN_CONTEXT, @@ -1973,7 +1973,7 @@ constexpr CatalogEntry kCatalog[] = { false}, // Non-LLM Hexagon primitives. Ids match engines/qhexrt/qhexrt_model_catalog.cpp. // Same folder-URL registration as the LLM rows — pass a local `*_HNPU` - // directory; do not expect `rcli pull` to fetch the HF repo HTML. + // directory; do not expect `wally pull` to fetch the HF repo HTML. {"whisper_base", "whisper-base-npu", "Whisper Base (Hexagon NPU)", v1::MODEL_CATEGORY_SPEECH_RECOGNITION, v1::INFERENCE_FRAMEWORK_QHEXRT, v1::MODEL_FORMAT_QNN_CONTEXT, @@ -2019,7 +2019,7 @@ constexpr CatalogEntry kCatalog[] = { // .build/checkouts/mlx-swift-lm/Libraries/{MLXLLM,MLXVLM}/*Factory.swift — // only "nemotron_h" exists, a different string). Loading either would fail // with ModelFactoryError.unsupportedModelType. The GGUF+mmproj rows above - // (llama.cpp) remain the way to run these two on rcli. + // (llama.cpp) remain the way to run these two on wally. }; constexpr size_t kCatalogCount = sizeof(kCatalog) / sizeof(kCatalog[0]); @@ -2028,12 +2028,12 @@ rac_result_t register_entry(const CatalogEntry &entry) { // CoreML bundles (a directory of compiled .mlmodelc sub-models) don't fit the // URL / multi-file download-factory grammar, which rejects a bare repo ref. // Register the ModelInfo directly so the id resolves in the general registry - // (and `rcli list` shows it); the bundle itself is fetched by the diffusion - // pipeline or supplied to `rcli image --model `. + // (and `wally list` shows it); the bundle itself is fetched by the diffusion + // pipeline or supplied to `wally image --model `. if (entry.framework == v1::INFERENCE_FRAMEWORK_COREML || entry.framework == v1::INFERENCE_FRAMEWORK_QHEXRT) { // CoreML bundles and QHexRT HNPU folders don't fit the single-file - // download-factory grammar. Register ModelInfo so `rcli list` / `rcli run` + // download-factory grammar. Register ModelInfo so `wally list` / `wally run` // resolve the id; the tree is fetched by the engine or passed as a local path. v1::ModelInfo model; model.set_id(entry.id); @@ -2166,4 +2166,4 @@ rac_result_t register_all() { return first_error; } -} // namespace rcli::catalog +} // namespace wally::catalog diff --git a/src/catalog/catalog.h b/src/catalog/catalog.h index 115086d..1e2ae9e 100644 --- a/src/catalog/catalog.h +++ b/src/catalog/catalog.h @@ -12,8 +12,8 @@ * re-register their catalogs on every launch the same way). */ -#ifndef RCLI_CATALOG_CATALOG_H -#define RCLI_CATALOG_CATALOG_H +#ifndef WALLY_CATALOG_CATALOG_H +#define WALLY_CATALOG_CATALOG_H #include #include @@ -23,7 +23,7 @@ #include "model_types.pb.h" #include "rac/core/rac_types.h" -namespace rcli::catalog { +namespace wally::catalog { struct CatalogFile { const char *url; @@ -65,6 +65,6 @@ std::vector suggestions(const std::string &input, size_t max); */ rac_result_t register_all(); -} // namespace rcli::catalog +} // namespace wally::catalog -#endif // RCLI_CATALOG_CATALOG_H +#endif // WALLY_CATALOG_CATALOG_H diff --git a/src/catalog/model_ref.cpp b/src/catalog/model_ref.cpp index 331bde1..9a9f9b4 100644 --- a/src/catalog/model_ref.cpp +++ b/src/catalog/model_ref.cpp @@ -16,7 +16,7 @@ #include "io/output.h" #include "io/proto.h" -namespace rcli::model_ref { +namespace wally::model_ref { namespace { @@ -46,7 +46,7 @@ bool is_local_path(const std::string &ref) { } // Only treat something as a path when it looks like one. A bare word is a // model id; requiring a separator (or a Windows drive prefix) keeps - // `rcli run qwen3` from probing the cwd and finding a stray directory. + // `wally run qwen3` from probing the cwd and finding a stray directory. const bool has_sep = ref.find('/') != std::string::npos || ref.find('\\') != std::string::npos; const bool win_drive = ref.size() >= 2 && @@ -78,7 +78,7 @@ std::string id_for_local_path(const std::string &path) { std::string full = without_trailing_slashes(path); // Canonicalize first so the same bundle spelled differently (relative, `..`, // a symlink) keeps ONE id; the raw path is the fallback when it cannot be - // resolved. `std::filesystem` rather than `realpath` because rcli builds on + // resolved. `std::filesystem` rather than `realpath` because wally builds on // Windows too and `realpath` is POSIX-only; `weakly_canonical` also tolerates // a path that does not fully exist instead of failing outright. std::error_code ec; @@ -164,7 +164,7 @@ void infer_local_kind(const std::string &path, } // QHexRT HNPU bundles: Hexagon arch folder (v75/v79/v81), a context.bin, or // a top-level non-aux .json next to QNN binaries. `_HNPU` is the published - // repo suffix; honor it so `rcli run --engine qhexrt ` is not required. + // repo suffix; honor it so `wally run --engine qhexrt ` is not required. const std::string leaf = p.filename().string(); auto looks_qnn = [&]() -> bool { if (leaf.find("_HNPU") != std::string::npos || leaf.find("-npu") != std::string::npos) { @@ -198,7 +198,7 @@ void infer_local_kind(const std::string &path, } // Register an already-present local bundle so the lifecycle loader can resolve -// it by id, with no download. Mirrors what `rcli image` has always done for a +// it by id, with no download. Mirrors what `wally image` has always done for a // local CoreML diffusion bundle (cmd_image.cpp::register_local_bundle) — this // is that capability moved down to the shared resolver, so every command that // takes a model ref gets it instead of just one. @@ -344,11 +344,18 @@ rac_result_t resolve(const std::string &ref, Resolved *out, std::string *error, } *error += "?"; } else { - *error += " (try `rcli list --all`, an hf.co/org/repo[:quant] ref, a " - "direct URL, or a path to a local bundle directory)"; + // No close local match: this is as likely a hosted console model id + // (glm-5.3-flash, ...) as a typo, and `wally run`/`llm generate` only + // ever loads a model on this machine — there is no cloud fallback here + // to dead-end into quietly. Point at the one that exists. + *error += " (try `wally list --all`, an hf.co/org/repo[:quant] ref, a " + "direct URL, or a path to a local bundle directory — or, if " + "it's a model your account has on the hosted console, `wally " + "claude-code -m " + + ref + "` / `wally opencode --cloud -m " + ref + "`)"; } } return RAC_ERROR_NOT_FOUND; } -} // namespace rcli::model_ref +} // namespace wally::model_ref diff --git a/src/catalog/model_ref.h b/src/catalog/model_ref.h index b418cb4..3c91bd4 100644 --- a/src/catalog/model_ref.h +++ b/src/catalog/model_ref.h @@ -15,15 +15,15 @@ * lives in commons — the CLI never guesses. */ -#ifndef RCLI_CATALOG_MODEL_REF_H -#define RCLI_CATALOG_MODEL_REF_H +#ifndef WALLY_CATALOG_MODEL_REF_H +#define WALLY_CATALOG_MODEL_REF_H #include #include "model_types.pb.h" #include "rac/core/rac_types.h" -namespace rcli::model_ref { +namespace wally::model_ref { struct Resolved { std::string model_id; // registry id to operate on @@ -48,6 +48,6 @@ struct ResolveOptions { rac_result_t resolve(const std::string &ref, Resolved *out, std::string *error, const ResolveOptions *options = nullptr); -} // namespace rcli::model_ref +} // namespace wally::model_ref -#endif // RCLI_CATALOG_MODEL_REF_H +#endif // WALLY_CATALOG_MODEL_REF_H diff --git a/src/cli_formatter.cpp b/src/cli_formatter.cpp new file mode 100644 index 0000000..7494e06 --- /dev/null +++ b/src/cli_formatter.cpp @@ -0,0 +1,235 @@ +#include "cli_formatter.h" + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace wally { + +namespace cli_color { + +namespace { +constexpr const char* kBoldCode = "\033[1m"; +constexpr const char* kBoldCyanCode = "\033[1;36m"; +constexpr const char* kResetCode = "\033[0m"; +} // namespace + +Palette make_palette(bool enabled) { + if (!enabled) return Palette{}; + return Palette{kBoldCode, kBoldCyanCode, kResetCode}; +} + +} // namespace cli_color + +namespace { + +std::string colorize(const std::string& text, const char* code, bool enabled) { + if (!enabled || text.empty()) return text; + return std::string(code) + text + cli_color::kResetCode; +} + +} // namespace + +bool color_output_enabled(bool no_color_flag) { + if (no_color_flag) return false; + if (std::getenv("NO_COLOR") != nullptr) return false; +#ifdef _WIN32 + return _isatty(_fileno(stdout)) != 0; +#else + return isatty(fileno(stdout)) != 0; +#endif +} + +CliFormatter::CliFormatter(bool color_enabled) : color_enabled_(color_enabled) {} + +std::string CliFormatter::make_group(std::string group, bool is_positional, + std::vector opts) const { + std::stringstream out; + out << "\n" << colorize(group, cli_color::kBoldCode, color_enabled_) << ":\n"; + for (const CLI::Option* opt : opts) { + out << make_option(opt, is_positional); + } + return out.str(); +} + +std::string CliFormatter::make_subcommands(const CLI::App* app, CLI::AppFormatMode mode) const { + std::stringstream out; + std::vector subcommands = app->get_subcommands({}); + + // Make a list in definition order of the groups seen (mirrors + // CLI::Formatter::make_subcommands -- only the heading gets color here). + std::vector subcmd_groups_seen; + for (const CLI::App* com : subcommands) { + if (com->get_name().empty()) { + if (!com->get_group().empty() && com->get_group().front() != '+') { + out << make_expanded(com, mode); + } + continue; + } + std::string group_key = com->get_group(); + if (!group_key.empty() && + std::find_if(subcmd_groups_seen.begin(), subcmd_groups_seen.end(), + [&group_key](const std::string& a) { + return CLI::detail::to_lower(a) == CLI::detail::to_lower(group_key); + }) == subcmd_groups_seen.end()) { + subcmd_groups_seen.push_back(group_key); + } + } + + for (const std::string& group : subcmd_groups_seen) { + out << '\n' << colorize(group, cli_color::kBoldCode, color_enabled_) << ":\n"; + std::vector subcommands_group = app->get_subcommands([&group](const CLI::App* sub_app) { + return CLI::detail::to_lower(sub_app->get_group()) == CLI::detail::to_lower(group); + }); + for (const CLI::App* new_com : subcommands_group) { + if (new_com->get_name().empty()) continue; + if (mode != CLI::AppFormatMode::All) { + out << make_subcommand(new_com); + } else { + out << new_com->help(new_com->get_name(), CLI::AppFormatMode::Sub); + out << '\n'; + } + } + } + + return out.str(); +} + +std::string CliFormatter::make_subcommand(const CLI::App* sub) const { + std::stringstream out; + const std::string suffix = sub->get_required() ? " " + get_label("REQUIRED") : ""; + const std::string plain_name = " " + sub->get_display_name(true) + suffix; + + out << colorize(" " + sub->get_display_name(true), cli_color::kBoldCyanCode, color_enabled_) << suffix; + if (plain_name.length() < get_column_width()) { + out << std::string(get_column_width() - plain_name.length(), ' '); + } + CLI::detail::streamOutAsParagraph(out, sub->get_description(), get_right_column_width(), + std::string(get_column_width(), ' '), true); + out << '\n'; + return out.str(); +} + +std::string CliFormatter::make_option(const CLI::Option* opt, bool is_positional) const { + std::stringstream out; + const std::size_t column_width = get_column_width(); + + if (is_positional) { + const std::string plain_left = " " + make_option_name(opt, true) + make_option_opts(opt); + const std::string desc = make_option_desc(opt); + + out << colorize(" " + make_option_name(opt, true), cli_color::kBoldCyanCode, color_enabled_) << make_option_opts(opt); + if (plain_left.length() < column_width) { + out << std::string(column_width - plain_left.length(), ' '); + } + + if (!desc.empty()) { + bool skip_first_line_prefix = true; + if (plain_left.length() >= column_width) { + out << '\n'; + skip_first_line_prefix = false; + } + CLI::detail::streamOutAsParagraph(out, desc, get_right_column_width(), std::string(column_width, ' '), + skip_first_line_prefix); + } + out << '\n'; + return out.str(); + } + + // Non-positional: same short-name / long-name column split as + // CLI::Formatter::make_option, reproduced here because coloring the name + // and padding it with std::setw don't mix -- setw counts the ANSI escape + // bytes as visible characters and under-pads the description column. + // `visible_length` tracks what setw would have measured on the plain + // (uncolored) text so the layout stays identical either way. + const std::string names_combined = make_option_name(opt, false); + const std::string opts_text = make_option_opts(opt); + const std::string desc = make_option_desc(opt); + + const auto names = CLI::detail::split(names_combined, ','); + std::vector short_names_v; + std::vector long_names_v; + std::for_each(names.begin(), names.end(), [&short_names_v, &long_names_v](const std::string& name) { + if (name.find("--", 0) != std::string::npos) + long_names_v.push_back(name); + else + short_names_v.push_back(name); + }); + + const std::string short_names = CLI::detail::join(short_names_v, ", "); + const std::string long_names = CLI::detail::join(long_names_v, ", "); + + const auto short_column_width = static_cast(column_width / 3); + const auto long_column_width = + static_cast(std::ceil(static_cast(column_width) / 3.0f * 2.0f)); + int short_over_size = 0; + std::size_t visible_length = 0; + + if (!short_names.empty()) { + std::string plain_short = " " + short_names; + if (long_names.empty() && !opts_text.empty()) plain_short += opts_text; + if (!long_names.empty()) plain_short += ","; + if (static_cast(plain_short.length()) >= short_column_width) { + plain_short += " "; + short_over_size = static_cast(plain_short.length()) - short_column_width; + } + + const std::string colored_name = colorize(" " + short_names, cli_color::kBoldCyanCode, color_enabled_); + const std::string trailer = plain_short.substr(2 + short_names.length()); + out << colored_name << trailer; + visible_length += plain_short.length(); + if (static_cast(plain_short.length()) < short_column_width) { + const std::size_t pad = static_cast(short_column_width) - plain_short.length(); + out << std::string(pad, ' '); + visible_length += pad; + } + } else { + out << std::string(static_cast(short_column_width), ' '); + visible_length += static_cast(short_column_width); + } + + short_over_size = (std::min)(short_over_size, long_column_width); + const auto adjusted_long_width = long_column_width - short_over_size; + + if (!long_names.empty()) { + std::string plain_long = long_names; + if (!opts_text.empty()) plain_long += opts_text; + if (static_cast(plain_long.length()) >= adjusted_long_width) plain_long += " "; + + const std::string colored_name = colorize(long_names, cli_color::kBoldCyanCode, color_enabled_); + const std::string trailer = plain_long.substr(long_names.length()); + out << colored_name << trailer; + visible_length += plain_long.length(); + if (static_cast(plain_long.length()) < adjusted_long_width) { + const std::size_t pad = static_cast(adjusted_long_width) - plain_long.length(); + out << std::string(pad, ' '); + visible_length += pad; + } + } else { + out << std::string(static_cast(adjusted_long_width), ' '); + visible_length += static_cast(adjusted_long_width); + } + + if (!desc.empty()) { + bool skip_first_line_prefix = true; + if (visible_length > column_width) { + out << '\n'; + skip_first_line_prefix = false; + } + CLI::detail::streamOutAsParagraph(out, desc, get_right_column_width(), std::string(column_width, ' '), + skip_first_line_prefix); + } + + out << '\n'; + return out.str(); +} + +} // namespace wally diff --git a/src/cli_formatter.h b/src/cli_formatter.h new file mode 100644 index 0000000..1ea39b0 --- /dev/null +++ b/src/cli_formatter.h @@ -0,0 +1,62 @@ +/** + * @file cli_formatter.h + * @brief Minimal, tasteful color formatter for `--help` (bold headings, + * bold+cyan command/option names, plain descriptions) -- think `gh`/`ollama`. + */ + +#ifndef WALLY_CLI_FORMATTER_H +#define WALLY_CLI_FORMATTER_H + +#include +#include + +#include + +namespace wally { + +// True when ANSI color is safe to emit on stdout: not forced off by +// --no-color or NO_COLOR, and stdout is actually a terminal. Piped or +// redirected output (CI logs, `| cat`, a file) always gets plain text. +bool color_output_enabled(bool no_color_flag); + +// The same tasteful, minimal palette CliFormatter uses for `--help`, shared so +// other output (`wally about`) styles itself identically instead of picking +// its own ANSI codes. +namespace cli_color { + +// Every field is "" when color is disabled, so a caller can always splice +// these in (`pal.bold + text + pal.reset`) without an if/else at the call +// site -- pasting empty strings is a no-op. +struct Palette { + const char* bold = ""; + const char* bold_cyan = ""; + const char* reset = ""; +}; + +Palette make_palette(bool enabled); + +} // namespace cli_color + +// Overrides just enough of CLI::Formatter to color section headings and +// command/option names, computing column padding from the visible (plain) +// text rather than the ANSI-decorated one -- CLI11's own setw-based padding +// would otherwise miscount escape bytes as visible characters and misalign +// the description column. Everything else (usage, footer, description +// wrapping) is untouched. +class CliFormatter : public CLI::Formatter { + public: + explicit CliFormatter(bool color_enabled); + + CLI11_NODISCARD std::string make_group(std::string group, bool is_positional, + std::vector opts) const override; + std::string make_subcommands(const CLI::App* app, CLI::AppFormatMode mode) const override; + std::string make_subcommand(const CLI::App* sub) const override; + std::string make_option(const CLI::Option* opt, bool is_positional) const override; + + private: + bool color_enabled_; +}; + +} // namespace wally + +#endif // WALLY_CLI_FORMATTER_H diff --git a/src/commands/bench_metrics.h b/src/commands/bench_metrics.h index 919104c..ed11e78 100644 --- a/src/commands/bench_metrics.h +++ b/src/commands/bench_metrics.h @@ -6,15 +6,15 @@ * stay 0 / absent. Harness wall clocks (load/warmup/measured e2e) are separate. */ -#ifndef RCLI_COMMANDS_BENCH_METRICS_H -#define RCLI_COMMANDS_BENCH_METRICS_H +#ifndef WALLY_COMMANDS_BENCH_METRICS_H +#define WALLY_COMMANDS_BENCH_METRICS_H #include "llm_options.pb.h" #include "vlm_options.pb.h" #include -namespace rcli::commands::bench_metrics { +namespace wally::commands::bench_metrics { struct LlmVlmMetrics { double end_to_end_ms = 0.0; @@ -75,6 +75,6 @@ inline bool fill_vlm(const runanywhere::v1::VLMResult& r, double measured_e2e_ms return true; } -} // namespace rcli::commands::bench_metrics +} // namespace wally::commands::bench_metrics -#endif // RCLI_COMMANDS_BENCH_METRICS_H +#endif // WALLY_COMMANDS_BENCH_METRICS_H diff --git a/src/commands/cmd_about.cpp b/src/commands/cmd_about.cpp new file mode 100644 index 0000000..0cad46f --- /dev/null +++ b/src/commands/cmd_about.cpp @@ -0,0 +1,205 @@ +/** + * @file cmd_about.cpp + * @brief `wally about` — a styled, richer environment panel: product, system, + * runtime backends, paths, and the signed-in account. See `wally info` for + * the terse sibling this expands on. + */ + +#include "commands/commands.h" + +#include +#include +#include + +#include "rac/core/rac_core.h" +#include "rac/core/rac_platform_adapter.h" +#include "runanywhere/proto/schema_lock.h" + +#include "account/baked_endpoints.h" +#include "account/credentials.h" +#include "cli_formatter.h" +#include "config/cli_paths.h" +#include "device_info.h" +#include "io/output.h" + +#ifndef WALLY_VERSION +#define WALLY_VERSION "0.0.0-dev" +#endif + +namespace wally::commands { + +namespace { + +#if defined(__APPLE__) +constexpr const char* kPlatform = "macos"; +#elif defined(__linux__) +constexpr const char* kPlatform = "linux"; +#elif defined(_WIN32) +constexpr const char* kPlatform = "windows"; +#else +constexpr const char* kPlatform = "unknown"; +#endif + +constexpr std::size_t kLabelWidth = 10; + +// " label value", the label column padded to kLabelWidth. Built with +// plain string ops rather than a fixed snprintf buffer -- a path or an +// account email has no natural length cap, so nothing here should risk +// silently truncating one. The label itself is left uncolored, same +// restraint --help's descriptions get. +void row(const std::string& label, const std::string& value) { + std::string line = " " + label; + line += std::string(line.length() < 2 + kLabelWidth ? 2 + kLabelWidth - line.length() : 1, ' '); + out::result_line(line + value); +} + +void heading(const cli_color::Palette& pal, const std::string& title) { + out::result_line(""); + out::result_line(pal.bold + title + pal.reset + std::string(":")); +} + +} // namespace + +void register_about(CLI::App& app, GlobalOptions& options) { + CLI::App* cmd = + app.add_subcommand("about", "Detailed product, system and runtime report"); + cmd->callback([&options]() { + Bootstrapped env; + if (bootstrap(options, &env) != RAC_SUCCESS) { + throw CLI::RuntimeError(1); + } + + const rac_version_t commons = rac_get_version(); + const std::string commons_version = commons.string ? commons.string : "unknown"; + + const DeviceSnapshot device = collect_device_snapshot(); + + rac_memory_info_t memory{}; + bool memory_ok = false; + if (const rac_platform_adapter_t* adapter = rac_get_platform_adapter()) { + memory_ok = adapter->get_memory_info && + adapter->get_memory_info(&memory, adapter->user_data) == RAC_SUCCESS; + } + + const std::map engines = collect_backend_rows(); + + // Which bottle this binary is: WALLY_BAKED_CONSOLE_API_URL is compiled + // in empty for a production build and non-empty for a dev one (see + // baked_endpoints.h.in) -- the same compile-time fact + // DefaultConsoleUrl() itself keys on. console_url is the *effective* + // target though, since an env override still wins over the bake. + const bool dev_channel = WALLY_BAKED_CONSOLE_API_URL[0] != '\0'; + const char* channel = dev_channel ? "development" : "production"; + const std::string console_url = account::DefaultConsoleUrl(); + + account::Credentials credentials; + std::string credentials_error; + const bool signed_in = + account::Load(&credentials, &credentials_error) && credentials.signed_in(); + + if (options.json) { + out::JsonWriter json; + json.begin_object() + .field("wally", WALLY_VERSION) + .field("commons", commons_version) + .field("idl_version", RUNANYWHERE_IDL_VERSION) + .field("idl_schema_sha256", RUNANYWHERE_IDL_SCHEMA_SHA256) + .field("platform", kPlatform) + .field("channel", channel) + .field("console", console_url) + .field("os", device.os_version) + .field("cpu", device.chip) + .field("core_count", static_cast(device.core_count)) + .field("performance_cores", static_cast(device.performance_cores)) + .field("efficiency_cores", static_cast(device.efficiency_cores)) + .field("architecture", device.architecture); + if (memory_ok) { + json.field("memory_total_bytes", static_cast(memory.total_bytes)) + .field("memory_available_bytes", + static_cast(memory.available_bytes)); + } + + json.begin_array("backends"); + for (const auto& [name, engine] : engines) { + json.begin_array_object() + .field("name", name) + .field("display_name", engine.display_name) + .field("version", engine.version) + .field("priority", static_cast(engine.priority)); + json.begin_array("primitives"); + for (const auto& primitive : engine.primitives) { + json.value(primitive); + } + json.end_array().end_object(); + } + json.end_array(); + + json.field("home", env.home) + .field("models_dir", env.models_dir) + .field("state_dir", paths::state_dir()) + .field("account_signed_in", signed_in); + if (signed_in) { + json.field("account_email", credentials.email) + .field("account_console", credentials.console_url); + } + json.end_object(); + out::result_line(json.str()); + return; + } + + const cli_color::Palette pal = + cli_color::make_palette(color_output_enabled(options.no_color)); + + heading(pal, "Product"); + row("wally", WALLY_VERSION); + row("commons", commons_version); + row("idl", std::string(RUNANYWHERE_IDL_VERSION) + " sha256 " + + RUNANYWHERE_IDL_SCHEMA_SHA256); + row("platform", kPlatform); + row("channel", channel); + row("console", console_url); + + heading(pal, "System"); + row("os", device.os_version.empty() ? "unknown" : device.os_version); + row("cpu", (device.chip.empty() ? "unknown" : device.chip) + " (" + + std::to_string(device.core_count) + " cores)"); + row("arch", device.architecture.empty() ? "unknown" : device.architecture); + if (memory_ok) { + row("memory", out::human_bytes(memory.available_bytes) + " available of " + + out::human_bytes(memory.total_bytes)); + } + + heading(pal, "Runtime"); + if (engines.empty()) { + row("backends", "none registered"); + } else { + for (const auto& [name, engine] : engines) { + std::string primitives; + for (const auto& primitive : engine.primitives) { + primitives += primitives.empty() ? primitive : ", " + primitive; + } + std::string label = pal.bold_cyan + name + pal.reset; + // Padding is computed from the plain (uncolored) name length, + // same reasoning as CliFormatter -- see cli_formatter.cpp. + const std::size_t pad = + name.length() < kLabelWidth ? kLabelWidth - name.length() : 1; + out::result_line(" " + label + std::string(pad, ' ') + "priority " + + std::to_string(engine.priority) + " " + primitives); + } + } + + heading(pal, "Paths"); + row("home", env.home); + row("models", env.models_dir); + row("state", paths::state_dir()); + + if (signed_in) { + heading(pal, "Account"); + row("email", credentials.email); + row("console", credentials.console_url); + } + out::result_line(""); + }); +} + +} // namespace wally::commands diff --git a/src/commands/cmd_account.cpp b/src/commands/cmd_account.cpp index 8424c15..c0c1799 100644 --- a/src/commands/cmd_account.cpp +++ b/src/commands/cmd_account.cpp @@ -16,12 +16,13 @@ #include #endif +#include "account/baked_endpoints.h" #include "account/console.h" #include "account/credentials.h" #include "commands/commands.h" #include "io/output.h" -namespace rcli::commands { +namespace wally::commands { namespace { void fail(int status) { @@ -40,7 +41,18 @@ void fail(int status) { /// passes the same rules — the path and request code stay exactly as sent. /// The console origin the operator declared, normalised, or empty if none. std::string ConsoleWebOrigin() { - const char* configured = std::getenv("RCLI_CONSOLE_WEB_URL"); + const char* configured = std::getenv("WALLY_CONSOLE_WEB_URL"); + if (configured == nullptr || *configured == '\0') { + // rcli-era override, still honored so it doesn't go silently unread + // after the wally rename. + configured = std::getenv("RCLI_CONSOLE_WEB_URL"); + } + if (configured == nullptr || *configured == '\0') { + // A dev build carries its approval console compiled in (see + // baked_endpoints.h.in) — empty in production builds, and the env + // overrides above always win. + configured = WALLY_BAKED_CONSOLE_WEB_ORIGIN; + } std::string origin; if (configured == nullptr || *configured == '\0' || !account::NormalizeConsoleUrl(configured, &origin, nullptr)) { @@ -139,7 +151,7 @@ bool RefreshSession(const account::ConsoleClient& client, account::Credentials* std::string* error) { if (credentials->refresh_token.empty()) { if (error != nullptr) { - *error = "the cloud session cannot be refreshed; run `rcli login`"; + *error = "the cloud session cannot be refreshed; run `wally login`"; } return false; } @@ -256,13 +268,13 @@ int Logout() { return 0; } -int WhoAmI() { +int WhoAmI(bool as_json) { account::Credentials credentials; if (!LoadCredentials(&credentials)) { return 1; } if (!credentials.signed_in()) { - out::error_line("not signed in — run `rcli login`"); + out::error_line("not signed in — run `wally login`"); return 1; } @@ -290,6 +302,19 @@ int WhoAmI() { return 1; } + // whoami is identity only, by decision: plan, spend and token usage belong + // to `wally usage`, and an e2e guard (tests/test_account_cli.py) fails the + // build if any of them leak in here. The README is worded to match. + if (as_json) { + out::JsonWriter json; + json.begin_object() + .field("email", identity.email) + .field("session", "active") + .field("console", credentials.console_url) + .end_object(); + out::result_line(json.str()); + return 0; + } char line[220]; std::snprintf(line, sizeof(line), "%-14s %s", "email", identity.email.c_str()); out::result_line(line); @@ -303,7 +328,6 @@ int WhoAmI() { } // namespace void register_account(CLI::App& app, GlobalOptions& options) { - static_cast(options); auto no_browser = std::make_shared(false); auto console_url = std::make_shared(); auto* login = app.add_subcommand("login", "sign in through the RunAnywhere console"); @@ -311,14 +335,18 @@ void register_account(CLI::App& app, GlobalOptions& options) { login ->add_option("--console-url", *console_url, "console API origin (default: " + account::DefaultConsoleUrl() + ")") - ->envname("RCLI_CONSOLE_URL"); + ->envname("WALLY_CONSOLE_URL"); login->callback([no_browser, console_url] { fail(Login(*console_url, !*no_browser)); }); auto* logout = app.add_subcommand("logout", "revoke and remove the cloud session"); logout->callback([] { fail(Logout()); }); + auto whoami_json = std::make_shared(false); auto* whoami = app.add_subcommand("whoami", "show the signed-in cloud account"); - whoami->callback([] { fail(WhoAmI()); }); + whoami->add_flag("--json", *whoami_json, "machine-readable output"); + // `wally --json whoami` and `wally whoami --json` mean the same thing; see + // the identical fix in register_usage (cmd_usage.cpp). + whoami->callback([whoami_json, &options] { fail(WhoAmI(*whoami_json || options.json)); }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_auth.cpp b/src/commands/cmd_auth.cpp index a0ba105..6c01a26 100644 --- a/src/commands/cmd_auth.cpp +++ b/src/commands/cmd_auth.cpp @@ -1,6 +1,6 @@ /** * @file cmd_auth.cpp - * @brief `rcli auth login` — real control-plane handshake. + * @brief `wally auth login` — real control-plane handshake. * * Runs the canonical staging/production auth sequence against the configured * backend (--base-url/--api-key/--environment or their RUNANYWHERE_* env @@ -19,7 +19,7 @@ #include "io/output.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -120,4 +120,4 @@ void register_auth(CLI::App& app, GlobalOptions& options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_backends.cpp b/src/commands/cmd_backends.cpp index b46cace..d7d87f9 100644 --- a/src/commands/cmd_backends.cpp +++ b/src/commands/cmd_backends.cpp @@ -1,6 +1,6 @@ /** * @file cmd_backends.cpp - * @brief `rcli backends` — registered engine plugins per primitive. + * @brief `wally backends` — registered engine plugins per primitive. */ #include "commands/commands.h" @@ -16,21 +16,37 @@ #include "io/output.h" -namespace rcli::commands { - -namespace { +namespace wally::commands { // Walk every live primitive. ONNX without RAG only advertises SEGMENT / // DIARIZE — omitting those made a registered onnx backend invisible. - -struct EngineRow { - std::string display_name; - std::string version; - int32_t priority = 0; - std::set primitives; -}; - -} // namespace +std::map collect_backend_rows() { + std::map engines; + for (int raw = 1; raw < static_cast(RAC_PRIMITIVE_COUNT); ++raw) { + if (raw == 6) { + continue; // retired RERANK wire value + } + const rac_primitive_t primitive = static_cast(raw); + const rac_engine_vtable_t* plugins[16] = {}; + size_t count = 0; + if (rac_plugin_list(primitive, plugins, 16, &count) != RAC_SUCCESS) { + continue; + } + for (size_t i = 0; i < count; ++i) { + const rac_engine_metadata_t& meta = plugins[i]->metadata; + EngineRow& row = engines[meta.name ? meta.name : "?"]; + if (meta.display_name) { + row.display_name = meta.display_name; + } + if (meta.engine_version) { + row.version = meta.engine_version; + } + row.priority = meta.priority; + row.primitives.insert(rac_primitive_name(primitive)); + } + } + return engines; +} void register_backends(CLI::App& app, GlobalOptions& options) { CLI::App* cmd = app.add_subcommand("backends", "List registered inference backends"); @@ -40,30 +56,7 @@ void register_backends(CLI::App& app, GlobalOptions& options) { throw CLI::RuntimeError(1); } - std::map engines; - for (int raw = 1; raw < static_cast(RAC_PRIMITIVE_COUNT); ++raw) { - if (raw == 6) { - continue; // retired RERANK wire value - } - const rac_primitive_t primitive = static_cast(raw); - const rac_engine_vtable_t* plugins[16] = {}; - size_t count = 0; - if (rac_plugin_list(primitive, plugins, 16, &count) != RAC_SUCCESS) { - continue; - } - for (size_t i = 0; i < count; ++i) { - const rac_engine_metadata_t& meta = plugins[i]->metadata; - EngineRow& row = engines[meta.name ? meta.name : "?"]; - if (meta.display_name) { - row.display_name = meta.display_name; - } - if (meta.engine_version) { - row.version = meta.engine_version; - } - row.priority = meta.priority; - row.primitives.insert(rac_primitive_name(primitive)); - } - } + std::map engines = collect_backend_rows(); if (options.json) { out::JsonWriter json; @@ -101,4 +94,4 @@ void register_backends(CLI::App& app, GlobalOptions& options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_bench.cpp b/src/commands/cmd_bench.cpp index eaf34c4..544b76f 100644 --- a/src/commands/cmd_bench.cpp +++ b/src/commands/cmd_bench.cpp @@ -1,6 +1,6 @@ /** * @file cmd_bench.cpp - * @brief `rcli bench [model]` — auto-benchmark installed models, like the + * @brief `wally bench [model]` — auto-benchmark installed models, like the * Android app's benchmark screen. * * With no model argument it enumerates every downloaded, non-built-in model @@ -29,10 +29,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include "catalog/model_ref.h" @@ -50,7 +52,7 @@ #include "rac/features/vlm/rac_vlm_service.h" #include "rac/infrastructure/model_management/rac_model_registry.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -650,7 +652,7 @@ int run_bench(const GlobalOptions& options, const std::string& model_ref_arg, in // bundle directory, an HF ref or a URL all work here too. collect_models // only ever scans the registry, so without this an unregistered ref — which // is what a freshly staged bundle on disk is — reported "not a downloaded - // benchmarkable model" even though `rcli run` could load it fine. + // benchmarkable model" even though `wally run` could load it fine. std::string only_model = model_ref_arg; if (!model_ref_arg.empty()) { model_ref::Resolved resolved; @@ -671,14 +673,36 @@ int run_bench(const GlobalOptions& options, const std::string& model_ref_arg, in } if (models.empty()) { out::error_line(only_model.empty() - ? "no downloaded models to benchmark (pull one with `rcli pull`)" + ? "no downloaded models to benchmark (pull one with `wally pull`)" : "model '" + only_model + "' is not a downloaded benchmarkable model"); return 1; } + // `--vlm-image`'s default is a path inside the wally source tree + // (docs/gifs/...), so it silently doesn't exist for anyone benchmarking an + // installed binary from any other cwd. Checked once, outside the loop: the + // llama.cpp load failure it otherwise causes reports "Input is invalid" + // with the real cause buried in the engine's own stderr lines above it. + std::error_code vlm_image_ec; + const bool vlm_image_exists = std::filesystem::exists(vlm_image, vlm_image_ec); + std::vector rows; for (const BenchModel& model : models) { for (const Scenario& scenario : scenarios_for(model.modality)) { + if (model.modality == Modality::kVlm && !vlm_image_exists) { + BenchRow row; + row.model_id = model.id; + row.modality = model.modality; + row.scenario = scenario.label; + row.trials = trials; + row.error = "VLM sample image not found: '" + vlm_image + + "' (pass --vlm-image ; the built-in default only " + "resolves from inside the wally source tree)"; + out::status_line(std::string("skipping ") + modality_label(model.modality) + " " + + model.id + " — " + scenario.label + ": " + row.error); + rows.push_back(row); + continue; + } out::status_line(std::string("benchmarking ") + modality_label(model.modality) + " " + model.id + " — " + scenario.label + " (" + std::to_string(trials) + " trials)"); @@ -783,4 +807,4 @@ void register_bench(CLI::App& app, GlobalOptions& options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_diarize.cpp b/src/commands/cmd_diarize.cpp index f81264b..23f9beb 100644 --- a/src/commands/cmd_diarize.cpp +++ b/src/commands/cmd_diarize.cpp @@ -1,6 +1,6 @@ /** * @file cmd_diarize.cpp - * @brief `rcli diarize --model ` — offline speaker + * @brief `wally diarize --model ` — offline speaker * diarization via the commons diarization service (audio-in → typed * speaker segments out). * @@ -28,7 +28,7 @@ #include "rac/features/diarization/rac_diarization_service.h" #include "rac/features/diarization/rac_diarization_types.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -180,4 +180,4 @@ void register_diarize(CLI::App& app, GlobalOptions& options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_editors.cpp b/src/commands/cmd_editors.cpp index eff3164..cdecb82 100644 --- a/src/commands/cmd_editors.cpp +++ b/src/commands/cmd_editors.cpp @@ -14,7 +14,7 @@ #include "ide/jetbrains_profile.h" #include "ide/openai_proxy.h" -namespace rcli::commands { +namespace wally::commands { namespace { /// CLI11 callbacks return void, so a non-zero status leaves as the runtime @@ -26,7 +26,7 @@ void fail(int status) { } -/// One editor or agent rcli can point at a model. +/// One editor or agent wally can point at a model. /// /// The list is the whole integration surface: a new target is a row here plus /// whatever `apply` has to set. Everything before that — resolving the model, @@ -45,7 +45,7 @@ enum class Wiring { }; struct Editor { - /// What the reader types after `rcli`. + /// What the reader types after `wally`. const char* id; /// An executable on PATH, or empty when this is a desktop app. const char* command; @@ -61,11 +61,11 @@ constexpr ide::Product kCLion{"clion", "CLion.app", "clion", "CLion"}; constexpr ide::Product kRustRover{"rustrover", "RustRover.app", "rustrover", "RustRover"}; /// Only tools that speak the Anthropic Messages API belong here. Anything -/// OpenAI-shaped needs no translator and goes through `rcli opencode`. +/// OpenAI-shaped needs no translator and goes through `wally opencode`. /// /// Claude Desktop earns its place because it forwards a fixed set of variables /// to the Claude Code it runs inside itself, and ANTHROPIC_BASE_URL is one of -/// them. That is the same trick as `rcli claude-code`, one process further out. +/// them. That is the same trick as `wally claude-code`, one process further out. constexpr Editor kEditors[] = { {"claude-code", "claude", "", "open Claude Code against a model", Wiring::Environment, nullptr}, @@ -194,7 +194,7 @@ class ScopedEnv { /// Starts the translator and holds it open, printing what to point at it. /// /// Worth having beyond debugging: it is how anything that speaks the Anthropic -/// API but is not on the list above gets wired up, without rcli needing to know +/// API but is not on the list above gets wired up, without wally needing to know /// that tool exists. int Serve(const std::string& model, bool verbose) { harness::Endpoint endpoint; @@ -257,7 +257,7 @@ int Run(const Editor& editor, const std::string& model, if (model.empty()) { // No model named means no wiring to do, so the tool runs exactly as the - // reader has it configured. Same contract as `rcli opencode`. + // reader has it configured. Same contract as `wally opencode`. return is_bundle ? harness::Launch("open", {}, OpenArgs(bundle, {}, args)) : harness::Launch(editor.command, {}, args); } @@ -287,7 +287,11 @@ int Run(const Editor& editor, const std::string& model, } std::string failure; - if (!ide::ApplyProvider(*editor.jetbrains, reachable, std::string(), model, &failure)) { + // The proxy holds a per-session secret and rejects a chat request that + // does not present it, so the IDE is given it as the provider key. A + // direct (keyless) endpoint leaves proxy.auth_token empty, which is the + // right value there too. + if (!ide::ApplyProvider(*editor.jetbrains, reachable, proxy.auth_token, model, &failure)) { out::error_line(failure); ide::StopProxy(&proxy); harness::Release(endpoint); @@ -413,4 +417,4 @@ void register_editors(CLI::App& app, GlobalOptions& options) { } } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_embed.cpp b/src/commands/cmd_embed.cpp index 7bdfbce..456f0d4 100644 --- a/src/commands/cmd_embed.cpp +++ b/src/commands/cmd_embed.cpp @@ -1,6 +1,6 @@ /** * @file cmd_embed.cpp - * @brief `rcli embed [input]` — text embeddings via the commons lifecycle path. + * @brief `wally embed [input]` — text embeddings via the commons lifecycle path. */ #include "commands/commands.h" @@ -24,7 +24,7 @@ #include "io/proto.h" #include "progress/progress_bar.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -305,4 +305,4 @@ void register_embed(CLI::App& app, GlobalOptions& options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_harness.cpp b/src/commands/cmd_harness.cpp index 968c17f..0013b91 100644 --- a/src/commands/cmd_harness.cpp +++ b/src/commands/cmd_harness.cpp @@ -8,7 +8,7 @@ #include "harness/harness.h" #include "harness/opencode.h" -namespace rcli::commands { +namespace wally::commands { namespace { /// CLI11 callbacks return void, so a non-zero status leaves as the runtime @@ -22,7 +22,7 @@ void fail(int status) { void register_harness(CLI::App& app, GlobalOptions& options) { static_cast(options); - // `rcli opencode ` rather than a flag on `run`: it hands the terminal + // `wally opencode ` rather than a flag on `run`: it hands the terminal // to another program, which is a different thing to do than talk to a model. auto model = std::make_shared(); auto rest = std::make_shared>(); @@ -30,7 +30,7 @@ void register_harness(CLI::App& app, GlobalOptions& options) { auto* opencode = app.add_subcommand("opencode", "open a coding session in opencode, wired to a model"); // A named option rather than a positional: with two positionals there is no - // way to tell `rcli opencode run` asking for passthrough from someone + // way to tell `wally opencode run` asking for passthrough from someone // naming a model called run, and the first reading wins silently. opencode->add_option("-m,--model", *model, "a model on this machine, or one served upstream"); opencode->add_flag("--cloud", *cloud, @@ -50,4 +50,4 @@ void register_harness(CLI::App& app, GlobalOptions& options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_image.cpp b/src/commands/cmd_image.cpp index 11c97f7..90f10e1 100644 --- a/src/commands/cmd_image.cpp +++ b/src/commands/cmd_image.cpp @@ -1,6 +1,6 @@ /** * @file cmd_image.cpp - * @brief `rcli image generate` — text-to-image via NeuRT (Core ML diffusion). + * @brief `wally image generate` — text-to-image via NeuRT (Core ML diffusion). * * Canonical SDK flow, all heavy lifting in commons (mirrors cmd_run/cmd_embed): * rac_model_lifecycle_load_proto(category=IMAGE_GENERATION, validate=true) @@ -24,7 +24,7 @@ #include "io/output.h" -#if defined(RCLI_HAS_NEURT) +#if defined(WALLY_HAS_NEURT) #include #include #include @@ -42,7 +42,7 @@ #include "progress/progress_bar.h" #endif -namespace rcli::commands { +namespace wally::commands { namespace { @@ -61,7 +61,7 @@ struct ImageParams { int64_t seed = -1; // -1 = random }; -#if defined(RCLI_HAS_NEURT) +#if defined(WALLY_HAS_NEURT) namespace v1 = runanywhere::v1; @@ -172,7 +172,7 @@ bool write_image(const v1::DiffusionImage& image_result, const std::string& out_ return true; } -#endif // RCLI_HAS_NEURT +#endif // WALLY_HAS_NEURT int run_image_generate(const GlobalOptions& options, const ImageParams& params) { Bootstrapped env; @@ -180,7 +180,7 @@ int run_image_generate(const GlobalOptions& options, const ImageParams& params) return 1; } -#if !defined(RCLI_HAS_NEURT) +#if !defined(WALLY_HAS_NEURT) (void)params; out::error_line("image generation (diffusion) requires NeuRT (Apple Neural Engine)"); return 1; @@ -318,4 +318,4 @@ void register_image(CLI::App& app, GlobalOptions& options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_info.cpp b/src/commands/cmd_info.cpp index f423eda..8e8ebfe 100644 --- a/src/commands/cmd_info.cpp +++ b/src/commands/cmd_info.cpp @@ -1,6 +1,6 @@ /** * @file cmd_info.cpp - * @brief `rcli info` — environment summary (versions, paths, memory, plugins). + * @brief `wally info` — environment summary (versions, paths, memory, plugins). */ #include "commands/commands.h" @@ -14,11 +14,11 @@ #include "config/cli_paths.h" #include "io/output.h" -#ifndef RCLI_VERSION -#define RCLI_VERSION "0.0.0-dev" +#ifndef WALLY_VERSION +#define WALLY_VERSION "0.0.0-dev" #endif -namespace rcli::commands { +namespace wally::commands { void register_info(CLI::App& app, GlobalOptions& options) { CLI::App* cmd = app.add_subcommand("info", "Report versions, paths, memory and backends"); @@ -52,7 +52,7 @@ void register_info(CLI::App& app, GlobalOptions& options) { if (options.json) { out::JsonWriter json; json.begin_object() - .field("rcli", RCLI_VERSION) + .field("wally", WALLY_VERSION) .field("commons", commons_version) .field("platform", platform) .field("home", env.home) @@ -69,7 +69,7 @@ void register_info(CLI::App& app, GlobalOptions& options) { return; } - out::result_line("rcli " RCLI_VERSION); + out::result_line("wally " WALLY_VERSION); out::result_line("commons " + commons_version); out::result_line("platform " + std::string(platform)); out::result_line("home " + env.home); @@ -82,4 +82,4 @@ void register_info(CLI::App& app, GlobalOptions& options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_list.cpp b/src/commands/cmd_list.cpp index 727d282..e019900 100644 --- a/src/commands/cmd_list.cpp +++ b/src/commands/cmd_list.cpp @@ -1,6 +1,6 @@ /** * @file cmd_list.cpp - * @brief `rcli models list` (alias `rcli list`) — downloaded models by + * @brief `wally models list` (alias `wally list`) — downloaded models by * default, the whole catalog with --all. * * The registry is refreshed with rescan_local so on-disk artifacts pulled by @@ -24,7 +24,7 @@ #include "io/output.h" #include "io/proto.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -108,8 +108,8 @@ int run_list(const GlobalOptions& options, bool show_all) { if (rows.empty()) { out::result_line(show_all ? "no models registered" - : "no models downloaded — try `rcli list --all` then " - "`rcli pull `"); + : "no models downloaded — try `wally list --all` then " + "`wally pull `"); return 0; } out::table({"ID", "MODALITY", "BACKEND", "SIZE", "DOWNLOADED"}, rows); @@ -129,4 +129,4 @@ void configure_models_list(CLI::App* cmd, GlobalOptions& options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_lora.cpp b/src/commands/cmd_lora.cpp index 0aaffc4..e8472f7 100644 --- a/src/commands/cmd_lora.cpp +++ b/src/commands/cmd_lora.cpp @@ -1,6 +1,6 @@ /** * @file cmd_lora.cpp - * @brief `rcli lora apply|remove|list|catalog` — LoRA adapters on the loaded + * @brief `wally lora apply|remove|list|catalog` — LoRA adapters on the loaded * LLM. * * `list` reports the adapters currently attached (rac_lora_state_proto) — the @@ -30,7 +30,7 @@ #include "io/output.h" #include "io/proto.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -346,4 +346,4 @@ void register_lora(CLI::App &app, GlobalOptions &options) { // package's own rule: never wire a flag/verb the C ABI cannot serve. } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_models.cpp b/src/commands/cmd_models.cpp index 7f9ceb1..851a76b 100644 --- a/src/commands/cmd_models.cpp +++ b/src/commands/cmd_models.cpp @@ -1,6 +1,6 @@ /** * @file cmd_models.cpp - * @brief `rcli models …` — the model-lifecycle namespace from the public API + * @brief `wally models …` — the model-lifecycle namespace from the public API * spec (list, get, register, download, delete, load, unload, state). * * list / get / download / delete reuse the configure_* functions owned by @@ -34,7 +34,7 @@ #include "io/proto.h" #include "progress/progress_bar.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -412,7 +412,7 @@ void register_models(CLI::App& app, GlobalOptions& options) { void register_models_aliases(CLI::App& app, GlobalOptions& options) { // CLI11's help banner always names the subcommand's primary registered // name, never the alias actually typed (App::get_display_name() ignores - // it) — so with "list" primary, `rcli ls --help` rendered "Usage: rcli + // it) — so with "list" primary, `wally ls --help` rendered "Usage: wally // list [OPTIONS]". Registering the shorter name as primary matches `rm` // below, which already makes the same call for the same reason. CLI::App* list = app.add_subcommand("ls", "List models (alias of `models list`)"); @@ -429,4 +429,4 @@ void register_models_aliases(CLI::App& app, GlobalOptions& options) { configure_models_delete(remove, options); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_pull.cpp b/src/commands/cmd_pull.cpp index cbc0431..8e66afe 100644 --- a/src/commands/cmd_pull.cpp +++ b/src/commands/cmd_pull.cpp @@ -1,6 +1,6 @@ /** * @file cmd_pull.cpp - * @brief `rcli models download ` (alias `rcli pull`) — + * @brief `wally models download ` (alias `wally pull`) — * download via the commons orchestrator: plan → start → progress * callback → terminal state. * @@ -30,7 +30,7 @@ #include "io/proto.h" #include "progress/progress_bar.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -86,7 +86,7 @@ int pull_model_flow(const GlobalOptions &options, const std::string &model_id) { // bootstrap() registers the catalog; it does not rescan what is on disk. So // registry_status() below can still read DOWNLOADED for a model whose files - // were deleted since, and `rcli pull` would report success without fetching + // were deleted since, and `wally pull` would report success without fetching // anything. A refresh failure is not fatal here: the download path that // follows is the fallback, and refusing to pull because a rescan failed would // be worse than pulling something already present. @@ -324,4 +324,4 @@ void configure_models_download(CLI::App *cmd, GlobalOptions &options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_rag.cpp b/src/commands/cmd_rag.cpp index 55f42fd..01f6d45 100644 --- a/src/commands/cmd_rag.cpp +++ b/src/commands/cmd_rag.cpp @@ -1,6 +1,6 @@ /** * @file cmd_rag.cpp - * @brief `rcli rag query` / `rcli rag search` — retrieval-augmented generation + * @brief `wally rag query` / `wally rag search` — retrieval-augmented generation * via the commons RAG session ABI. * * Single-shot flow in one process (the CLI is stateless across invocations and @@ -23,14 +23,14 @@ // The RAG pipeline is not folded into this binary (RAC_BACKEND_RAG=OFF, e.g. the // Windows CLI preset), so the rac_rag_*_proto symbols are unavailable. Register // no `rag` subcommand rather than fail to link. -namespace rcli::commands { +namespace wally::commands { void register_rag(CLI::App& app, GlobalOptions& options) { (void)app; (void)options; } -} // namespace rcli::commands +} // namespace wally::commands #else @@ -47,7 +47,7 @@ void register_rag(CLI::App& app, GlobalOptions& options) { #include "io/output.h" #include "io/proto.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -108,7 +108,7 @@ bool collect_documents(const RagParams& params, std::vector* docume bool open_and_ingest(const GlobalOptions& options, const RagParams& params, const std::vector& documents, rac_handle_t* session) { // Models must already be downloaded — the session resolves them from the - // registry. (Pull them first with `rcli models download `.) + // registry. (Pull them first with `wally models download `.) v1::RAGConfiguration config; config.set_embedding_model_id(params.embed_model); if (params.require_llm || !params.llm_model.empty()) { @@ -417,6 +417,6 @@ void register_rag(CLI::App& app, GlobalOptions& options) { }); } -} // namespace rcli::commands +} // namespace wally::commands #endif // RAC_HAVE_RAG diff --git a/src/commands/cmd_rerank.cpp b/src/commands/cmd_rerank.cpp index 194a0ea..4cb4254 100644 --- a/src/commands/cmd_rerank.cpp +++ b/src/commands/cmd_rerank.cpp @@ -1,6 +1,6 @@ /** * @file cmd_rerank.cpp - * @brief `rcli rerank --doc … ` — cross-encoder relevance scoring. + * @brief `wally rerank --doc … ` — cross-encoder relevance scoring. * * Same component sequence the other model-backed commands use: * ensure_model_ready (resolve + auto-pull + resolve paths) @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -27,7 +28,7 @@ #include "io/output.h" #include "io/proto.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -167,7 +168,11 @@ void register_rerank(CLI::App& app, GlobalOptions& options) { cmd->add_option("--model,-m", *model, "Reranker model id or on-disk path")->required(); cmd->add_option("--doc,-d", *docs, "Document text to score; repeat for several"); cmd->add_option("--file,-f", *files, "Text file to score; repeat for several"); - cmd->add_option("--top-n", *top_n, "Return only this many best matches"); + cmd->add_option("--top-n", *top_n, "Return only this many best matches") + // 0 or negative used to reach run_rerank unrejected and fall through + // the `top_n > 0` guard there, silently returning every document + // instead of the usage error a nonsensical count should be. + ->check(CLI::Range(1, std::numeric_limits::max())); cmd->callback([&options, query, model, docs, files, top_n]() { const int exit_code = run_rerank(options, *model, *query, *docs, *files, *top_n); if (exit_code != 0) { @@ -176,4 +181,4 @@ void register_rerank(CLI::App& app, GlobalOptions& options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_rm.cpp b/src/commands/cmd_rm.cpp index ad5944a..485ba51 100644 --- a/src/commands/cmd_rm.cpp +++ b/src/commands/cmd_rm.cpp @@ -1,6 +1,6 @@ /** * @file cmd_rm.cpp - * @brief `rcli models delete ` (alias `rcli rm`) — delete downloaded + * @brief `wally models delete ` (alias `wally rm`) — delete downloaded * files + unregister. * * File deletion is CLI-owned (registry remove only unregisters, per the @@ -26,7 +26,7 @@ #include "io/proto.h" #include "util/term.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -185,4 +185,4 @@ void configure_models_delete(CLI::App *cmd, GlobalOptions &options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_run.cpp b/src/commands/cmd_run.cpp index 4154e32..f1c1c98 100644 --- a/src/commands/cmd_run.cpp +++ b/src/commands/cmd_run.cpp @@ -1,7 +1,7 @@ /** * @file cmd_run.cpp - * @brief `rcli llm generate|stream`, `rcli vlm generate`, and the terminal - * aliases `rcli run` / `rcli chat`. + * @brief `wally llm generate|stream`, `wally vlm generate`, and the terminal + * aliases `wally run` / `wally chat`. * * Canonical SDK flow, all heavy lifting in commons: * rac_model_lifecycle_load_proto(validate_availability=true) → auto-pulls @@ -16,7 +16,7 @@ * Ctrl-C: rac_llm_cancel_proto from the token callback thread. * * REPL turns are independent generations (no cross-turn memory yet — that - * needs a commons chat-session API; tracked in the rcli plan doc). + * needs a commons chat-session API; tracked in the wally plan doc). */ #include "commands/commands.h" @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -50,7 +51,7 @@ #include "repl/repl.h" #include "util/term.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -649,7 +650,7 @@ int run_llm(const GlobalOptions& options, LlmVerb verb, const std::string& promp std::string effective_prompt = prompt; if (effective_prompt.empty() && !term::stdin_is_tty()) { - // Piped stdin is the prompt: echo "..." | rcli llm generate -m qwen3 + // Piped stdin is the prompt: echo "..." | wally llm generate -m qwen3 effective_prompt = read_piped_prompt(); } @@ -662,7 +663,11 @@ int run_llm(const GlobalOptions& options, LlmVerb verb, const std::string& promp : stream_once(options, resolved.model_id, effective_prompt, params); } - if (verb == LlmVerb::Chat) { + // The REPL is interactive by nature; --json promises exactly one JSON + // document on stdout, which an interactive prompt loop can never keep. + // `wally run m "" --json` used to fall through into it anyway and exit 0 + // with nothing on stdout. + if (verb == LlmVerb::Chat && !options.json) { return run_repl(options, resolved.model_id, params); } out::error_line("no prompt given"); @@ -710,7 +715,12 @@ void add_generation_options(CLI::App* cmd, const std::shared_ptr& par cmd->add_option("--stop", params->stop_sequences, "Stop as soon as this text is produced (repeat for several)"); cmd->add_option("--max-output-tokens,--max-tokens", params->max_output_tokens, - "Cap the generated tokens (default 1024)"); + "Cap the generated tokens (default 1024)") + // Range, not PositiveNumber, for the message alone (mirrors + // cmd_bench.cpp's --trials): 0 or negative used to reach the engine + // as-is and read as "no cap" — full/whole-context output — instead of + // the usage error a nonsensical budget should be. + ->check(CLI::Range(1, std::numeric_limits::max())); cmd->add_option("--reasoning", params->reasoning, "Turn the model's thinking phase on or off (default on)") ->check(CLI::IsMember({"on", "off"})); @@ -784,4 +794,4 @@ void register_llm_aliases(CLI::App& app, GlobalOptions& options) { options, LlmVerb::Chat, ModelArg::Positional); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_segment.cpp b/src/commands/cmd_segment.cpp index 89e9f53..faca021 100644 --- a/src/commands/cmd_segment.cpp +++ b/src/commands/cmd_segment.cpp @@ -1,6 +1,6 @@ /** * @file cmd_segment.cpp - * @brief `rcli segment --model ` — segmentation via + * @brief `wally segment --model ` — segmentation via * the commons segmentation service (image-in → per-class mask summary). * * Mirrors cmd_image's structure (bootstrap → resolve model → one commons path → @@ -30,7 +30,7 @@ #include "rac/features/segmentation/rac_segmentation_service.h" #include "rac/features/segmentation/rac_segmentation_types.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -194,4 +194,4 @@ void register_segment(CLI::App& app, GlobalOptions& options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_serve.cpp b/src/commands/cmd_serve.cpp index e9ffbb1..72efe93 100644 --- a/src/commands/cmd_serve.cpp +++ b/src/commands/cmd_serve.cpp @@ -1,6 +1,6 @@ /** * @file cmd_serve.cpp - * @brief `rcli serve [model]` — OpenAI-compatible local HTTP server. + * @brief `wally serve [model]` — OpenAI-compatible local HTTP server. * * Wraps the existing commons rac_server (include/rac/server/rac_server.h — * same engine behind tools/runanywhere-server.cpp). Scope inherited from @@ -17,20 +17,20 @@ #include #include -#if defined(RCLI_HAS_SERVER) +#if defined(WALLY_HAS_SERVER) #include "rac/server/rac_server.h" #endif #include "commands/model_setup.h" #include "io/output.h" -namespace rcli::commands { +namespace wally::commands { namespace { constexpr const char* kDefaultServeModel = "qwen3-0.6b"; -#if defined(RCLI_HAS_SERVER) +#if defined(WALLY_HAS_SERVER) // Async-signal-safe shutdown: the handler only sets a flag; the main thread // polls and performs the actual stop. Calling rac_server_stop() from signal @@ -98,14 +98,14 @@ int run_serve(const GlobalOptions& options, const std::string& ref, const std::s return exit_code; } -#endif // RCLI_HAS_SERVER +#endif // WALLY_HAS_SERVER } // namespace void register_serve(CLI::App& app, GlobalOptions& options) { CLI::App* cmd = app.add_subcommand("serve", "Serve a model over an OpenAI-compatible HTTP API"); -#if defined(RCLI_HAS_SERVER) +#if defined(WALLY_HAS_SERVER) auto ref = std::make_shared(); auto host = std::make_shared("127.0.0.1"); auto port = std::make_shared(8080); @@ -131,10 +131,10 @@ void register_serve(CLI::App& app, GlobalOptions& options) { }); #else cmd->callback([]() { - out::error_line("this rcli build does not include the server (RAC_BUILD_SERVER=OFF)"); + out::error_line("this wally build does not include the server (RAC_BUILD_SERVER=OFF)"); throw CLI::RuntimeError(1); }); #endif } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_show.cpp b/src/commands/cmd_show.cpp index e0826c8..fee3ffe 100644 --- a/src/commands/cmd_show.cpp +++ b/src/commands/cmd_show.cpp @@ -1,6 +1,6 @@ /** * @file cmd_show.cpp - * @brief `rcli models get ` (alias `rcli show`) — registry entry + * @brief `wally models get ` (alias `wally show`) — registry entry * details. */ @@ -19,7 +19,7 @@ #include "io/output.h" #include "io/proto.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -135,4 +135,4 @@ void configure_models_get(CLI::App *cmd, GlobalOptions &options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_stt.cpp b/src/commands/cmd_stt.cpp index 1a13e2e..73ae3a3 100644 --- a/src/commands/cmd_stt.cpp +++ b/src/commands/cmd_stt.cpp @@ -1,9 +1,9 @@ /** * @file cmd_stt.cpp - * @brief `rcli stt transcribe ` — file transcription via the STT + * @brief `wally stt transcribe ` — file transcription via the STT * component (same call sequence as the commons real-inference tests). * - * `rcli stt --input a.wav` is the same command: the options live on the `stt` + * `wally stt --input a.wav` is the same command: the options live on the `stt` * namespace and `transcribe` is a CLI11 fallthrough alias, so both spellings * reach one callback. */ @@ -20,7 +20,7 @@ #include "io/output.h" #include "io/wav_io.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -161,4 +161,4 @@ void register_stt(CLI::App& app, GlobalOptions& options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_telemetry.cpp b/src/commands/cmd_telemetry.cpp index fb50782..88a4b48 100644 --- a/src/commands/cmd_telemetry.cpp +++ b/src/commands/cmd_telemetry.cpp @@ -1,6 +1,6 @@ /** * @file cmd_telemetry.cpp - * @brief `rcli telemetry emit|blast` — model-free control-plane telemetry. + * @brief `wally telemetry emit|blast` — model-free control-plane telemetry. * * Drives the real commons telemetry pipeline end-to-end: payloads are queued * with rac_telemetry_manager_track, batched + serialized by commons @@ -35,11 +35,11 @@ #include "io/output.h" #include "net/control_plane.h" -#ifndef RCLI_VERSION -#define RCLI_VERSION "0.0.0-dev" +#ifndef WALLY_VERSION +#define WALLY_VERSION "0.0.0-dev" #endif -namespace rcli::commands { +namespace wally::commands { namespace { @@ -258,7 +258,7 @@ bool run_telemetry_session(const GlobalOptions& options, FlushReport* report, Tr const char* device_id = rac_state_get_device_id(); rac_telemetry_manager_t* manager = rac_telemetry_manager_create( rac_state_get_environment(), device_id != nullptr ? device_id : "", net::platform_name(), - RCLI_VERSION); + WALLY_VERSION); if (manager == nullptr) { out::error_line("telemetry manager creation failed"); return false; @@ -509,4 +509,4 @@ void register_telemetry(CLI::App& app, GlobalOptions& options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_tool.cpp b/src/commands/cmd_tool.cpp index 77d81ce..f2f25d6 100644 --- a/src/commands/cmd_tool.cpp +++ b/src/commands/cmd_tool.cpp @@ -1,6 +1,6 @@ /** * @file cmd_tool.cpp - * @brief `rcli llm tool-call` — exercise the tool-calling loop end to end. + * @brief `wally llm tool-call` — exercise the tool-calling loop end to end. * * Thin wrapper over rac_tool_calling_run_loop_proto: load an LLM, hand commons * a prompt plus two built-in demo tools (get_weather, calculate), and let @@ -31,7 +31,7 @@ #include "io/proto.h" #include "progress/progress_bar.h" -namespace rcli::commands { +namespace wally::commands { namespace { namespace v1 = runanywhere::v1; @@ -283,4 +283,4 @@ void register_tool(CLI::App& app, GlobalOptions& options) { options); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_tts.cpp b/src/commands/cmd_tts.cpp index 0a57438..6c050ce 100644 --- a/src/commands/cmd_tts.cpp +++ b/src/commands/cmd_tts.cpp @@ -1,8 +1,8 @@ /** * @file cmd_tts.cpp - * @brief `rcli tts synthesize "text" --output o.wav` — speech synthesis. + * @brief `wally tts synthesize "text" --output o.wav` — speech synthesis. * - * `rcli tts --text "…" --output o.wav` is the same command: the options live on + * `wally tts --text "…" --output o.wav` is the same command: the options live on * the `tts` namespace and `synthesize` is a CLI11 fallthrough alias. * * The sherpa TTS engine returns float PCM at the voice's native sample rate @@ -20,7 +20,7 @@ #include "rac/features/tts/rac_tts_component.h" #include "rac/features/tts/rac_tts_types.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -159,4 +159,4 @@ void register_tts(CLI::App& app, GlobalOptions& options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_usage.cpp b/src/commands/cmd_usage.cpp index d05b5f5..f9cca86 100644 --- a/src/commands/cmd_usage.cpp +++ b/src/commands/cmd_usage.cpp @@ -8,7 +8,7 @@ #include "commands/commands.h" #include "io/output.h" -namespace rcli::commands { +namespace wally::commands { namespace { void fail(int status) { @@ -44,7 +44,7 @@ bool RefreshSession(const account::ConsoleClient& client, account::Credentials* std::string* error) { if (credentials->refresh_token.empty()) { if (error != nullptr) { - *error = "the cloud session cannot be refreshed; run `rcli login`"; + *error = "the cloud session cannot be refreshed; run `wally login`"; } return false; } @@ -153,7 +153,7 @@ int Usage(bool as_json) { return 1; } if (!credentials.signed_in()) { - out::error_line("not signed in — run `rcli login`"); + out::error_line("not signed in — run `wally login`"); return 1; } @@ -183,7 +183,7 @@ int Usage(bool as_json) { // was is not something we know — and sending someone to re-login // over a revoked key wastes the trip. out::error_line("the console rejected this session (" + refresh_failure + - "); run `rcli login`"); + "); run `wally login`"); return 1; } usage = account::Usage{}; @@ -210,10 +210,10 @@ void register_usage(CLI::App& app, GlobalOptions& options) { auto* usage = app.add_subcommand("usage", "credit left, and what the last day cost"); usage->add_flag("--json", *as_json, "machine-readable output"); - // `rcli --json usage` and `rcli usage --json` mean the same thing. The root + // `wally --json usage` and `wally usage --json` mean the same thing. The root // parser accepts the first, so reading only the command-local flag printed a // human table to something asking for one JSON document. usage->callback([as_json, &options] { fail(Usage(*as_json || options.json)); }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_vad.cpp b/src/commands/cmd_vad.cpp index a764d03..b0671d2 100644 --- a/src/commands/cmd_vad.cpp +++ b/src/commands/cmd_vad.cpp @@ -1,8 +1,8 @@ /** * @file cmd_vad.cpp - * @brief `rcli vad detect ` — speech segment detection. + * @brief `wally vad detect ` — speech segment detection. * - * `rcli vad --input a.wav` is the same command: the options live on the `vad` + * `wally vad --input a.wav` is the same command: the options live on the `vad` * namespace and `detect` is a CLI11 fallthrough alias. * * Feeds 16 kHz float frames through the VAD component (Silero when the model @@ -22,7 +22,7 @@ #include "io/output.h" #include "io/wav_io.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -181,4 +181,4 @@ void register_vad(CLI::App& app, GlobalOptions& options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_version.cpp b/src/commands/cmd_version.cpp index cdf89f8..f6ebf7e 100644 --- a/src/commands/cmd_version.cpp +++ b/src/commands/cmd_version.cpp @@ -1,6 +1,6 @@ /** * @file cmd_version.cpp - * @brief `rcli version` — CLI + commons versions. No bootstrap needed. + * @brief `wally version` — CLI + commons versions. No bootstrap needed. */ #include "commands/commands.h" @@ -13,31 +13,31 @@ #include "io/output.h" -#ifndef RCLI_VERSION -#define RCLI_VERSION "0.0.0-dev" +#ifndef WALLY_VERSION +#define WALLY_VERSION "0.0.0-dev" #endif -#ifndef RCLI_PINNED_SDK_VERSION -#define RCLI_PINNED_SDK_VERSION "" +#ifndef WALLY_PINNED_SDK_VERSION +#define WALLY_PINNED_SDK_VERSION "" #endif -namespace rcli::commands { +namespace wally::commands { void register_version(CLI::App& app, GlobalOptions& options) { - CLI::App* cmd = app.add_subcommand("version", "Show rcli and commons versions"); + CLI::App* cmd = app.add_subcommand("version", "Show wally and commons versions"); cmd->callback([&options]() { const rac_version_t commons = rac_get_version(); const std::string commons_version = commons.string ? commons.string : "unknown"; - const bool pin_ok = RCLI_PINNED_SDK_VERSION[0] == '\0' || + const bool pin_ok = WALLY_PINNED_SDK_VERSION[0] == '\0' || (commons.string && - std::strcmp(commons.string, RCLI_PINNED_SDK_VERSION) == 0); + std::strcmp(commons.string, WALLY_PINNED_SDK_VERSION) == 0); if (options.json) { out::JsonWriter json; json.begin_object() - .field("rcli", RCLI_VERSION) + .field("wally", WALLY_VERSION) .field("commons", commons_version) - .field("pinned_sdk", RCLI_PINNED_SDK_VERSION) + .field("pinned_sdk", WALLY_PINNED_SDK_VERSION) .field("idl_version", RUNANYWHERE_IDL_VERSION) .field("idl_schema_sha256", RUNANYWHERE_IDL_SCHEMA_SHA256) .field("idl_protoc", RUNANYWHERE_IDL_PROTOC_VERSION) @@ -45,17 +45,17 @@ void register_version(CLI::App& app, GlobalOptions& options) { .end_object(); out::result_line(json.str()); } else { - out::result_line(std::string("rcli ") + RCLI_VERSION + " (commons " + + out::result_line(std::string("wally ") + WALLY_VERSION + " (commons " + commons_version + ")"); out::result_line(std::string("idl ") + RUNANYWHERE_IDL_VERSION + " sha256 " + RUNANYWHERE_IDL_SCHEMA_SHA256); } if (!pin_ok) { out::error_line(std::string("commons ") + commons_version + - " does not match pin " + RCLI_PINNED_SDK_VERSION); + " does not match pin " + WALLY_PINNED_SDK_VERSION); throw CLI::RuntimeError(1); } }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/cmd_voice.cpp b/src/commands/cmd_voice.cpp index 637d17f..fe8bded 100644 --- a/src/commands/cmd_voice.cpp +++ b/src/commands/cmd_voice.cpp @@ -1,6 +1,6 @@ /** * @file cmd_voice.cpp - * @brief `rcli voice --input a.wav` — one-shot voice turn (STT → LLM → TTS) + * @brief `wally voice --input a.wav` — one-shot voice turn (STT → LLM → TTS) * via the commons voice agent, mirroring tests/test_voice_agent.cpp. */ @@ -23,7 +23,7 @@ #include "io/proto.h" #include "io/wav_io.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -174,4 +174,4 @@ void register_voice(CLI::App& app, GlobalOptions& options) { }); } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/commands.h b/src/commands/commands.h index 9b72c36..7dfc77a 100644 --- a/src/commands/commands.h +++ b/src/commands/commands.h @@ -4,7 +4,7 @@ * * The command surface mirrors the SDK public API spec * (thoughts/shared/plans/public_api_spec.md): a namespace per modality and the - * spec's verb under it (`rcli llm generate`, `rcli models download`, …), with + * spec's verb under it (`wally llm generate`, `wally models download`, …), with * option names in kebab-case (`--max-output-tokens`, `--top-p`). * * Each register_* attaches a CLI11 subcommand whose callback performs: @@ -21,14 +21,19 @@ * to the process exit code (0 ok, 1 runtime error, 2 usage error). */ -#ifndef RCLI_COMMANDS_COMMANDS_H -#define RCLI_COMMANDS_COMMANDS_H +#ifndef WALLY_COMMANDS_COMMANDS_H +#define WALLY_COMMANDS_COMMANDS_H + +#include +#include +#include +#include #include #include "bootstrap.h" -namespace rcli::commands { +namespace wally::commands { // --- Feature namespaces (spec verb grammar) -------------------------------- void register_llm(CLI::App& app, GlobalOptions& options); @@ -54,7 +59,23 @@ void register_models_aliases(CLI::App& app, GlobalOptions& options); // list, p // --- Infrastructure -------------------------------------------------------- void register_version(CLI::App& app, GlobalOptions& options); void register_info(CLI::App& app, GlobalOptions& options); +void register_about(CLI::App& app, GlobalOptions& options); void register_backends(CLI::App& app, GlobalOptions& options); + +/** One registered engine, folded across every primitive it advertises. */ +struct EngineRow { + std::string display_name; + std::string version; + int32_t priority = 0; + std::set primitives; +}; + +/** + * Snapshot of every registered inference backend, keyed by engine name. + * Assumes bootstrap() has already run so the plugin registry is populated — + * shared by `wally backends` and `wally about`. + */ +std::map collect_backend_rows(); void register_serve(CLI::App& app, GlobalOptions& options); void register_bench(CLI::App& app, GlobalOptions& options); void register_auth(CLI::App& app, GlobalOptions& options); @@ -97,7 +118,7 @@ int pull_model_flow(const GlobalOptions& options, const std::string& model_id); /** * Attach the spec verb name to a namespace whose options live on the namespace - * itself (`rcli stt transcribe --input a.wav` and `rcli stt --input a.wav` are + * itself (`wally stt transcribe --input a.wav` and `wally stt --input a.wav` are * the same command). The verb is a grammar marker: CLI11 fallthrough hands its * options to the parent, and the parent owns the single callback. */ @@ -108,6 +129,6 @@ inline CLI::App* add_verb_alias(CLI::App* ns, const std::string& verb, return verb_app; } -} // namespace rcli::commands +} // namespace wally::commands -#endif // RCLI_COMMANDS_COMMANDS_H +#endif // WALLY_COMMANDS_COMMANDS_H diff --git a/src/commands/engine_options.cpp b/src/commands/engine_options.cpp index 2d51cf0..0995e0e 100644 --- a/src/commands/engine_options.cpp +++ b/src/commands/engine_options.cpp @@ -3,7 +3,7 @@ #include #include -namespace rcli::commands { +namespace wally::commands { bool parse_engine_hint(const std::string& engine, runanywhere::v1::InferenceFramework* out_framework, @@ -75,4 +75,4 @@ bool resolve_engine_hint(const std::string& engine, EngineHintResolution* out_re return true; } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/engine_options.h b/src/commands/engine_options.h index b129c80..0a54e95 100644 --- a/src/commands/engine_options.h +++ b/src/commands/engine_options.h @@ -1,17 +1,17 @@ /** * @file engine_options.h - * @brief Shared parsing for rcli engine/framework hints. + * @brief Shared parsing for wally engine/framework hints. */ -#ifndef RCLI_COMMANDS_ENGINE_OPTIONS_H -#define RCLI_COMMANDS_ENGINE_OPTIONS_H +#ifndef WALLY_COMMANDS_ENGINE_OPTIONS_H +#define WALLY_COMMANDS_ENGINE_OPTIONS_H #include #include "model_types.pb.h" #include "catalog/model_ref.h" -namespace rcli::commands { +namespace wally::commands { struct EngineHintResolution { runanywhere::v1::InferenceFramework framework = @@ -26,6 +26,6 @@ bool parse_engine_hint(const std::string& engine, bool resolve_engine_hint(const std::string& engine, EngineHintResolution* out_resolution, std::string* error); -} // namespace rcli::commands +} // namespace wally::commands -#endif // RCLI_COMMANDS_ENGINE_OPTIONS_H +#endif // WALLY_COMMANDS_ENGINE_OPTIONS_H diff --git a/src/commands/model_labels.h b/src/commands/model_labels.h index c8bbbd3..6e23b07 100644 --- a/src/commands/model_labels.h +++ b/src/commands/model_labels.h @@ -2,7 +2,7 @@ #include "model_types.pb.h" -namespace rcli::commands::model_labels { +namespace wally::commands::model_labels { namespace v1 = runanywhere::v1; @@ -118,4 +118,4 @@ inline const char* format(v1::ModelFormat format) { } } -} // namespace rcli::commands::model_labels +} // namespace wally::commands::model_labels diff --git a/src/commands/model_setup.cpp b/src/commands/model_setup.cpp index a75b50f..607413a 100644 --- a/src/commands/model_setup.cpp +++ b/src/commands/model_setup.cpp @@ -10,7 +10,7 @@ #include "io/output.h" #include "io/proto.h" -namespace rcli::commands { +namespace wally::commands { namespace { @@ -117,4 +117,4 @@ int ensure_model_ready(const GlobalOptions &options, const std::string &ref, return 0; } -} // namespace rcli::commands +} // namespace wally::commands diff --git a/src/commands/model_setup.h b/src/commands/model_setup.h index 157b59d..661afe0 100644 --- a/src/commands/model_setup.h +++ b/src/commands/model_setup.h @@ -2,20 +2,20 @@ * @file model_setup.h * @brief Shared ensure-downloaded + resolve-paths step for speech commands. * - * Resolves a model ref, pulls it when missing (same flow as `rcli pull`), and + * Resolves a model ref, pulls it when missing (same flow as `wally pull`), and * resolves the on-disk artifact paths through commons' * rac_model_lifecycle_resolve_paths_proto — no engine load, no path guessing * in the CLI. */ -#ifndef RCLI_COMMANDS_MODEL_SETUP_H -#define RCLI_COMMANDS_MODEL_SETUP_H +#ifndef WALLY_COMMANDS_MODEL_SETUP_H +#define WALLY_COMMANDS_MODEL_SETUP_H #include #include "bootstrap.h" -namespace rcli::commands { +namespace wally::commands { struct ResolvedModelPaths { std::string model_id; @@ -37,6 +37,6 @@ int ensure_model_ready(const GlobalOptions& options, const std::string& ref, */ bool refresh_registry(std::string* error); -} // namespace rcli::commands +} // namespace wally::commands -#endif // RCLI_COMMANDS_MODEL_SETUP_H +#endif // WALLY_COMMANDS_MODEL_SETUP_H diff --git a/src/config/cli_paths.cpp b/src/config/cli_paths.cpp index 4d2b496..93d863b 100644 --- a/src/config/cli_paths.cpp +++ b/src/config/cli_paths.cpp @@ -14,7 +14,7 @@ #include "rac/desktop/rac_desktop.h" -namespace rcli::paths { +namespace wally::paths { namespace { @@ -48,7 +48,7 @@ std::string normalize_dir(std::string dir) { #if defined(_WIN32) // Windows environment values (LOCALAPPDATA, USERPROFILE) come back // backslash-separated, while every path built from them here appends - // '/'-joined segments -- leaving `rcli info` printing a mixed + // '/'-joined segments -- leaving `wally info` printing a mixed // C:\Users\...\AppData\Local/RunAnywhere. Fold to '/' so a single // style survives into the output; Win32 accepts either separator. for (char& c : dir) { @@ -95,4 +95,4 @@ std::string state_dir() { return {}; } -} // namespace rcli::paths +} // namespace wally::paths diff --git a/src/config/cli_paths.h b/src/config/cli_paths.h index 73e2854..1d79347 100644 --- a/src/config/cli_paths.h +++ b/src/config/cli_paths.h @@ -1,6 +1,6 @@ /** * @file cli_paths.h - * @brief rcli directory resolution. + * @brief wally directory resolution. * * One knob controls where models live: the RunAnywhere HOME directory. * resolution: --home flag → $RUNANYWHERE_HOME → ${XDG_DATA_HOME:-~/.local/share}/runanywhere @@ -12,12 +12,12 @@ * REPL history under ${XDG_STATE_HOME:-~/.local/state}/runanywhere. */ -#ifndef RCLI_CONFIG_CLI_PATHS_H -#define RCLI_CONFIG_CLI_PATHS_H +#ifndef WALLY_CONFIG_CLI_PATHS_H +#define WALLY_CONFIG_CLI_PATHS_H #include -namespace rcli::paths { +namespace wally::paths { /** * Resolve the RunAnywhere home (storage base dir) — see file header for the @@ -31,6 +31,6 @@ std::string state_dir(); /** Strip one trailing '/' (keeps root "/"). */ std::string normalize_dir(std::string dir); -} // namespace rcli::paths +} // namespace wally::paths -#endif // RCLI_CONFIG_CLI_PATHS_H +#endif // WALLY_CONFIG_CLI_PATHS_H diff --git a/src/desktop/claude_profile.cpp b/src/desktop/claude_profile.cpp index 0adf7db..1a75a27 100644 --- a/src/desktop/claude_profile.cpp +++ b/src/desktop/claude_profile.cpp @@ -8,7 +8,7 @@ #include -namespace rcli::desktop { +namespace wally::desktop { namespace { using Json = nlohmann::json; @@ -230,4 +230,4 @@ bool GatewayApplied() { return meta.value("appliedId", std::string()) == kProfileID; } -} // namespace rcli::desktop +} // namespace wally::desktop diff --git a/src/desktop/claude_profile.h b/src/desktop/claude_profile.h index c140596..df16ea9 100644 --- a/src/desktop/claude_profile.h +++ b/src/desktop/claude_profile.h @@ -1,5 +1,5 @@ -#ifndef RCLI_DESKTOP_CLAUDE_PROFILE_H -#define RCLI_DESKTOP_CLAUDE_PROFILE_H +#ifndef WALLY_DESKTOP_CLAUDE_PROFILE_H +#define WALLY_DESKTOP_CLAUDE_PROFILE_H #include @@ -12,12 +12,12 @@ /// ends. Neither is the mechanism. /// /// A gateway here speaks the Anthropic Messages API, which is exactly what -/// `rcli::anthropic` already serves. So pointing Claude Desktop at a model we +/// `wally::anthropic` already serves. So pointing Claude Desktop at a model we /// serve is a matter of writing the profile and restarting the app. /// /// Shape learned from ollama/ollama cmd/launch/claude_desktop.go, which drives /// the same feature. -namespace rcli::desktop { +namespace wally::desktop { /// Writes the gateway profile, marks it applied, and switches both config /// trees to third-party mode. @@ -40,6 +40,6 @@ bool GatewayApplied(); /// Where the app keeps its third-party profiles, for a message worth printing. std::string ProfileDirectory(); -} // namespace rcli::desktop +} // namespace wally::desktop -#endif // RCLI_DESKTOP_CLAUDE_PROFILE_H +#endif // WALLY_DESKTOP_CLAUDE_PROFILE_H diff --git a/src/device_info.cpp b/src/device_info.cpp index a827681..e495602 100644 --- a/src/device_info.cpp +++ b/src/device_info.cpp @@ -33,7 +33,7 @@ #include #endif -namespace rcli { +namespace wally { namespace { @@ -570,7 +570,7 @@ void device_set_registered(rac_bool_t registered, void * /*user_data*/) { state().registered = registered == RAC_TRUE; } -// Same control-plane POST shape as rcli_telemetry_http_callback: commons base +// Same control-plane POST shape as wally_telemetry_http_callback: commons base // URL + relative endpoint over the registered desktop HTTP transport, bearer // token attached when the auth manager holds one. rac_result_t device_http_post(const char *endpoint, const char *json_body, @@ -669,6 +669,19 @@ rac_result_t device_http_post(const char *endpoint, const char *json_body, } // namespace +DeviceSnapshot collect_device_snapshot() { + DeviceInfoState info; + collect_device_info(info); + DeviceSnapshot snapshot; + snapshot.chip = info.chip; + snapshot.os_version = info.os_version; + snapshot.architecture = info.architecture; + snapshot.core_count = info.core_count; + snapshot.performance_cores = info.performance_cores; + snapshot.efficiency_cores = info.efficiency_cores; + return snapshot; +} + rac_result_t install_device_callbacks() { auto &info = state(); char device_id[RAC_DEVICE_ID_BUFFER_MIN_SIZE] = {}; @@ -688,4 +701,4 @@ rac_result_t install_device_callbacks() { return rac_device_manager_set_callbacks(&callbacks); } -} // namespace rcli +} // namespace wally diff --git a/src/device_info.h b/src/device_info.h index d18262b..1e2b12f 100644 --- a/src/device_info.h +++ b/src/device_info.h @@ -1,15 +1,34 @@ -#ifndef RCLI_DEVICE_INFO_H -#define RCLI_DEVICE_INFO_H +#ifndef WALLY_DEVICE_INFO_H +#define WALLY_DEVICE_INFO_H + +#include +#include #include "rac/core/rac_types.h" -namespace rcli { +namespace wally { // Installs the desktop device-registration callbacks on the commons device // manager. Must run before SDK phase 2 so registration carries real hardware // info instead of being skipped for missing callbacks. rac_result_t install_device_callbacks(); -} // namespace rcli +// Local CPU/OS/architecture facts, computed the same way the device +// registration payload is (see collect_device_info() in device_info.cpp) but +// without touching the device manager or registering anything. Used by +// `wally about`; memory comes from rac_get_platform_adapter() instead (same +// source `wally info` already uses), not repeated here. +struct DeviceSnapshot { + std::string chip; // CPU model / chip name + std::string os_version; // e.g. "macOS 15.1" + std::string architecture; // e.g. "arm64" + int32_t core_count = 0; + int32_t performance_cores = 0; + int32_t efficiency_cores = 0; +}; + +DeviceSnapshot collect_device_snapshot(); + +} // namespace wally -#endif // RCLI_DEVICE_INFO_H +#endif // WALLY_DEVICE_INFO_H diff --git a/src/harness/harness.cpp b/src/harness/harness.cpp index b03cd9b..68db917 100644 --- a/src/harness/harness.cpp +++ b/src/harness/harness.cpp @@ -14,17 +14,17 @@ #include // Winsock spells these differently: a socket is an unsigned SOCKET rather than // a file descriptor, and getsockname takes an int length rather than socklen_t. -using rcli_socklen_t = int; +using wally_socklen_t = int; #else #include #include #include #include #include -using rcli_socklen_t = socklen_t; +using wally_socklen_t = socklen_t; #endif -#if defined(RCLI_HAS_SERVER) +#if defined(WALLY_HAS_SERVER) #include "rac/server/rac_server.h" #endif @@ -34,13 +34,13 @@ using rcli_socklen_t = socklen_t; #include "bootstrap.h" #include "harness/local_models.h" -namespace rcli::harness { +namespace wally::harness { namespace { /// A port nothing is listening on, found by letting the OS pick one and giving /// it straight back. There is a race between closing and the server binding, -/// but the alternative is a fixed port that collides with a second rcli. +/// but the alternative is a fixed port that collides with a second wally. /// /// `preferred` asks for one particular port and settles for any free one when /// it is taken. An integration that writes the port into a config file wants @@ -75,7 +75,7 @@ int FreePort(int preferred) { address.sin_port = htons(static_cast(preferred)); int port = 0; if (bind(sock, reinterpret_cast(&address), sizeof(address)) == 0) { - rcli_socklen_t length = static_cast(sizeof(address)); + wally_socklen_t length = static_cast(sizeof(address)); if (getsockname(sock, reinterpret_cast(&address), &length) == 0) { port = ntohs(address.sin_port); } @@ -263,14 +263,14 @@ long long EpochSeconds() { .count(); } -/// The refresh half of the same dance `rcli usage` uses: exchange the refresh +/// The refresh half of the same dance `wally usage` uses: exchange the refresh /// token for a new access token and persist it, so later commands in the same /// session do not pay for the refresh again. bool RefreshSession(const account::ConsoleClient& console, account::Credentials* credentials, std::string* error) { if (credentials->refresh_token.empty()) { if (error != nullptr) { - *error = "the cloud session cannot be refreshed; run `rcli login`"; + *error = "the cloud session cannot be refreshed; run `wally login`"; } return false; } @@ -388,7 +388,7 @@ bool Resolve(const std::string& model, Endpoint* endpoint, int preferred_port) { out::error_line("could not find a free port for the local server"); return false; } -#if !defined(RCLI_HAS_SERVER) +#if !defined(WALLY_HAS_SERVER) // This kit was built without the OpenAI-compatible server, so there is // nothing here that can serve a file on disk. An upstream model still // works, and saying which is the case beats starting nothing and @@ -414,7 +414,7 @@ bool Resolve(const std::string& model, Endpoint* endpoint, int preferred_port) { } serving = true; base_url = "http://127.0.0.1:" + std::to_string(port) + "/v1"; -#endif // RCLI_HAS_SERVER +#endif // WALLY_HAS_SERVER } else { account::Credentials credentials; std::string load_error; @@ -424,15 +424,15 @@ bool Resolve(const std::string& model, Endpoint* endpoint, int preferred_port) { } if (!credentials.signed_in()) { out::error_line(model + " is not on this machine, and you are not signed in"); - out::status_line("run `rcli login`, or `rcli pull " + model + "` to run it here"); + out::status_line("run `wally login`, or `wally pull " + model + "` to run it here"); return false; } // signed_in() only proves a token is present, not that it is real: a // hand-written credentials.json satisfies it with any non-empty // string. Everything past this point is destructive to a caller's // running app or session, so confirm the session against the console - // first — the same identity check `rcli whoami` makes, with the same - // refresh-on-401 dance `rcli usage` uses. + // first — the same identity check `wally whoami` makes, with the same + // refresh-on-401 dance `wally usage` uses. const account::ConsoleClient console; std::string email; std::string verify_error; @@ -441,7 +441,7 @@ bool Resolve(const std::string& model, Endpoint* endpoint, int preferred_port) { " is not on this machine, and the signed-in cloud session did not " "check out: " + verify_error); - out::status_line("run `rcli login`, or `rcli pull " + model + "` to run it here"); + out::status_line("run `wally login`, or `wally pull " + model + "` to run it here"); return false; } base_url = credentials.console_url + "/v1"; @@ -457,7 +457,7 @@ bool Resolve(const std::string& model, Endpoint* endpoint, int preferred_port) { void Release(const Endpoint& endpoint) { if (endpoint.serving) { -#if defined(RCLI_HAS_SERVER) +#if defined(WALLY_HAS_SERVER) rac_server_stop(); #endif } @@ -496,4 +496,4 @@ int Launch(const std::string& tool, const std::string& model, return status; } -} // namespace rcli::harness +} // namespace wally::harness diff --git a/src/harness/harness.h b/src/harness/harness.h index 578e2d0..001fc34 100644 --- a/src/harness/harness.h +++ b/src/harness/harness.h @@ -1,5 +1,5 @@ -#ifndef RCLI_HARNESS_HARNESS_H -#define RCLI_HARNESS_HARNESS_H +#ifndef WALLY_HARNESS_HARNESS_H +#define WALLY_HARNESS_HARNESS_H #include #include @@ -15,7 +15,7 @@ /// model the URL is a server this process starts and stops; for an upstream one /// it is the provider's own. That is the same shape Ollama uses, and it is why /// a harness needs no plugin to work with us. -namespace rcli::harness { +namespace wally::harness { /// Where a model can be reached over HTTP, and whether we are serving it. struct Endpoint { @@ -37,8 +37,8 @@ struct Endpoint { bool ModelIdIsSafe(const std::string& id); /// Confirms a cloud session is real before it is used to route a live editor -/// or agent session: refreshes an expired token first (the same dance `rcli -/// usage` uses), then calls the console's identity endpoint the way `rcli +/// or agent session: refreshes an expired token first (the same dance `wally +/// usage` uses), then calls the console's identity endpoint the way `wally /// whoami` does. A non-empty `access_token` alone — `Credentials::signed_in()` /// — proves nothing: it is a local, offline check that a hand-written /// credentials.json satisfies trivially. @@ -74,10 +74,10 @@ void Release(const Endpoint& endpoint); /// exit code. Blocks until the tool exits, then stops anything it started. /// /// An empty `model` uses whatever the tool is already configured for, which -/// makes `rcli opencode` a plain passthrough. +/// makes `wally opencode` a plain passthrough. int Launch(const std::string& tool, const std::string& model, const std::vector& args); -} // namespace rcli::harness +} // namespace wally::harness -#endif // RCLI_HARNESS_HARNESS_H +#endif // WALLY_HARNESS_HARNESS_H diff --git a/src/harness/local_models.cpp b/src/harness/local_models.cpp index ea68871..9494e8f 100644 --- a/src/harness/local_models.cpp +++ b/src/harness/local_models.cpp @@ -5,7 +5,7 @@ #include #include -namespace rcli::harness { +namespace wally::harness { namespace { namespace fs = std::filesystem; @@ -107,4 +107,4 @@ std::vector LocalModels(const std::string& home) { return models; } -} // namespace rcli::harness +} // namespace wally::harness diff --git a/src/harness/local_models.h b/src/harness/local_models.h index 94f39d8..e4fafe9 100644 --- a/src/harness/local_models.h +++ b/src/harness/local_models.h @@ -1,11 +1,11 @@ -#ifndef RCLI_HARNESS_LOCAL_MODELS_H -#define RCLI_HARNESS_LOCAL_MODELS_H +#ifndef WALLY_HARNESS_LOCAL_MODELS_H +#define WALLY_HARNESS_LOCAL_MODELS_H #include #include #include -namespace rcli::harness { +namespace wally::harness { /// A model already on disk under the RunAnywhere home. struct LocalModel { @@ -27,6 +27,6 @@ struct LocalModel { /// model placed by hand as readily as one that was downloaded. std::vector LocalModels(const std::string& home); -} // namespace rcli::harness +} // namespace wally::harness -#endif // RCLI_HARNESS_LOCAL_MODELS_H +#endif // WALLY_HARNESS_LOCAL_MODELS_H diff --git a/src/harness/opencode.cpp b/src/harness/opencode.cpp index e2c30d5..dc0607c 100644 --- a/src/harness/opencode.cpp +++ b/src/harness/opencode.cpp @@ -21,7 +21,7 @@ #include "account/credentials.h" #include "io/output.h" -namespace rcli::harness { +namespace wally::harness { namespace { constexpr const char* kOpenCodeConfigVariable = "OPENCODE_CONFIG_CONTENT"; @@ -170,7 +170,7 @@ int LaunchOpenCodeCloud(const std::string& model, const std::vector return 1; } if (!credentials.signed_in()) { - out::error_line("not signed in - run `rcli login`"); + out::error_line("not signed in - run `wally login`"); return 1; } if (!VerifyCloudSession(console, &credentials, nullptr, &error)) { @@ -195,4 +195,4 @@ int LaunchOpenCodeCloud(const std::string& model, const std::vector return LaunchOpenCodeCloud(model, arguments, console, Spawn); } -} // namespace rcli::harness +} // namespace wally::harness diff --git a/src/harness/opencode.h b/src/harness/opencode.h index 42db3c4..d2fc8ba 100644 --- a/src/harness/opencode.h +++ b/src/harness/opencode.h @@ -1,5 +1,5 @@ -#ifndef RCLI_HARNESS_OPENCODE_H -#define RCLI_HARNESS_OPENCODE_H +#ifndef WALLY_HARNESS_OPENCODE_H +#define WALLY_HARNESS_OPENCODE_H #include #include @@ -7,7 +7,7 @@ #include "account/console.h" -namespace rcli::harness { +namespace wally::harness { using SpawnFunction = std::function& arguments)>; @@ -28,6 +28,6 @@ int LaunchOpenCodeCloud(const std::string& model, const std::vector int LaunchOpenCodeCloud(const std::string& model, const std::vector& arguments, const account::ConsoleClient& console, const SpawnFunction& spawn); -} // namespace rcli::harness +} // namespace wally::harness -#endif // RCLI_HARNESS_OPENCODE_H +#endif // WALLY_HARNESS_OPENCODE_H diff --git a/src/ide/jetbrains_profile.cpp b/src/ide/jetbrains_profile.cpp index 7519550..eb66d8e 100644 --- a/src/ide/jetbrains_profile.cpp +++ b/src/ide/jetbrains_profile.cpp @@ -16,7 +16,7 @@ #include #endif -namespace rcli::ide { +namespace wally::ide { namespace { namespace fs = std::filesystem; @@ -87,7 +87,7 @@ std::string SettingsXML(const std::string& base_url) { /// Which model each of the IDE's three roles should use. /// -/// One model answers all three because that is what rcli is serving. The id is +/// One model answers all three because that is what wally is serving. The id is /// `/`, the separator being `ThirdPartyLLMProfileId.DELIM`. std::string ModelsXML(const std::string& model) { const std::string id = std::string(kProviderID) + "/" + model; @@ -182,7 +182,7 @@ CFMutableDictionaryRef CopyQuery() { /// An access list naming the two programs allowed to read the item without /// asking: this one, which writes it, and the IDE, which reads it. /// -/// Without this the item belongs to rcli alone, and the IDE's first read pops a +/// Without this the item belongs to wally alone, and the IDE's first read pops a /// keychain dialog — the one manual step this command exists to remove. The /// legacy access APIs are what create such a list, and they are also what the /// platform's own `MacOSKeychainStorage` uses, so the item ends up the shape @@ -372,4 +372,4 @@ bool RestoreProvider(const Product& product, std::string* error) { return true; } -} // namespace rcli::ide +} // namespace wally::ide diff --git a/src/ide/jetbrains_profile.h b/src/ide/jetbrains_profile.h index 0fb3dc8..d4f30d1 100644 --- a/src/ide/jetbrains_profile.h +++ b/src/ide/jetbrains_profile.h @@ -1,5 +1,5 @@ -#ifndef RCLI_IDE_JETBRAINS_PROFILE_H -#define RCLI_IDE_JETBRAINS_PROFILE_H +#ifndef WALLY_IDE_JETBRAINS_PROFILE_H +#define WALLY_IDE_JETBRAINS_PROFILE_H #include @@ -23,14 +23,14 @@ /// and the enable flag beside them. /// /// None of this needs a JetBrains AI subscription. BYOK is the supported path. -namespace rcli::ide { +namespace wally::ide { /// The port the local server is asked for when serving a JetBrains IDE. /// /// Fixed on purpose. The IDE reads its base URL once at startup, from a file /// written before it launches, so a port that moved between runs would leave -/// that file naming something dead every time rcli exited first. Asking for the -/// same one keeps the configuration true, and a second rcli holding it only +/// that file naming something dead every time wally exited first. Asking for the +/// same one keeps the configuration true, and a second wally holding it only /// costs this run a rewrite. constexpr int kProviderPort = 11636; @@ -68,6 +68,6 @@ std::string ConfigDirectory(const Product& product); /// The application bundle's path, or empty when the IDE is not installed. std::string BundlePath(const Product& product); -} // namespace rcli::ide +} // namespace wally::ide -#endif // RCLI_IDE_JETBRAINS_PROFILE_H +#endif // WALLY_IDE_JETBRAINS_PROFILE_H diff --git a/src/ide/openai_proxy.cpp b/src/ide/openai_proxy.cpp index 6abd19a..6d5874b 100644 --- a/src/ide/openai_proxy.cpp +++ b/src/ide/openai_proxy.cpp @@ -18,8 +18,9 @@ #include "account/console.h" #include "account/credentials.h" #include "io/output.h" +#include "net/loopback_auth.h" -namespace rcli::ide { +namespace wally::ide { namespace { /// Splits `http://host:port/v1` into `http://host:port` and `/v1`. @@ -46,6 +47,9 @@ struct Runtime { std::string prefix; std::string api_key; std::string model; + // The secret the editor must present. Loopback binding keeps the network + // out; this keeps another local process out. + std::string local_token; bool verbose = false; }; @@ -158,10 +162,10 @@ using Json = nlohmann::json; /// something the editor cannot read. std::string ChunkSaying(const std::string& message) { Json chunk; - chunk["id"] = "chatcmpl-rcli"; + chunk["id"] = "chatcmpl-wally"; chunk["object"] = "chat.completion.chunk"; chunk["created"] = 0; - chunk["model"] = "rcli"; + chunk["model"] = "wally"; Json choice; choice["index"] = 0; choice["delta"] = Json{{"role", "assistant"}, {"content", message}}; @@ -242,7 +246,7 @@ std::string Normalise(const std::string& frame, bool verbose) { return ChunkSaying(message); } -/// Points a request at the model rcli is serving, whatever it named. +/// Points a request at the model wally is serving, whatever it named. /// /// A stale selection saved in the editor's own settings outlives any change to /// the list we advertise, so the name in the request cannot be trusted even @@ -381,8 +385,8 @@ void Stream(Runtime& runtime, const std::string& body, httplib::Response& respon } } const std::string frame = - "data: {\"id\":\"chatcmpl-rcli\",\"object\":\"chat.completion.chunk\"," - "\"created\":0,\"model\":\"rcli\",\"choices\":[{\"index\":0,\"delta\":" + "data: {\"id\":\"chatcmpl-wally\",\"object\":\"chat.completion.chunk\"," + "\"created\":0,\"model\":\"wally\",\"choices\":[{\"index\":0,\"delta\":" "{\"role\":\"assistant\",\"content\":\"" + message + "\"},\"finish_reason\":\"stop\"}]}\n\n"; sink.write(frame.data(), frame.size()); @@ -412,14 +416,15 @@ bool StartProxy(const harness::Endpoint& endpoint, const std::string& model, int } runtime->api_key = endpoint.api_key; runtime->model = model; + runtime->local_token = wally::net::GenerateLoopbackToken(); runtime->verbose = verbose; Runtime* raw = runtime.get(); // Every handler catches. An exception thrown into cpp-httplib takes the - // process down with it, and a dead rcli takes the model with it too. + // process down with it, and a dead wally takes the model with it too. raw->server.Get("/v1/models", [raw](const httplib::Request&, httplib::Response& response) { try { - // Not forwarded. The one model rcli was asked to serve is the one + // Not forwarded. The one model wally was asked to serve is the one // offered, so there is nothing in the picker that cannot answer. Json entry; entry["id"] = raw->model; @@ -436,6 +441,20 @@ bool StartProxy(const harness::Endpoint& endpoint, const std::string& model, int raw->server.Post("/v1/chat/completions", [raw](const httplib::Request& request, httplib::Response& response) { + // This endpoint spends the signed-in user's credit, so + // it serves only the editor wally configured. Bearer + // token, from the provider key stored in the IDE. + std::string presented; + const std::string authorization = request.get_header_value("Authorization"); + constexpr const char* kBearer = "Bearer "; + if (authorization.rfind(kBearer, 0) == 0) { + presented = authorization.substr(std::string(kBearer).size()); + } + if (!wally::net::ConstantTimeEquals(presented, raw->local_token)) { + Fail(response, 401, + "this local endpoint only serves the editor wally configured"); + return; + } try { const std::string body = Retarget(*raw, request.body); // The editor decides whether to stream; we only @@ -453,6 +472,14 @@ bool StartProxy(const harness::Endpoint& endpoint, const std::string& model, int return; } response.status = reply->status; + // An overloaded upstream answers 429 with a + // Retry-After the wrapped tool is expected to back + // off on. httplib drops response headers unless we + // copy them, so forward this one explicitly. + if (reply->status == 429 && reply->has_header("Retry-After")) { + response.set_header("Retry-After", + reply->get_header_value("Retry-After")); + } response.set_content(reply->body, "application/json"); } catch (const std::exception& error) { Fail(response, 500, error.what()); @@ -475,6 +502,7 @@ bool StartProxy(const harness::Endpoint& endpoint, const std::string& model, int proxy->running = true; proxy->base_url = "http://127.0.0.1:" + std::to_string(bound) + "/v1"; + proxy->auth_token = raw->local_token; return true; } @@ -492,4 +520,4 @@ void StopProxy(Proxy* proxy) { } } -} // namespace rcli::ide +} // namespace wally::ide diff --git a/src/ide/openai_proxy.h b/src/ide/openai_proxy.h index 725fd37..cc4a35f 100644 --- a/src/ide/openai_proxy.h +++ b/src/ide/openai_proxy.h @@ -1,5 +1,5 @@ -#ifndef RCLI_IDE_OPENAI_PROXY_H -#define RCLI_IDE_OPENAI_PROXY_H +#ifndef WALLY_IDE_OPENAI_PROXY_H +#define WALLY_IDE_OPENAI_PROXY_H #include @@ -13,16 +13,20 @@ /// it in the IDE's credential store, and the IDE never reads what we write /// there — the provider comes up with an empty key and the console answers 401. /// -/// So the token stays here. rcli listens on loopback, adds the header, and +/// So the token stays here. wally listens on loopback, adds the header, and /// forwards. The IDE is configured exactly as it is for a local model, with no /// key at all, which is the case already known to work. It also keeps the /// reader's token out of a second store that neither of us controls. -namespace rcli::ide { +namespace wally::ide { struct Proxy { bool running = false; /// What to point the editor at. An OpenAI-compatible root ending in `/v1`. std::string base_url; + /// The API key the editor must send back. A per-session secret, so a stray + /// local process cannot reach the loopback port and spend on the signed-in + /// user's credit. Give it to the editor as the provider key. + std::string auth_token; }; /// Listens on `port` and forwards to `endpoint`, adding its credential. @@ -31,7 +35,7 @@ struct Proxy { /// whatever it asked for. The console lists its provider's whole catalogue, /// deprecated entries included, and an editor showing all of them invites a /// choice that fails — which is how `models/gemini-2.5-pro`, retired for new -/// users, ended up being asked a question. rcli was told which model to serve; +/// users, ended up being asked a question. wally was told which model to serve; /// that is the one the editor gets. /// /// Returns false having already said why. Nothing else is translated on the way @@ -42,6 +46,6 @@ bool StartProxy(const harness::Endpoint& endpoint, const std::string& model, int /// Stops the listener. Safe on a proxy that never started. void StopProxy(Proxy* proxy); -} // namespace rcli::ide +} // namespace wally::ide -#endif // RCLI_IDE_OPENAI_PROXY_H +#endif // WALLY_IDE_OPENAI_PROXY_H diff --git a/src/io/image_io.cpp b/src/io/image_io.cpp index 0cca55b..305c728 100644 --- a/src/io/image_io.cpp +++ b/src/io/image_io.cpp @@ -3,7 +3,7 @@ * @brief RGBA → PNG encoder (stored/uncompressed DEFLATE, zero dependencies). * * The commons CoreML diffusion engine returns raw RGBA pixel data. To honour - * `rcli image ... --out foo.png` we wrap those pixels in a valid PNG container. + * `wally image ... --out foo.png` we wrap those pixels in a valid PNG container. * We avoid pulling libpng/zlib into the CLI by emitting the IDAT as a zlib * stream built from *stored* DEFLATE blocks (BTYPE=00). The file is larger than * a compressed PNG but is byte-for-byte valid per the PNG/zlib/DEFLATE specs. @@ -18,7 +18,7 @@ #include #include -namespace rcli::image { +namespace wally::image { namespace { @@ -254,4 +254,4 @@ bool read_ppm(const std::string& path, RgbImage* out, std::string* error) { return true; } -} // namespace rcli::image +} // namespace wally::image diff --git a/src/io/image_io.h b/src/io/image_io.h index 471b6b3..0c34670 100644 --- a/src/io/image_io.h +++ b/src/io/image_io.h @@ -1,22 +1,22 @@ /** * @file image_io.h - * @brief Minimal RGBA → PNG encoder for `rcli image` output. + * @brief Minimal RGBA → PNG encoder for `wally image` output. * * CLI-owned file I/O, mirroring io/wav_io: commons diffusion engines return - * raw RGBA pixels (image_media_type "image/raw-rgba"); rcli renders them to a + * raw RGBA pixels (image_media_type "image/raw-rgba"); wally renders them to a * real PNG so `--out foo.png` is a valid image. Self-contained (no libpng / * zlib dependency): emits a PNG whose IDAT is a zlib stream of *stored* * (uncompressed) DEFLATE blocks, which every decoder accepts. */ -#ifndef RCLI_IO_IMAGE_IO_H -#define RCLI_IO_IMAGE_IO_H +#ifndef WALLY_IO_IMAGE_IO_H +#define WALLY_IO_IMAGE_IO_H #include #include #include -namespace rcli::image { +namespace wally::image { /** * Write 8-bit RGBA pixels (row-major, width*height*4 bytes) as a PNG file. @@ -40,6 +40,6 @@ struct RgbImage { */ bool read_ppm(const std::string& path, RgbImage* out, std::string* error); -} // namespace rcli::image +} // namespace wally::image -#endif // RCLI_IO_IMAGE_IO_H +#endif // WALLY_IO_IMAGE_IO_H diff --git a/src/io/output.cpp b/src/io/output.cpp index 4dfbbc5..d061d92 100644 --- a/src/io/output.cpp +++ b/src/io/output.cpp @@ -1,11 +1,12 @@ #include "io/output.h" #include +#include #include #include "rac/core/rac_error.h" -namespace rcli::out { +namespace wally::out { std::string json_escape(const std::string& value) { std::string escaped; @@ -106,9 +107,17 @@ JsonWriter& JsonWriter::field(const std::string& key, int64_t value) { JsonWriter& JsonWriter::field(const std::string& key, double value) { comma(); + buffer_ += '"' + json_escape(key) + "\":"; + // NaN and Infinity have no JSON literal; an engine that doesn't compute a + // metric (e.g. sherpa's STT confidence) hands back a raw NaN, and %g would + // print it verbatim as the bareword `nan` — invalid JSON on every call. + if (!std::isfinite(value)) { + buffer_ += "null"; + return *this; + } char buf[48]; std::snprintf(buf, sizeof(buf), "%g", value); - buffer_ += '"' + json_escape(key) + "\":" + buf; + buffer_ += buf; return *this; } @@ -138,6 +147,10 @@ JsonWriter& JsonWriter::value(int64_t value) { JsonWriter& JsonWriter::value(double value) { comma(); + if (!std::isfinite(value)) { + buffer_ += "null"; + return *this; + } char buf[48]; std::snprintf(buf, sizeof(buf), "%g", value); buffer_ += buf; @@ -218,4 +231,4 @@ void table(const std::vector& header, } } -} // namespace rcli::out +} // namespace wally::out diff --git a/src/io/output.h b/src/io/output.h index f8b8acf..d785b7c 100644 --- a/src/io/output.h +++ b/src/io/output.h @@ -2,14 +2,14 @@ * @file output.h * @brief Output discipline helpers + minimal JSON emission. * - * Contract (see rcli/AGENTS.md): + * Contract (see wally/AGENTS.md): * - command RESULTS go to stdout; * - logs, progress, banners, prompts go to stderr; * - --json mode prints exactly ONE JSON document on stdout. */ -#ifndef RCLI_IO_OUTPUT_H -#define RCLI_IO_OUTPUT_H +#ifndef WALLY_IO_OUTPUT_H +#define WALLY_IO_OUTPUT_H #include #include @@ -17,13 +17,13 @@ #include "rac/core/rac_types.h" -namespace rcli::out { +namespace wally::out { /** JSON-escape a UTF-8 string (quotes not included). */ std::string json_escape(const std::string& value); /** - * Minimal JSON document builder — enough for rcli's flat objects/arrays + * Minimal JSON document builder — enough for wally's flat objects/arrays * without pulling a JSON dependency into the CLI. */ class JsonWriter { @@ -73,6 +73,6 @@ std::string human_bytes(uint64_t bytes); void table(const std::vector& header, const std::vector>& rows); -} // namespace rcli::out +} // namespace wally::out -#endif // RCLI_IO_OUTPUT_H +#endif // WALLY_IO_OUTPUT_H diff --git a/src/io/proto.h b/src/io/proto.h index 425bff1..73b0610 100644 --- a/src/io/proto.h +++ b/src/io/proto.h @@ -8,8 +8,8 @@ * the buffer. */ -#ifndef RCLI_IO_PROTO_H -#define RCLI_IO_PROTO_H +#ifndef WALLY_IO_PROTO_H +#define WALLY_IO_PROTO_H #include @@ -17,7 +17,7 @@ #include "io/output.h" -namespace rcli::proto { +namespace wally::proto { /** * Parse an out-buffer into `message`, freeing the buffer in all paths. @@ -31,7 +31,7 @@ bool parse_proto_buffer(rac_proto_buffer_t* buffer, Message* message, std::strin if (error) { *error = (buffer->error_message && buffer->error_message[0] != '\0') ? buffer->error_message - : rcli::out::describe_result(buffer->status); + : wally::out::describe_result(buffer->status); } } else if (!message->ParseFromArray(buffer->data, static_cast(buffer->size))) { if (error) { @@ -56,6 +56,6 @@ std::string serialize(const Message& message) { return bytes; } -} // namespace rcli::proto +} // namespace wally::proto -#endif // RCLI_IO_PROTO_H +#endif // WALLY_IO_PROTO_H diff --git a/src/io/wav_io.cpp b/src/io/wav_io.cpp index 68a92be..3dcf0e3 100644 --- a/src/io/wav_io.cpp +++ b/src/io/wav_io.cpp @@ -7,7 +7,7 @@ #include "rac/core/rac_audio_utils.h" #include "rac/core/rac_types.h" -namespace rcli::wav { +namespace wally::wav { namespace { @@ -217,4 +217,4 @@ std::vector to_float(const std::vector& samples) { return out; } -} // namespace rcli::wav +} // namespace wally::wav diff --git a/src/io/wav_io.h b/src/io/wav_io.h index 95d8312..fb09f49 100644 --- a/src/io/wav_io.h +++ b/src/io/wav_io.h @@ -7,14 +7,14 @@ * thin wrappers that forward to those primitives. */ -#ifndef RCLI_IO_WAV_IO_H -#define RCLI_IO_WAV_IO_H +#ifndef WALLY_IO_WAV_IO_H +#define WALLY_IO_WAV_IO_H #include #include #include -namespace rcli::wav { +namespace wally::wav { struct WavData { std::vector samples; // mono (channels collapsed by averaging) @@ -38,6 +38,6 @@ std::vector resample(const std::vector& samples, int from_rate /** int16 → float [-1, 1] via rac_audio_pcm16_to_float32. */ std::vector to_float(const std::vector& samples); -} // namespace rcli::wav +} // namespace wally::wav -#endif // RCLI_IO_WAV_IO_H +#endif // WALLY_IO_WAV_IO_H diff --git a/src/main.cpp b/src/main.cpp index 83be311..d0fd7f8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,6 @@ /** * @file main.cpp - * @brief rcli — RunAnywhere desktop CLI entry point. + * @brief wally — RunAnywhere desktop CLI entry point. * * Thin dispatch layer: global flags + CLI11 subcommands. All real work * happens in commons behind the rac_* C ABI (see AGENTS.md layering rule). @@ -11,7 +11,7 @@ #include "app.h" int main(int argc, char** argv) { - // Nothing but dispatch. Quieting the SDK lives in rcli_run_main(), which is + // Nothing but dispatch. Quieting the SDK lives in wally_run_main(), which is // the entry both this binary and the Swift MLX host go through. - return rcli_run_main(argc, argv); + return wally_run_main(argc, argv); } diff --git a/src/net/control_plane.cpp b/src/net/control_plane.cpp index c48e55b..6856775 100644 --- a/src/net/control_plane.cpp +++ b/src/net/control_plane.cpp @@ -1,6 +1,6 @@ /** * @file control_plane.cpp - * @brief Control-plane network wiring for rcli — see control_plane.h. + * @brief Control-plane network wiring for wally — see control_plane.h. * * The CLI drives the canonical commons entry points and adds only what is * genuinely CLI-shaped: a buffered POST helper the telemetry commands reuse and @@ -27,7 +27,7 @@ #include "io/output.h" #include "io/proto.h" -namespace rcli::net { +namespace wally::net { namespace { @@ -246,4 +246,4 @@ rac_result_t login(LoginSummary* out, std::string* error) { return RAC_SUCCESS; } -} // namespace rcli::net +} // namespace wally::net diff --git a/src/net/control_plane.h b/src/net/control_plane.h index 8397d88..ba780f7 100644 --- a/src/net/control_plane.h +++ b/src/net/control_plane.h @@ -1,8 +1,8 @@ /** * @file control_plane.h - * @brief Control-plane network wiring for rcli (auth, device, telemetry HTTP). + * @brief Control-plane network wiring for wally (auth, device, telemetry HTTP). * - * rcli is the 6th consumer of runanywhere-commons and plays the same role the + * wally is the 6th consumer of runanywhere-commons and plays the same role the * Swift/Kotlin/Flutter/RN/Web bridges play for the control plane. This module * drives the canonical commons entry points (rac_auth_* + * rac_sdk_init_phase2_proto); bootstrap.cpp installs platform callbacks through @@ -12,15 +12,15 @@ * Requires bootstrap() (rac_init + curl transport + rac_state) to have run. */ -#ifndef RCLI_NET_CONTROL_PLANE_H -#define RCLI_NET_CONTROL_PLANE_H +#ifndef WALLY_NET_CONTROL_PLANE_H +#define WALLY_NET_CONTROL_PLANE_H #include #include #include "rac/core/rac_types.h" -namespace rcli::net { +namespace wally::net { /** "macos" / "linux" / "windows" — the X-Platform header + auth payload value. */ const char* platform_name(); @@ -83,6 +83,6 @@ struct LoginSummary { */ rac_result_t login(LoginSummary* out, std::string* error); -} // namespace rcli::net +} // namespace wally::net -#endif // RCLI_NET_CONTROL_PLANE_H +#endif // WALLY_NET_CONTROL_PLANE_H diff --git a/src/net/loopback_auth.cpp b/src/net/loopback_auth.cpp new file mode 100644 index 0000000..2a0ec22 --- /dev/null +++ b/src/net/loopback_auth.cpp @@ -0,0 +1,34 @@ +#include "net/loopback_auth.h" + +#include +#include +#include + +namespace wally::net { + +std::string GenerateLoopbackToken() { + std::random_device device; + std::uniform_int_distribution byte(0, 255); + static constexpr char kHex[] = "0123456789abcdef"; + std::string token; + token.reserve(64); + for (int i = 0; i < 32; ++i) { + const unsigned value = byte(device); + token.push_back(kHex[(value >> 4) & 0xF]); + token.push_back(kHex[value & 0xF]); + } + return token; +} + +bool ConstantTimeEquals(const std::string& a, const std::string& b) { + if (a.size() != b.size()) { + return false; + } + unsigned char difference = 0; + for (std::size_t i = 0; i < a.size(); ++i) { + difference |= static_cast(a[i]) ^ static_cast(b[i]); + } + return difference == 0; +} + +} // namespace wally::net diff --git a/src/net/loopback_auth.h b/src/net/loopback_auth.h new file mode 100644 index 0000000..3bb9bca --- /dev/null +++ b/src/net/loopback_auth.h @@ -0,0 +1,29 @@ +#ifndef WALLY_NET_LOOPBACK_AUTH_H +#define WALLY_NET_LOOPBACK_AUTH_H + +#include + +namespace wally::net { + +/// A fresh random secret for a loopback proxy to hand its wrapped tool. +/// +/// The coding-harness proxies bind 127.0.0.1 and forward to the hosted API on +/// the signed-in user's credit. The port is reachable by any other process +/// running as the same local user, so the proxy must prove its caller is the +/// tool it launched and not a bystander. It does that by generating one of +/// these per session, giving it to the tool as its API key, and rejecting any +/// request that does not present it. +/// +/// The value is 32 bytes of `std::random_device` output rendered as hex. On the +/// supported platforms `random_device` is the OS CSPRNG; this is not a +/// cryptographic key exchange, only a same-host capability token, so that is +/// enough. Compared byte-for-byte with a constant-time check at the boundary. +std::string GenerateLoopbackToken(); + +/// A length-independent equality check, so a caller cannot learn the secret one +/// byte at a time from how long a rejection takes. +bool ConstantTimeEquals(const std::string& a, const std::string& b); + +} // namespace wally::net + +#endif // WALLY_NET_LOOPBACK_AUTH_H diff --git a/src/progress/progress_bar.cpp b/src/progress/progress_bar.cpp index 4bd490e..560d8e9 100644 --- a/src/progress/progress_bar.cpp +++ b/src/progress/progress_bar.cpp @@ -8,7 +8,7 @@ #include "io/output.h" #include "util/term.h" -namespace rcli::progress { +namespace wally::progress { namespace { @@ -183,4 +183,4 @@ void DownloadProgressScope::callback(const uint8_t* proto_bytes, size_t proto_si } } -} // namespace rcli::progress +} // namespace wally::progress diff --git a/src/progress/progress_bar.h b/src/progress/progress_bar.h index 13fa26d..2433dd1 100644 --- a/src/progress/progress_bar.h +++ b/src/progress/progress_bar.h @@ -7,15 +7,15 @@ * so CI logs stay readable. */ -#ifndef RCLI_PROGRESS_PROGRESS_BAR_H -#define RCLI_PROGRESS_PROGRESS_BAR_H +#ifndef WALLY_PROGRESS_PROGRESS_BAR_H +#define WALLY_PROGRESS_PROGRESS_BAR_H #include #include #include "download_service.pb.h" -namespace rcli::progress { +namespace wally::progress { class ProgressRenderer { public: @@ -39,7 +39,7 @@ class ProgressRenderer { /** * RAII wrapper: registers the process-wide download progress callback and * renders updates for one model while alive (used by commands whose commons - * call may auto-download, e.g. lifecycle load in `rcli run`). Only one scope + * call may auto-download, e.g. lifecycle load in `wally run`). Only one scope * may be active per process at a time. Thread-safe — events arrive on * orchestrator worker threads. */ @@ -56,6 +56,6 @@ class DownloadProgressScope { std::string model_id_; }; -} // namespace rcli::progress +} // namespace wally::progress -#endif // RCLI_PROGRESS_PROGRESS_BAR_H +#endif // WALLY_PROGRESS_PROGRESS_BAR_H diff --git a/src/repl/repl.cpp b/src/repl/repl.cpp index 28b8ad6..6c4b970 100644 --- a/src/repl/repl.cpp +++ b/src/repl/repl.cpp @@ -5,13 +5,13 @@ #include #include -#if !defined(RCLI_NO_LINENOISE) +#if !defined(WALLY_NO_LINENOISE) extern "C" { #include } #endif -namespace rcli::repl { +namespace wally::repl { namespace { // MSVC's / decode a narrow std::string through the ANSI @@ -26,7 +26,7 @@ LineEditor::LineEditor(std::string history_path) : history_path_(std::move(histo if (!history_path_.empty()) { std::error_code ec; std::filesystem::create_directories(utf8_path(history_path_).parent_path(), ec); -#if !defined(RCLI_NO_LINENOISE) +#if !defined(WALLY_NO_LINENOISE) linenoiseHistoryLoad(history_path_.c_str()); linenoiseHistorySetMaxLen(512); #endif @@ -34,7 +34,7 @@ LineEditor::LineEditor(std::string history_path) : history_path_(std::move(histo } LineEditor::~LineEditor() { -#if !defined(RCLI_NO_LINENOISE) +#if !defined(WALLY_NO_LINENOISE) if (!history_path_.empty()) { linenoiseHistorySave(history_path_.c_str()); } @@ -42,7 +42,7 @@ LineEditor::~LineEditor() { } bool LineEditor::read_line(const std::string& prompt, std::string* out_line) { -#if defined(RCLI_NO_LINENOISE) +#if defined(WALLY_NO_LINENOISE) std::cerr << prompt; std::cerr.flush(); return static_cast(std::getline(std::cin, *out_line)); @@ -59,7 +59,7 @@ bool LineEditor::read_line(const std::string& prompt, std::string* out_line) { void LineEditor::add_history(const std::string& line) { if (!line.empty()) { -#if defined(RCLI_NO_LINENOISE) +#if defined(WALLY_NO_LINENOISE) if (!history_path_.empty()) { std::ofstream history(utf8_path(history_path_), std::ios::app); history << line << '\n'; @@ -70,4 +70,4 @@ void LineEditor::add_history(const std::string& line) { } } -} // namespace rcli::repl +} // namespace wally::repl diff --git a/src/repl/repl.h b/src/repl/repl.h index 32b6522..bfef7d8 100644 --- a/src/repl/repl.h +++ b/src/repl/repl.h @@ -3,12 +3,12 @@ * @brief Thin RAII wrapper over vendored linenoise (history + line input). */ -#ifndef RCLI_REPL_REPL_H -#define RCLI_REPL_REPL_H +#ifndef WALLY_REPL_REPL_H +#define WALLY_REPL_REPL_H #include -namespace rcli::repl { +namespace wally::repl { class LineEditor { public: @@ -26,6 +26,6 @@ class LineEditor { std::string history_path_; }; -} // namespace rcli::repl +} // namespace wally::repl -#endif // RCLI_REPL_REPL_H +#endif // WALLY_REPL_REPL_H diff --git a/src/util/term.cpp b/src/util/term.cpp index 3aaf011..7e73972 100644 --- a/src/util/term.cpp +++ b/src/util/term.cpp @@ -14,7 +14,7 @@ #include #endif -namespace rcli::term { +namespace wally::term { bool stdout_is_tty() { #if defined(_WIN32) @@ -59,4 +59,4 @@ bool color_enabled() { return stderr_is_tty() && std::getenv("NO_COLOR") == nullptr; } -} // namespace rcli::term +} // namespace wally::term diff --git a/src/util/term.h b/src/util/term.h index ac24dbe..29b17e7 100644 --- a/src/util/term.h +++ b/src/util/term.h @@ -3,10 +3,10 @@ * @brief Terminal capabilities: TTY detection, width, color policy. */ -#ifndef RCLI_UTIL_TERM_H -#define RCLI_UTIL_TERM_H +#ifndef WALLY_UTIL_TERM_H +#define WALLY_UTIL_TERM_H -namespace rcli::term { +namespace wally::term { /** True when stdout is an interactive terminal. */ bool stdout_is_tty(); @@ -23,6 +23,6 @@ int terminal_width(); /** ANSI color allowed on stderr: TTY and NO_COLOR unset. */ bool color_enabled(); -} // namespace rcli::term +} // namespace wally::term -#endif // RCLI_UTIL_TERM_H +#endif // WALLY_UTIL_TERM_H diff --git a/src/windows_proto_compat.h b/src/windows_proto_compat.h index 8dff488..48a255e 100644 --- a/src/windows_proto_compat.h +++ b/src/windows_proto_compat.h @@ -1,7 +1,7 @@ -#ifndef RCLI_WINDOWS_PROTO_COMPAT_H -#define RCLI_WINDOWS_PROTO_COMPAT_H +#ifndef WALLY_WINDOWS_PROTO_COMPAT_H +#define WALLY_WINDOWS_PROTO_COMPAT_H -// Force-included ahead of every rcli translation unit on Windows (see the CLI +// Force-included ahead of every wally translation unit on Windows (see the CLI // CMakeLists). — pulled in via by Abseil/protobuf headers // the CLI transitively includes — defines ERROR_SEVERITY_WARNING and // ERROR_SEVERITY_ERROR as preprocessor macros. Those clobber the identically @@ -25,4 +25,4 @@ #undef ERROR_SEVERITY_ERROR #endif // _WIN32 -#endif // RCLI_WINDOWS_PROTO_COMPAT_H +#endif // WALLY_WINDOWS_PROTO_COMPAT_H diff --git a/swift/Package.resolved b/swift/Package.resolved index be2a205..8367b6d 100644 --- a/swift/Package.resolved +++ b/swift/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "9ec40429b727a2dbc9dfa215154f7b4eaebe18efe98501d496078383a93b3a98", + "originHash" : "09188216ea12a84d6b8fd68c13caba30ed63ab44062bf5e249c37b478eb6af7c", "pins" : [ { "identity" : "devicekit", @@ -55,13 +55,22 @@ "version" : "3.31.5" } }, + { + "identity" : "runanywhere-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/RunanywhereAI/runanywhere-swift.git", + "state" : { + "revision" : "3626ecd3db1d33aa3411895bc80f87fc821eaa1e", + "version" : "0.20.25" + } + }, { "identity" : "swift-asn1", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-asn1.git", "state" : { - "revision" : "a9a5efd40eaf558a2bcd48d64b1d1646be686008", - "version" : "1.7.1" + "revision" : "d9a5b37470adc940d22c3bcd5ca6953a516b727f", + "version" : "1.7.2" } }, { @@ -87,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/huggingface/swift-huggingface.git", "state" : { - "revision" : "b721959445b617d0bf03910b2b4aced345fd93bf", - "version" : "0.9.0" + "revision" : "9ebe9cb568ff9ebe2f1bcd3ae7d925fbb45ec8d2", + "version" : "0.10.0" } }, { @@ -132,8 +141,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/huggingface/swift-transformers", "state" : { - "revision" : "2fa33e1f5e7131a7fc64c28e6d161dcec0d24820", - "version" : "1.3.3" + "revision" : "c21fdcde390313a6d98d8e33a346f2c3486c3ab0", + "version" : "1.3.4" } }, { diff --git a/swift/Package.swift b/swift/Package.swift index 30d1be5..436240a 100644 --- a/swift/Package.swift +++ b/swift/Package.swift @@ -2,50 +2,50 @@ import PackageDescription import Foundation -// The Apple build of rcli. +// The Apple build of wally. // // MLX inference is written in Swift; the MLX engine inside commons is only a // table of callbacks. A pure C++ binary therefore cannot run MLX. This package -// registers the callbacks and then calls `rcli_run_main`, the same entry the -// CMake `rcli-cxx` binary uses. +// registers the callbacks and then calls `wally_run_main`, the same entry the +// CMake `wally-cxx` binary uses. // // SwiftPM owns the final link so MLX Metal shader bundles land beside the // executable. The C++ objects arrive as linker flags from -// `scripts/build-mlx.sh` (merged archive + system libs), not as a path listed +// `scripts/build/build-mlx.sh` (merged archive + system libs), not as a path listed // here — SwiftPM caches the manifest and would not notice a rebuilt archive. // // The Swift MLX runtime is a product of the SDK. Independent clones pin the -// published Swift distribution. In this monorepo, set RCLI_SDK_SWIFT_PATH to +// published Swift distribution. In this monorepo, set WALLY_SDK_SWIFT_PATH to // the SDK root (the directory with Package.swift). let mlxPackage: Package.Dependency = { - if let local = Context.environment["RCLI_SDK_SWIFT_PATH"], !local.isEmpty { + if let local = Context.environment["WALLY_SDK_SWIFT_PATH"], !local.isEmpty { return .package(path: local) } return .package(url: "https://github.com/RunanywhereAI/runanywhere-swift.git", exact: "0.20.25") }() let mlxPackageName: String = { - if let local = Context.environment["RCLI_SDK_SWIFT_PATH"], !local.isEmpty { + if let local = Context.environment["WALLY_SDK_SWIFT_PATH"], !local.isEmpty { // SwiftPM identity for a path dependency is the directory name, not // Package.swift's `name:`. Canonicalize so `/foo/bar/../..` is not `..`. return URL(fileURLWithPath: local).standardizedFileURL.lastPathComponent } return "runanywhere-swift" }() -let mlxProductName = Context.environment["RCLI_SDK_SWIFT_PATH"].map { _ in "RunAnywhereMLXRuntime" } +let mlxProductName = Context.environment["WALLY_SDK_SWIFT_PATH"].map { _ in "RunAnywhereMLXRuntime" } ?? "RunAnywhereMLX" let package = Package( - name: "rcli-mlx", + name: "wally-mlx", platforms: [.macOS("14.5")], dependencies: [mlxPackage], targets: [ - .target(name: "CRCLIApp"), + .target(name: "CWallyApp"), .executableTarget( - name: "RCLIMLX", + name: "WallyMLX", dependencies: [ - "CRCLIApp", + "CWallyApp", .product(name: mlxProductName, package: mlxPackageName), ] ), diff --git a/swift/Sources/CRCLIApp/include/app.h b/swift/Sources/CRCLIApp/include/app.h deleted file mode 100644 index 78c08ff..0000000 --- a/swift/Sources/CRCLIApp/include/app.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef RCLI_APP_H -#define RCLI_APP_H - -#ifdef __cplusplus -extern "C" { -#endif - -/// Same entry the C++ `rcli-cxx` binary uses. The Swift MLX host registers -/// MLX callbacks, then calls this so there is one application, two ways in. -int rcli_run_main(int argc, char** argv); - -#ifdef __cplusplus -} -#endif - -#endif // RCLI_APP_H diff --git a/swift/Sources/CWallyApp/include/app.h b/swift/Sources/CWallyApp/include/app.h new file mode 100644 index 0000000..fafb3a8 --- /dev/null +++ b/swift/Sources/CWallyApp/include/app.h @@ -0,0 +1,22 @@ +#ifndef WALLY_APP_H +#define WALLY_APP_H + +#ifdef __cplusplus +extern "C" { +#endif + +/// Lowers the SDK's log level before MLX.register() runs, which logs its own +/// INFO lines through the same logger. Call this first; wally_run_main() +/// lowers it again for entries that skip this call, so the order here only +/// matters for silencing MLX's own registration lines. +void wally_quiet_sdk_logging(void); + +/// Same entry the C++ `wally-cxx` binary uses. The Swift MLX host registers +/// MLX callbacks, then calls this so there is one application, two ways in. +int wally_run_main(int argc, char** argv); + +#ifdef __cplusplus +} +#endif + +#endif // WALLY_APP_H diff --git a/swift/Sources/CRCLIApp/metal_anchor.m b/swift/Sources/CWallyApp/metal_anchor.m similarity index 100% rename from swift/Sources/CRCLIApp/metal_anchor.m rename to swift/Sources/CWallyApp/metal_anchor.m diff --git a/swift/Sources/CRCLIApp/shim.c b/swift/Sources/CWallyApp/shim.c similarity index 100% rename from swift/Sources/CRCLIApp/shim.c rename to swift/Sources/CWallyApp/shim.c diff --git a/swift/Sources/RCLIMLX/RCLIMLX.swift b/swift/Sources/RCLIMLX/RCLIMLX.swift deleted file mode 100644 index e2408d3..0000000 --- a/swift/Sources/RCLIMLX/RCLIMLX.swift +++ /dev/null @@ -1,24 +0,0 @@ -import CRCLIApp -import Darwin -import Foundation -import MLXRuntime - -/// Product `rcli` on Apple: install MLX callbacks, then the same C++ CLI -/// (`rcli_run_main`) that Windows runs as `rcli.exe`. One command surface. -@main -struct RCLIMLX { - static func main() { - var arguments = CommandLine.arguments.map { strdup($0) } - defer { arguments.forEach { free($0) } } - - if !MLX.register() { - FileHandle.standardError.write( - Data("rcli: MLX callbacks did not register; other engines still available\n".utf8)) - } - - let status = arguments.withUnsafeMutableBufferPointer { buffer in - rcli_run_main(Int32(buffer.count), buffer.baseAddress) - } - Darwin.exit(status) - } -} diff --git a/swift/Sources/WallyMLX/WallyMLX.swift b/swift/Sources/WallyMLX/WallyMLX.swift new file mode 100644 index 0000000..248d2bb --- /dev/null +++ b/swift/Sources/WallyMLX/WallyMLX.swift @@ -0,0 +1,55 @@ +import CWallyApp +import Darwin +import Foundation +import MLXRuntime + +/// MLXRuntime logs its own registration result through `os.Logger` +/// (runanywhere-sdks/bindings/swift/Sources/MLXRuntime/MLX.swift), a +/// different logging system in a different repo that +/// `wally_quiet_sdk_logging()` has no reach into -- confirmed live: neither +/// `-q` nor `--json` touch it either, and OS_ACTIVITY_MODE=disable doesn't +/// on this OS version. Muting stderr for just this call is the only handle +/// wally has on it. Safe to lose whatever else prints in that window: the +/// call's own return value is what the caller below actually acts on. +@MainActor +func registerMLXQuietly() -> Bool { + let saved = dup(STDERR_FILENO) + let devNull = open("/dev/null", O_WRONLY) + if saved >= 0, devNull >= 0 { + dup2(devNull, STDERR_FILENO) + } + defer { + if saved >= 0 { + dup2(saved, STDERR_FILENO) + close(saved) + } + if devNull >= 0 { + close(devNull) + } + } + return MLX.register() +} + +/// Product `wally` on Apple: install MLX callbacks, then the same C++ CLI +/// (`wally_run_main`) that Windows runs as `wally.exe`. One command surface. +@main +struct WallyMLX { + static func main() { + var arguments = CommandLine.arguments.map { strdup($0) } + defer { arguments.forEach { free($0) } } + + // Ahead of MLX.register(): it logs through the same SDK logger this + // silences, and wally_run_main() below only mutes what comes after it. + wally_quiet_sdk_logging() + + if !registerMLXQuietly() { + FileHandle.standardError.write( + Data("wally: MLX callbacks did not register; other engines still available\n".utf8)) + } + + let status = arguments.withUnsafeMutableBufferPointer { buffer in + wally_run_main(Int32(buffer.count), buffer.baseAddress) + } + Darwin.exit(status) + } +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a3b4fb3..98cf969 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,59 +1,67 @@ -# CLI parse/REPL/output tests. Proto headers come from the kit via rcli_core. +# CLI parse/REPL/output tests. Proto headers come from the kit via wally_core. enable_testing() -add_executable(test_rcli_unit test_rcli_unit.cpp) -target_include_directories(test_rcli_unit PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") -target_link_libraries(test_rcli_unit PRIVATE rcli_core) -rcli_stage_windows_runtime_dlls(test_rcli_unit) -add_test(NAME rcli_unit_tests COMMAND test_rcli_unit --run-all) - -add_executable(test_rcli_account test_rcli_account.cpp) -target_include_directories(test_rcli_account PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") -target_link_libraries(test_rcli_account PRIVATE rcli_core nlohmann_json::nlohmann_json) -rcli_stage_windows_runtime_dlls(test_rcli_account) -add_test(NAME rcli_account_tests COMMAND test_rcli_account --run-all) - -add_executable(test_rcli_opencode test_rcli_opencode.cpp) -target_include_directories(test_rcli_opencode PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") -target_link_libraries(test_rcli_opencode PRIVATE rcli_core nlohmann_json::nlohmann_json) -rcli_stage_windows_runtime_dlls(test_rcli_opencode) -add_test(NAME rcli_opencode_tests COMMAND test_rcli_opencode --run-all) - -add_executable(test_rcli_harness test_rcli_harness.cpp) -target_include_directories(test_rcli_harness PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") -target_link_libraries(test_rcli_harness PRIVATE rcli_core nlohmann_json::nlohmann_json) -rcli_stage_windows_runtime_dlls(test_rcli_harness) -add_test(NAME rcli_harness_tests COMMAND test_rcli_harness --run-all) +add_executable(test_wally_unit test_wally_unit.cpp) +target_include_directories(test_wally_unit PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(test_wally_unit PRIVATE wally_core) +wally_stage_windows_runtime_dlls(test_wally_unit) +add_test(NAME wally_unit_tests COMMAND test_wally_unit --run-all) + +add_executable(test_wally_account test_wally_account.cpp) +target_include_directories(test_wally_account PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(test_wally_account PRIVATE wally_core nlohmann_json::nlohmann_json) +wally_stage_windows_runtime_dlls(test_wally_account) +add_test(NAME wally_account_tests COMMAND test_wally_account --run-all) + +add_executable(test_wally_contract test_wally_contract.cpp) +target_include_directories(test_wally_contract PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_compile_definitions(test_wally_contract PRIVATE + "WALLY_CONTRACT_PATH=\"${CMAKE_CURRENT_SOURCE_DIR}/../contracts/wally-cli-v1.openapi.json\"") +target_link_libraries(test_wally_contract PRIVATE wally_core nlohmann_json::nlohmann_json) +wally_stage_windows_runtime_dlls(test_wally_contract) +add_test(NAME wally_contract_tests COMMAND test_wally_contract --run-all) + +add_executable(test_wally_opencode test_wally_opencode.cpp) +target_include_directories(test_wally_opencode PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(test_wally_opencode PRIVATE wally_core nlohmann_json::nlohmann_json) +wally_stage_windows_runtime_dlls(test_wally_opencode) +add_test(NAME wally_opencode_tests COMMAND test_wally_opencode --run-all) + +add_executable(test_wally_harness test_wally_harness.cpp) +target_include_directories(test_wally_harness PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(test_wally_harness PRIVATE wally_core nlohmann_json::nlohmann_json) +wally_stage_windows_runtime_dlls(test_wally_harness) +add_test(NAME wally_harness_tests COMMAND test_wally_harness --run-all) find_package(Python3 REQUIRED COMPONENTS Interpreter) add_test( - NAME rcli_account_cli_e2e + NAME wally_account_cli_e2e COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/test_account_cli.py" - "$") -# 20s was too tight and failed on the macOS runner: this launches the rcli + "$") +# 20s was too tight and failed on the macOS runner: this launches the wally # binary three times (login, whoami, logout) and each launch initialises the # whole SDK — the MLX e2e test alone takes ~10s on the same machine. The budget # is per-test, not per-command, and the individual commands are already bounded # by the 15s subprocess timeout inside test_account_cli.py, so a real hang is # still caught there rather than here. -set_tests_properties(rcli_account_cli_e2e PROPERTIES TIMEOUT 120) +set_tests_properties(wally_account_cli_e2e PROPERTIES TIMEOUT 120) -add_executable(test_rcli_segment test_rcli_segment.cpp) -target_include_directories(test_rcli_segment PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") -target_link_libraries(test_rcli_segment PRIVATE rcli_core) -rcli_stage_windows_runtime_dlls(test_rcli_segment) -add_test(NAME rcli_segment_tests COMMAND test_rcli_segment --run-all) +add_executable(test_wally_segment test_wally_segment.cpp) +target_include_directories(test_wally_segment PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(test_wally_segment PRIVATE wally_core) +wally_stage_windows_runtime_dlls(test_wally_segment) +add_test(NAME wally_segment_tests COMMAND test_wally_segment --run-all) -add_executable(test_rcli_telemetry_live test_rcli_telemetry_live.cpp) -target_include_directories(test_rcli_telemetry_live PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") -target_link_libraries(test_rcli_telemetry_live PRIVATE rcli_core) -rcli_stage_windows_runtime_dlls(test_rcli_telemetry_live) -add_test(NAME rcli_telemetry_live_tests COMMAND test_rcli_telemetry_live) +add_executable(test_wally_telemetry_live test_wally_telemetry_live.cpp) +target_include_directories(test_wally_telemetry_live PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(test_wally_telemetry_live PRIVATE wally_core) +wally_stage_windows_runtime_dlls(test_wally_telemetry_live) +add_test(NAME wally_telemetry_live_tests COMMAND test_wally_telemetry_live) if(APPLE AND RunAnywhere_HAS_MLX) - add_executable(test_rcli_mlx_e2e test_rcli_mlx_e2e.cpp) - target_include_directories(test_rcli_mlx_e2e PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") - target_link_libraries(test_rcli_mlx_e2e PRIVATE rcli_core) - rcli_stage_windows_runtime_dlls(test_rcli_mlx_e2e) - add_test(NAME rcli_mlx_e2e_tests COMMAND test_rcli_mlx_e2e --run-all) + add_executable(test_wally_mlx_e2e test_wally_mlx_e2e.cpp) + target_include_directories(test_wally_mlx_e2e PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") + target_link_libraries(test_wally_mlx_e2e PRIVATE wally_core) + wally_stage_windows_runtime_dlls(test_wally_mlx_e2e) + add_test(NAME wally_mlx_e2e_tests COMMAND test_wally_mlx_e2e --run-all) endif() diff --git a/tests/test_account_cli.py b/tests/test_account_cli.py index bace9e6..62c9689 100644 --- a/tests/test_account_cli.py +++ b/tests/test_account_cli.py @@ -20,7 +20,7 @@ # is 0 in the 24h window on purpose: SGLang does not report cached tokens for # glm-5.3, so zero is what a real console sends today and the row has to survive # it honestly rather than disappear. `timeline`, `models` and `recent` are -# present because the console sends them; `rcli usage` ignores all three. +# present because the console sends them; `wally usage` ignores all three. USAGE_WINDOWS = [ { "window": "1h", @@ -157,7 +157,7 @@ def run(binary, arguments, environment): def main(): if len(sys.argv) != 2: - raise SystemExit("usage: test_account_cli.py /path/to/rcli") + raise SystemExit("usage: test_account_cli.py /path/to/wally") binary = sys.argv[1] server = ThreadingHTTPServer(("127.0.0.1", 0), ConsoleHandler) ConsoleHandler.console_origin = f"http://127.0.0.1:{server.server_port}" @@ -165,10 +165,10 @@ def main(): thread.start() try: - with tempfile.TemporaryDirectory(prefix="rcli-account-e2e-") as profile: + with tempfile.TemporaryDirectory(prefix="wally-account-e2e-") as profile: environment = os.environ.copy() - environment["RCLI_PROFILE_DIR"] = profile - environment["RCLI_CONSOLE_URL"] = ConsoleHandler.console_origin + environment["WALLY_PROFILE_DIR"] = profile + environment["WALLY_CONSOLE_URL"] = ConsoleHandler.console_origin for name in ( "RUNANYWHERE_API_KEY", "RUNANYWHERE_API_SECRET", @@ -219,7 +219,7 @@ def main(): raise AssertionError(f"usage still prints {banned!r}:\n{usage}") # The root flag and the command flag mean the same thing. The root - # parser accepts `rcli --json usage`, and reading only the local + # parser accepts `wally --json usage`, and reading only the local # flag printed a human table to something asking for one document. for argv in (["usage", "--json"], ["--json", "usage"]): combined = run(binary, argv, environment) diff --git a/tests/test_release_assets.py b/tests/test_release_assets.py index c1b2634..a5f52b8 100644 --- a/tests/test_release_assets.py +++ b/tests/test_release_assets.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Hermetic tests for scripts/verify-release-assets.py.""" +"""Hermetic tests for scripts/release/verify-release-assets.py.""" from __future__ import annotations @@ -16,7 +16,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] SPEC = importlib.util.spec_from_file_location( - "verify_release_assets", ROOT / "scripts" / "verify-release-assets.py" + "verify_release_assets", ROOT / "scripts" / "release" / "verify-release-assets.py" ) assert SPEC is not None and SPEC.loader is not None VERIFY = importlib.util.module_from_spec(SPEC) @@ -38,45 +38,45 @@ def add_tar_file(self, bundle: tarfile.TarFile, name: str, contents: bytes, mode def test_valid_macos_archive(self) -> None: with tempfile.TemporaryDirectory() as temporary: - archive = pathlib.Path(temporary) / "rcli-1.2.3-macos-arm64.tar.gz" + archive = pathlib.Path(temporary) / "wally-1.2.3-macos-arm64.tar.gz" with tarfile.open(archive, "w:gz") as bundle: - self.add_tar_file(bundle, "rcli-macos-arm64/README.md", b"readme", 0o644) - self.add_tar_file(bundle, "rcli-macos-arm64/bin/rcli", b"binary", 0o755) + self.add_tar_file(bundle, "wally-macos-arm64/README.md", b"readme", 0o644) + self.add_tar_file(bundle, "wally-macos-arm64/bin/wally", b"binary", 0o755) VERIFY.verify(archive, self.sidecar(archive)) def test_valid_windows_archive_with_backslash_members(self) -> None: with tempfile.TemporaryDirectory() as temporary: - archive = pathlib.Path(temporary) / "rcli-1.2.3-windows-x86_64.zip" + archive = pathlib.Path(temporary) / "wally-1.2.3-windows-x86_64.zip" with zipfile.ZipFile(archive, "w") as bundle: - bundle.writestr("rcli-windows-x86_64\\README.md", b"readme") - bundle.writestr("rcli-windows-x86_64\\bin\\rcli.exe", b"binary") + bundle.writestr("wally-windows-x86_64\\README.md", b"readme") + bundle.writestr("wally-windows-x86_64\\bin\\wally.exe", b"binary") VERIFY.verify(archive, self.sidecar(archive)) def test_rejects_traversal(self) -> None: with tempfile.TemporaryDirectory() as temporary: - archive = pathlib.Path(temporary) / "rcli-1.2.3-windows-x86_64.zip" + archive = pathlib.Path(temporary) / "wally-1.2.3-windows-x86_64.zip" with zipfile.ZipFile(archive, "w") as bundle: - bundle.writestr("rcli-windows-x86_64/README.md", b"readme") - bundle.writestr("rcli-windows-x86_64/bin/rcli.exe", b"binary") - bundle.writestr("rcli-windows-x86_64/../outside", b"bad") + bundle.writestr("wally-windows-x86_64/README.md", b"readme") + bundle.writestr("wally-windows-x86_64/bin/wally.exe", b"binary") + bundle.writestr("wally-windows-x86_64/../outside", b"bad") with self.assertRaisesRegex(VERIFY.VerificationError, "unsafe archive member"): VERIFY.verify(archive, self.sidecar(archive)) def test_rejects_wrong_checksum_filename(self) -> None: with tempfile.TemporaryDirectory() as temporary: - archive = pathlib.Path(temporary) / "rcli-1.2.3-macos-arm64.tar.gz" + archive = pathlib.Path(temporary) / "wally-1.2.3-macos-arm64.tar.gz" with tarfile.open(archive, "w:gz") as bundle: - self.add_tar_file(bundle, "rcli-macos-arm64/README.md", b"readme", 0o644) - self.add_tar_file(bundle, "rcli-macos-arm64/bin/rcli", b"binary", 0o755) + self.add_tar_file(bundle, "wally-macos-arm64/README.md", b"readme", 0o644) + self.add_tar_file(bundle, "wally-macos-arm64/bin/wally", b"binary", 0o755) with self.assertRaisesRegex(VERIFY.VerificationError, "sidecar names"): VERIFY.verify(archive, self.sidecar(archive, filename="different.tar.gz")) def test_rejects_non_executable_macos_binary(self) -> None: with tempfile.TemporaryDirectory() as temporary: - archive = pathlib.Path(temporary) / "rcli-1.2.3-macos-arm64.tar.gz" + archive = pathlib.Path(temporary) / "wally-1.2.3-macos-arm64.tar.gz" with tarfile.open(archive, "w:gz") as bundle: - self.add_tar_file(bundle, "rcli-macos-arm64/README.md", b"readme", 0o644) - self.add_tar_file(bundle, "rcli-macos-arm64/bin/rcli", b"binary", 0o644) + self.add_tar_file(bundle, "wally-macos-arm64/README.md", b"readme", 0o644) + self.add_tar_file(bundle, "wally-macos-arm64/bin/wally", b"binary", 0o644) with self.assertRaisesRegex(VERIFY.VerificationError, "executable mode bit"): VERIFY.verify(archive, self.sidecar(archive)) diff --git a/tests/test_rcli_account.cpp b/tests/test_wally_account.cpp similarity index 73% rename from tests/test_rcli_account.cpp rename to tests/test_wally_account.cpp index 410d39f..96d0083 100644 --- a/tests/test_rcli_account.cpp +++ b/tests/test_wally_account.cpp @@ -57,7 +57,7 @@ class TempDirectory { public: TempDirectory() { const auto nonce = std::chrono::steady_clock::now().time_since_epoch().count(); - path_ = fs::temp_directory_path() / ("rcli-account-test-" + std::to_string(nonce)); + path_ = fs::temp_directory_path() / ("wally-account-test-" + std::to_string(nonce)); fs::create_directories(path_); } ~TempDirectory() { @@ -84,11 +84,15 @@ TestResult test_console_url_validation() { {"http://localhost:8080", "http://localhost:8080"}, {"http://127.0.0.1:8002", "http://127.0.0.1:8002"}, {"http://[::1]:9000", "http://[::1]:9000"}, + // Development is a path prefix on the production host, not its own one. + {"https://inference.runanywhere.ai/api-dev", "https://inference.runanywhere.ai/api-dev"}, + {"https://inference.runanywhere.ai/api-dev/", "https://inference.runanywhere.ai/api-dev"}, + {"http://localhost:8080/api-dev", "http://localhost:8080/api-dev"}, }; for (const Accepted& test : accepted) { std::string normalized; std::string error; - if (!rcli::account::NormalizeConsoleUrl(test.input, &normalized, &error) || + if (!wally::account::NormalizeConsoleUrl(test.input, &normalized, &error) || normalized != test.normalized) { result.details = std::string("rejected safe origin: ") + test.input + " " + error; result.expected = test.normalized; @@ -104,8 +108,12 @@ TestResult test_console_url_validation() { "http://[::1].evil.example", "http://localhost@evil.example", "https://user:password@console.runanywhere.ai", - "https://console.runanywhere.ai/path", "https://console.runanywhere.ai?query=1", + "https://console.runanywhere.ai/api-dev?query=1", + "https://console.runanywhere.ai/api-dev#fragment", + "https://console.runanywhere.ai//api-dev", + "https://console.runanywhere.ai/../api-dev", + "https://console.runanywhere.ai/api/../../dev", "https://console.runanywhere.ai:", "http://[::1]:", "https://", @@ -114,19 +122,24 @@ TestResult test_console_url_validation() { for (const char* input : rejected) { std::string normalized; std::string error; - if (rcli::account::NormalizeConsoleUrl(input, &normalized, &error)) { + if (wally::account::NormalizeConsoleUrl(input, &normalized, &error)) { result.details = std::string("accepted unsafe origin: ") + input; return result; } } - if (!rcli::account::BrowserUrlIsSafe("https://console.runanywhere.ai/device?code=ABCD-EFGH") || - !rcli::account::BrowserUrlIsSafe("http://localhost:8080/device?code=ABCD") || - rcli::account::BrowserUrlIsSafe("http://localhost.evil.example/device") || - !rcli::account::BrowserUrlMatchesConsole( + if (!wally::account::BrowserUrlIsSafe("https://console.runanywhere.ai/device?code=ABCD-EFGH") || + !wally::account::BrowserUrlIsSafe("http://localhost:8080/device?code=ABCD") || + wally::account::BrowserUrlIsSafe("http://localhost.evil.example/device") || + !wally::account::BrowserUrlMatchesConsole( "https://console.runanywhere.ai/device?code=ABCD-EFGH", "https://console.runanywhere.ai") || - rcli::account::BrowserUrlMatchesConsole("https://auth.attacker.example/device", + !wally::account::BrowserUrlMatchesConsole( + "https://inference.runanywhere.ai/cloud/cli?code=ABCD", + "https://inference.runanywhere.ai/api-dev") || + wally::account::BrowserUrlMatchesConsole("https://auth.attacker.example/device", + "https://inference.runanywhere.ai/api-dev") || + wally::account::BrowserUrlMatchesConsole("https://auth.attacker.example/device", "https://console.runanywhere.ai")) { result.details = "browser URL policy does not match the origin policy"; return result; @@ -139,10 +152,10 @@ TestResult test_credential_roundtrip_and_permissions() { TestResult result; result.test_name = "credential_roundtrip_and_permissions"; TempDirectory temporary; - EnvVar profile("RCLI_PROFILE_DIR", temporary.path().string().c_str()); - EnvVar console("RCLI_CONSOLE_URL", nullptr); + EnvVar profile("WALLY_PROFILE_DIR", temporary.path().string().c_str()); + EnvVar console("WALLY_CONSOLE_URL", nullptr); - rcli::account::Credentials expected; + wally::account::Credentials expected; expected.console_url = "https://CONSOLE.RUNANYWHERE.AI/"; expected.email = "dev+\"json\"@example.test"; expected.access_token = "access-secret-that-must-not-be-logged"; @@ -150,7 +163,7 @@ TestResult test_credential_roundtrip_and_permissions() { expected.expires_at = 123456789; std::string error; - if (!rcli::account::Save(expected, &error)) { + if (!wally::account::Save(expected, &error)) { result.details = error; return result; } @@ -159,15 +172,15 @@ TestResult test_credential_roundtrip_and_permissions() { struct stat directory{}; struct stat file{}; if (::stat(temporary.path().c_str(), &directory) != 0 || - ::stat(rcli::account::CredentialsPath().c_str(), &file) != 0 || + ::stat(wally::account::CredentialsPath().c_str(), &file) != 0 || (directory.st_mode & 0777) != 0700 || (file.st_mode & 0777) != 0600) { result.details = "credentials must be stored in mode 0700/0600"; return result; } #endif - rcli::account::Credentials actual; - if (!rcli::account::Load(&actual, &error)) { + wally::account::Credentials actual; + if (!wally::account::Load(&actual, &error)) { result.details = error; return result; } @@ -178,7 +191,7 @@ TestResult test_credential_roundtrip_and_permissions() { result.details = "credential JSON did not round-trip exactly"; return result; } - if (!rcli::account::Clear(&error) || fs::exists(rcli::account::CredentialsPath())) { + if (!wally::account::Clear(&error) || fs::exists(wally::account::CredentialsPath())) { result.details = error.empty() ? "credential file still exists after clear" : error; return result; } @@ -192,12 +205,12 @@ TestResult test_credentials_default_console_url_without_a_file() { TestResult result; result.test_name = "credentials_default_console_url_without_a_file"; TempDirectory temporary; - EnvVar profile("RCLI_PROFILE_DIR", temporary.path().string().c_str()); - EnvVar console("RCLI_CONSOLE_URL", nullptr); + EnvVar profile("WALLY_PROFILE_DIR", temporary.path().string().c_str()); + EnvVar console("WALLY_CONSOLE_URL", nullptr); - rcli::account::Credentials credentials; + wally::account::Credentials credentials; std::string error; - if (!rcli::account::Load(&credentials, &error)) { + if (!wally::account::Load(&credentials, &error)) { result.details = error.empty() ? "load failed with no session file present" : error; return result; } @@ -205,8 +218,8 @@ TestResult test_credentials_default_console_url_without_a_file() { // that the fallback happens at all; which host it lands on is pinned by // the_api_host_and_the_browser_host_stay_apart, and duplicating the string // here only bought two failures for one change. - if (credentials.console_url != rcli::account::DefaultConsoleUrl()) { - result.expected = rcli::account::DefaultConsoleUrl(); + if (credentials.console_url != wally::account::DefaultConsoleUrl()) { + result.expected = wally::account::DefaultConsoleUrl(); result.actual = credentials.console_url; result.details = "no session file must still give the default console origin"; return result; @@ -234,22 +247,22 @@ TestResult test_credentials_missing_console_url_falls_back() { TestResult result; result.test_name = "credentials_missing_console_url_falls_back"; TempDirectory temporary; - EnvVar profile("RCLI_PROFILE_DIR", temporary.path().string().c_str()); - EnvVar console("RCLI_CONSOLE_URL", nullptr); + EnvVar profile("WALLY_PROFILE_DIR", temporary.path().string().c_str()); + EnvVar console("WALLY_CONSOLE_URL", nullptr); - std::ofstream raw(rcli::account::CredentialsPath()); + std::ofstream raw(wally::account::CredentialsPath()); raw << Json{{"email", "dev@example.test"}, {"access_token", "a-token"}}.dump(); raw.close(); - ::chmod(rcli::account::CredentialsPath().c_str(), 0600); + ::chmod(wally::account::CredentialsPath().c_str(), 0600); - rcli::account::Credentials credentials; + wally::account::Credentials credentials; std::string error; - if (!rcli::account::Load(&credentials, &error)) { + if (!wally::account::Load(&credentials, &error)) { result.details = error.empty() ? "load failed on a document missing console_url" : error; return result; } - if (credentials.console_url != rcli::account::DefaultConsoleUrl()) { - result.expected = rcli::account::DefaultConsoleUrl(); + if (credentials.console_url != wally::account::DefaultConsoleUrl()) { + result.expected = wally::account::DefaultConsoleUrl(); result.actual = credentials.console_url; result.details = "a missing console_url must fall back to the default, not error"; return result; @@ -266,16 +279,16 @@ TestResult test_credentials_reject_a_document_they_cannot_unlock() { TestResult result; result.test_name = "credentials_reject_a_document_they_cannot_unlock"; TempDirectory temporary; - EnvVar profile("RCLI_PROFILE_DIR", temporary.path().string().c_str()); - EnvVar console("RCLI_CONSOLE_URL", nullptr); + EnvVar profile("WALLY_PROFILE_DIR", temporary.path().string().c_str()); + EnvVar console("WALLY_CONSOLE_URL", nullptr); - std::ofstream raw(rcli::account::CredentialsPath(), std::ios::binary); + std::ofstream raw(wally::account::CredentialsPath(), std::ios::binary); raw << Json{{"email", "dev@example.test"}, {"access_token", "a-token"}}.dump(); raw.close(); - rcli::account::Credentials credentials; + wally::account::Credentials credentials; std::string error; - if (rcli::account::Load(&credentials, &error)) { + if (wally::account::Load(&credentials, &error)) { result.details = "a session file that does not decrypt must not load"; return result; } @@ -300,20 +313,20 @@ TestResult test_credentials_warns_on_exposed_permissions() { TestResult result; result.test_name = "credentials_warns_on_exposed_permissions"; TempDirectory temporary; - EnvVar profile("RCLI_PROFILE_DIR", temporary.path().string().c_str()); - EnvVar console("RCLI_CONSOLE_URL", nullptr); + EnvVar profile("WALLY_PROFILE_DIR", temporary.path().string().c_str()); + EnvVar console("WALLY_CONSOLE_URL", nullptr); - rcli::account::Credentials seed; + wally::account::Credentials seed; seed.console_url = "https://console.runanywhere.ai"; seed.email = "dev@example.test"; seed.access_token = "a-token"; seed.expires_at = 0; std::string error; - if (!rcli::account::Save(seed, &error)) { + if (!wally::account::Save(seed, &error)) { result.details = error; return result; } - const std::string path = rcli::account::CredentialsPath(); + const std::string path = wally::account::CredentialsPath(); if (::chmod(path.c_str(), 0644) != 0) { result.details = "could not widen permissions for the test fixture"; return result; @@ -333,8 +346,8 @@ TestResult test_credentials_warns_on_exposed_permissions() { ::dup2(capture_fd, fileno(stderr)); ::close(capture_fd); - rcli::account::Credentials loaded; - const bool ok = rcli::account::Load(&loaded, &error); + wally::account::Credentials loaded; + const bool ok = wally::account::Load(&loaded, &error); std::fflush(stderr); ::dup2(saved_stderr, fileno(stderr)); @@ -368,10 +381,10 @@ TestResult test_credentials_warns_on_exposed_permissions() { TestResult test_console_client_contract() { TestResult result; result.test_name = "console_client_contract"; - std::vector requests; + std::vector requests; int polls = 0; - rcli::account::Transport transport = [&](const rcli::account::HttpRequest& request, - rcli::account::HttpResponse* response, std::string*) { + wally::account::Transport transport = [&](const wally::account::HttpRequest& request, + wally::account::HttpResponse* response, std::string*) { requests.push_back(request); if (request.url.ends_with("/auth/cli/start")) { response->status = 200; @@ -408,31 +421,31 @@ TestResult test_console_client_contract() { return true; }; - rcli::account::ConsoleClient client(transport); + wally::account::ConsoleClient client(transport); std::string error; - rcli::account::Authorization authorization; + wally::account::Authorization authorization; if (!client.BeginAuthorization("https://console.runanywhere.ai", "test-host", &authorization, &error) || authorization.request_code != "ABCD-EFGH" || authorization.interval != 1) { result.details = error.empty() ? "authorization response mismatch" : error; return result; } - rcli::account::Grant grant; + wally::account::Grant grant; if (client.Poll("https://console.runanywhere.ai", authorization, &grant, &error) != - rcli::account::PollResult::Pending || + wally::account::PollResult::Pending || client.Poll("https://console.runanywhere.ai", authorization, &grant, &error) != - rcli::account::PollResult::Approved || + wally::account::PollResult::Approved || grant.access_token != "access-one" || grant.refresh_token != "refresh-one") { result.details = error.empty() ? "poll contract mismatch" : error; return result; } - rcli::account::Identity identity; + wally::account::Identity identity; if (client.WhoAmI("https://console.runanywhere.ai", grant.access_token, &identity, &error) != - rcli::account::IdentityResult::Ok) { + wally::account::IdentityResult::Ok) { result.details = error.empty() ? "identity contract mismatch" : error; return result; } - rcli::account::Grant refreshed; + wally::account::Grant refreshed; if (!client.Refresh("https://console.runanywhere.ai", grant.refresh_token, &refreshed, &error) || refreshed.access_token != "access-two" || refreshed.refresh_token != "refresh-two" || @@ -468,13 +481,13 @@ TestResult test_console_errors_do_not_echo_secrets() { TestResult result; result.test_name = "console_errors_do_not_echo_secrets"; const std::string secret = "access-secret-from-server"; - rcli::account::ConsoleClient client([&](const rcli::account::HttpRequest&, - rcli::account::HttpResponse* response, std::string*) { + wally::account::ConsoleClient client([&](const wally::account::HttpRequest&, + wally::account::HttpResponse* response, std::string*) { response->status = 500; response->body = Json{{"detail", secret}}.dump(); return true; }); - rcli::account::Authorization authorization; + wally::account::Authorization authorization; std::string error; if (client.BeginAuthorization("https://console.runanywhere.ai", "host", &authorization, &error) || @@ -483,8 +496,8 @@ TestResult test_console_errors_do_not_echo_secrets() { return result; } - rcli::account::ConsoleClient malformed([&](const rcli::account::HttpRequest&, - rcli::account::HttpResponse* response, + wally::account::ConsoleClient malformed([&](const wally::account::HttpRequest&, + wally::account::HttpResponse* response, std::string*) { response->status = 200; response->body = "{\"access_token\":\"" + secret; @@ -501,11 +514,70 @@ TestResult test_console_errors_do_not_echo_secrets() { return result; } +TestResult test_a_rate_limit_surfaces_its_retry_after() { + TestResult result; + result.test_name = "a_rate_limit_surfaces_its_retry_after"; + + // A 429 with a numeric Retry-After: the error a caller sees should name the + // wait in seconds, not just "HTTP 429". + wally::account::ConsoleClient with_hint([&](const wally::account::HttpRequest&, + wally::account::HttpResponse* response, std::string*) { + response->status = 429; + response->headers["retry-after"] = "30"; + response->body = "{}"; + return true; + }); + wally::account::Authorization authorization; + std::string error; + if (with_hint.BeginAuthorization("https://console.runanywhere.ai", "host", &authorization, + &error) || + error.find("retry after 30s") == std::string::npos) { + result.details = "a 429 with Retry-After did not surface the wait: " + error; + return result; + } + + // A 429 without the header still reads as a rate limit, just without a + // number, and a garbage value is treated as absent rather than echoed. + wally::account::ConsoleClient no_hint([&](const wally::account::HttpRequest&, + wally::account::HttpResponse* response, std::string*) { + response->status = 429; + response->headers["retry-after"] = "soon"; + response->body = "{}"; + return true; + }); + error.clear(); + if (no_hint.BeginAuthorization("https://console.runanywhere.ai", "host", &authorization, + &error) || + error.find("rate limiting") == std::string::npos || + error.find("soon") != std::string::npos) { + result.details = "a 429 with a non-numeric Retry-After was mishandled: " + error; + return result; + } + + // The parser itself: valid, absent, non-numeric, and the day ceiling. + wally::account::HttpResponse r; + r.headers["retry-after"] = "45"; + const bool ok = r.retry_after_seconds() == 45; + r.headers.clear(); + const bool absent = r.retry_after_seconds() == -1; + r.headers["retry-after"] = "-5"; + const bool negative = r.retry_after_seconds() == -1; + r.headers["retry-after"] = "999999"; + const bool capped = r.retry_after_seconds() == 86400; + if (!ok || !absent || !negative || !capped) { + result.details = "retry_after_seconds parsed a value wrong"; + return result; + } + + result.passed = true; + return result; +} + TestResult test_console_rejects_header_injection() { TestResult result; result.test_name = "console_rejects_header_injection"; - rcli::account::ConsoleClient client([](const rcli::account::HttpRequest& request, - rcli::account::HttpResponse* response, std::string*) { + wally::account::ConsoleClient client([](const wally::account::HttpRequest& request, + wally::account::HttpResponse* response, std::string*) { response->status = 200; if (request.url.ends_with("/auth/cli/poll")) { response->body = Json{{"status", "approved"}, @@ -522,7 +594,7 @@ TestResult test_console_rejects_header_injection() { }); std::string error; - rcli::account::Authorization authorization; + wally::account::Authorization authorization; if (client.BeginAuthorization("https://console.runanywhere.ai", "host", &authorization, &error) || error != "console returned an invalid authorization request") { @@ -532,10 +604,10 @@ TestResult test_console_rejects_header_injection() { authorization.request_code = "ABCD-EFGH"; authorization.poll_secret = "poll-secret"; - rcli::account::Grant grant; + wally::account::Grant grant; error.clear(); if (client.Poll("https://console.runanywhere.ai", authorization, &grant, &error) != - rcli::account::PollResult::Failed || + wally::account::PollResult::Failed || error != "console returned an invalid cloud session") { result.details = "HTTP header control characters were accepted in an access token"; return result; @@ -553,11 +625,11 @@ TestResult test_console_rejects_header_injection() { TestResult test_the_api_host_and_the_browser_host_stay_apart() { TestResult result; result.test_name = "the_api_host_and_the_browser_host_stay_apart"; - EnvVar console("RCLI_CONSOLE_URL", nullptr); - EnvVar web("RCLI_CONSOLE_WEB_URL", nullptr); + EnvVar console("WALLY_CONSOLE_URL", nullptr); + EnvVar web("WALLY_CONSOLE_WEB_URL", nullptr); - const std::string api = rcli::account::DefaultConsoleUrl(); - const std::vector browser = rcli::account::TrustedBrowserOrigins(api); + const std::string api = wally::account::DefaultConsoleUrl(); + const std::vector browser = wally::account::TrustedBrowserOrigins(api); if (api == "https://console.runanywhere.ai") { result.details = "the API default is the web console, which serves no /auth/cli or /v1 route"; @@ -578,17 +650,17 @@ TestResult test_the_api_host_and_the_browser_host_stay_apart() { // The origin the control plane actually puts in verification_url today. Drop // this once that config names the custom domain; until then, removing it // refuses every production sign-in. - if (!rcli::account::BrowserUrlIsTrusted( + if (!wally::account::BrowserUrlIsTrusted( "https://runanywhere-frontend-production.up.railway.app/cloud/cli?code=abc", browser)) { result.details = "production's own approval URL must pass the origin check"; return result; } - if (!rcli::account::BrowserUrlIsTrusted("https://console.runanywhere.ai/cloud/cli?code=abc", + if (!wally::account::BrowserUrlIsTrusted("https://console.runanywhere.ai/cloud/cli?code=abc", browser)) { result.details = "the console's custom domain must pass the origin check"; return result; } - if (rcli::account::BrowserUrlIsTrusted("https://console.runanywhere.ai.evil.test/cloud/cli", + if (wally::account::BrowserUrlIsTrusted("https://console.runanywhere.ai.evil.test/cloud/cli", browser)) { result.details = "a lookalike host must not pass on a prefix match"; return result; @@ -598,38 +670,38 @@ TestResult test_the_api_host_and_the_browser_host_stay_apart() { } // The pinning only does something if it is on by default. Before this, the -// trusted origin came from RCLI_CONSOLE_WEB_URL alone — unset in every shipped +// trusted origin came from WALLY_CONSOLE_WEB_URL alone — unset in every shipped // install — so the check ran against an empty string and took whatever origin // the server put in verification_url. TestResult test_the_trusted_browser_origin_is_never_empty() { TestResult result; result.test_name = "the_trusted_browser_origin_is_never_empty"; - EnvVar web("RCLI_CONSOLE_WEB_URL", nullptr); + EnvVar web("WALLY_CONSOLE_WEB_URL", nullptr); // An unknown console is trusted at its own origin and nowhere else, so a // dev or loopback console keeps working without widening what we accept. const std::vector loopback = - rcli::account::TrustedBrowserOrigins("http://127.0.0.1:8080"); + wally::account::TrustedBrowserOrigins("http://127.0.0.1:8080"); if (loopback.size() != 1 || loopback[0] != "http://127.0.0.1:8080") { result.details = "an unknown console must be trusted only at its own origin"; return result; } - if (rcli::account::TrustedBrowserOrigins("https://dev.example.test").empty()) { + if (wally::account::TrustedBrowserOrigins("https://dev.example.test").empty()) { result.details = "an empty trusted origin list pins nothing"; return result; } - if (rcli::account::BrowserUrlIsTrusted("https://console.runanywhere.ai/cloud/cli", loopback)) { + if (wally::account::BrowserUrlIsTrusted("https://console.runanywhere.ai/cloud/cli", loopback)) { result.details = "the production console must not be trusted for a dev API"; return result; } // A declared origin still wins, and replaces the pair rather than adding // to it, which is what points sign-in at a console served somewhere else. - EnvVar declared("RCLI_CONSOLE_WEB_URL", "https://console.dev.example.test"); + EnvVar declared("WALLY_CONSOLE_WEB_URL", "https://console.dev.example.test"); const std::vector overridden = - rcli::account::TrustedBrowserOrigins("https://inference.runanywhere.ai"); + wally::account::TrustedBrowserOrigins("https://inference.runanywhere.ai"); if (overridden.size() != 1 || overridden[0] != "https://console.dev.example.test") { - result.details = "a declared RCLI_CONSOLE_WEB_URL must outrank and replace the defaults"; + result.details = "a declared WALLY_CONSOLE_WEB_URL must outrank and replace the defaults"; return result; } result.passed = true; @@ -643,14 +715,14 @@ TestResult test_usage_counters_survive_beyond_thirty_two_bits() { TestResult result; result.test_name = "usage_counters_survive_beyond_thirty_two_bits"; TempDirectory temporary; - EnvVar profile("RCLI_PROFILE_DIR", temporary.path().string().c_str()); + EnvVar profile("WALLY_PROFILE_DIR", temporary.path().string().c_str()); constexpr std::int64_t kCost = 9'000'000'000; // $9,000, well past 2^31 constexpr std::int64_t kPrompt = 5'000'000'000; // more than INT32_MAX constexpr std::int64_t kBalance = 4'000'000'000; - rcli::account::ConsoleClient console( - [&](const rcli::account::HttpRequest& request, rcli::account::HttpResponse* response, + wally::account::ConsoleClient console( + [&](const wally::account::HttpRequest& request, wally::account::HttpResponse* response, std::string*) { if (request.url.find("/v1/cli/usage") == std::string::npos) { return false; @@ -669,11 +741,11 @@ TestResult test_usage_counters_survive_beyond_thirty_two_bits() { return true; }); - rcli::account::Usage usage; + wally::account::Usage usage; std::string error; - const rcli::account::IdentityResult status = console.FetchUsage( - "https://console.example.test", "a-token", rcli::account::UsageQuery{}, &usage, &error); - if (status != rcli::account::IdentityResult::Ok) { + const wally::account::IdentityResult status = console.FetchUsage( + "https://console.example.test", "a-token", wally::account::UsageQuery{}, &usage, &error); + if (status != wally::account::IdentityResult::Ok) { result.details = error.empty() ? "usage request failed" : error; return result; } @@ -695,7 +767,7 @@ TestResult test_usage_counters_survive_beyond_thirty_two_bits() { } // namespace int main(int argc, char** argv) { - TestSuite suite("rcli_account"); + TestSuite suite("wally_account"); suite.add("console_url_validation", test_console_url_validation); suite.add("credential_roundtrip_and_permissions", test_credential_roundtrip_and_permissions); suite.add("credentials_default_console_url_without_a_file", @@ -711,6 +783,7 @@ int main(int argc, char** argv) { #endif suite.add("console_client_contract", test_console_client_contract); suite.add("console_errors_do_not_echo_secrets", test_console_errors_do_not_echo_secrets); + suite.add("a_rate_limit_surfaces_its_retry_after", test_a_rate_limit_surfaces_its_retry_after); suite.add("console_rejects_header_injection", test_console_rejects_header_injection); suite.add("the_api_host_and_the_browser_host_stay_apart", test_the_api_host_and_the_browser_host_stay_apart); diff --git a/tests/test_wally_contract.cpp b/tests/test_wally_contract.cpp new file mode 100644 index 0000000..e28675c --- /dev/null +++ b/tests/test_wally_contract.cpp @@ -0,0 +1,201 @@ +// The generated console binding is in lockstep with its pinned contract, and it +// round-trips the shapes the CLI actually sends and receives. + +#include "test_common.h" + +#include +#include +#include +#include +#include + +#include + +#include "account/console_contract.h" + +namespace { + +using Json = nlohmann::json; +namespace contract = wally::account::contract; + +// contracts/wally-cli-v1.openapi.json, relative to this repo. Located by +// walking up from the test binary is fragile, so the path is passed at compile +// time; see tests/CMakeLists.txt. +#ifndef WALLY_CONTRACT_PATH +#define WALLY_CONTRACT_PATH "" +#endif + +std::string Sha256Hex(const std::string& bytes); // small local impl below + +TestResult test_binding_matches_the_pinned_contract() { + TestResult result; + result.test_name = "binding_matches_the_pinned_contract"; + + const std::string path = WALLY_CONTRACT_PATH; + std::ifstream file(path, std::ios::binary); + if (!file) { + result.details = std::string("cannot open pinned contract at ") + path; + return result; + } + std::ostringstream buffer; + buffer << file.rdbuf(); + const std::string bytes = buffer.str(); + + // The header's pin must equal the SHA-256 of the artifact on disk. If they + // differ, someone edited one without regenerating the other. + const std::string digest = Sha256Hex(bytes); + if (digest != contract::kContractSha256) { + result.details = "contract hash drifted from the generated binding: disk=" + digest + + " header=" + std::string(contract::kContractSha256); + return result; + } + result.passed = true; + return result; +} + +TestResult test_request_and_response_round_trip() { + TestResult result; + result.test_name = "request_and_response_round_trip"; + + // A request serializes to exactly the fields the server expects. + contract::CliStartRequest start; + start.client = contract::CliClient::kRcli; + start.hostname = "Homes-MacBook-Pro.local"; + const Json start_json = start; + if (start_json.at("client") != "rcli" || start_json.at("hostname") != start.hostname || + start_json.size() != 2) { + result.details = "CliStartRequest did not serialize to the contract shape"; + return result; + } + + // A response with a nullable field absent leaves the optional empty; present + // fills it. PollResponse is the one with optionals. + const Json pending = Json{{"status", "pending"}}; + const auto poll_pending = pending.get(); + if (poll_pending.status != contract::PollStatus::kPending || + poll_pending.access_token.has_value()) { + result.details = "PollResponse mis-parsed the pending case"; + return result; + } + const Json approved = Json{{"status", "approved"}, + {"access_token", "sk-x"}, + {"email", "a@b.co"}, + {"expires_in", 3600}, + {"plan", "beta"}, + {"refresh_token", "r-x"}}; + const auto poll_approved = approved.get(); + if (poll_approved.status != contract::PollStatus::kApproved || + poll_approved.access_token.value_or("") != "sk-x" || + poll_approved.plan.value_or(contract::CliPlan::kBeta) != contract::CliPlan::kBeta) { + result.details = "PollResponse mis-parsed the approved case"; + return result; + } + + // An unknown enum value is a hard parse error, never a silent default. + bool threw = false; + try { + Json{{"status", "banana"}}.get(); + } catch (const Json::exception&) { + threw = true; + } + if (!threw) { + result.details = "an unknown PollStatus should have thrown"; + return result; + } + + // A nested response with arrays parses end to end. + const Json usage = Json{ + {"credit", {{"balance_micros", 1985000}, {"granted_micros", 2000000}, {"spent_micros", 15000}}}, + {"totals", {{"requests", 3}, {"prompt_tokens", 10}, {"completion_tokens", 5}, {"cached_tokens", 0}, {"cost_micros", 16}}}, + {"windows", Json::array({{{"window", "1h"}, {"seconds", 3600}, {"totals", {{"requests", 0}, {"prompt_tokens", 0}, {"completion_tokens", 0}, {"cached_tokens", 0}, {"cost_micros", 0}}}}})}, + {"timeline", Json::array()}, + {"models", Json::array()}, + {"recent", Json::array()}, + }; + const auto parsed = usage.get(); + if (parsed.credit.balance_micros != 1985000 || parsed.windows.size() != 1 || + parsed.windows[0].window != contract::CliUsageWindowLabel::k1h) { + result.details = "CliUsageResponse mis-parsed a nested body"; + return result; + } + result.passed = true; + return result; +} + +// A tiny, dependency-free SHA-256 so the test does not pull in a crypto lib. +std::string Sha256Hex(const std::string& message) { + auto rotr = [](std::uint32_t x, std::uint32_t n) { return (x >> n) | (x << (32 - n)); }; + static const std::uint32_t k[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, + 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, + 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, + 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, + 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, + 0xc67178f2}; + std::uint32_t h[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; + std::string data = message; + const std::uint64_t bit_length = static_cast(data.size()) * 8; + data.push_back(static_cast(0x80)); + while (data.size() % 64 != 56) { + data.push_back(0); + } + for (int i = 7; i >= 0; --i) { + data.push_back(static_cast((bit_length >> (i * 8)) & 0xff)); + } + for (std::size_t chunk = 0; chunk < data.size(); chunk += 64) { + std::uint32_t w[64]; + for (int i = 0; i < 16; ++i) { + w[i] = (static_cast(data[chunk + i * 4]) << 24) | + (static_cast(data[chunk + i * 4 + 1]) << 16) | + (static_cast(data[chunk + i * 4 + 2]) << 8) | + (static_cast(data[chunk + i * 4 + 3])); + } + for (int i = 16; i < 64; ++i) { + const std::uint32_t s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3); + const std::uint32_t s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + std::uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], + hh = h[7]; + for (int i = 0; i < 64; ++i) { + const std::uint32_t s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25); + const std::uint32_t ch = (e & f) ^ (~e & g); + const std::uint32_t t1 = hh + s1 + ch + k[i] + w[i]; + const std::uint32_t s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22); + const std::uint32_t maj = (a & b) ^ (a & c) ^ (b & c); + const std::uint32_t t2 = s0 + maj; + hh = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + } + h[0] += a; h[1] += b; h[2] += c; h[3] += d; + h[4] += e; h[5] += f; h[6] += g; h[7] += hh; + } + static const char* hex = "0123456789abcdef"; + std::string out; + for (std::uint32_t value : h) { + for (int i = 7; i >= 0; --i) { + out.push_back(hex[(value >> (i * 4)) & 0xf]); + } + } + return out; +} + +} // namespace + +int main(int argc, char** argv) { + TestSuite suite("wally_contract"); + suite.add("binding_matches_the_pinned_contract", test_binding_matches_the_pinned_contract); + suite.add("request_and_response_round_trip", test_request_and_response_round_trip); + return suite.run(argc, argv); +} diff --git a/tests/test_rcli_harness.cpp b/tests/test_wally_harness.cpp similarity index 83% rename from tests/test_rcli_harness.cpp rename to tests/test_wally_harness.cpp index 1ef1429..90a4ac2 100644 --- a/tests/test_rcli_harness.cpp +++ b/tests/test_wally_harness.cpp @@ -43,7 +43,7 @@ class TemporaryDirectory { public: TemporaryDirectory() { const auto nonce = std::chrono::steady_clock::now().time_since_epoch().count(); - path_ = fs::temp_directory_path() / ("rcli-harness-test-" + std::to_string(nonce)); + path_ = fs::temp_directory_path() / ("wally-harness-test-" + std::to_string(nonce)); fs::create_directories(path_); } ~TemporaryDirectory() { @@ -73,7 +73,7 @@ TestResult test_model_id_rejects_empty_and_control_characters() { const std::string unsafe[] = {"", std::string("qwen\nrm -rf"), std::string("qwen\x7f", 5)}; for (const std::string& id : unsafe) { - if (rcli::harness::ModelIdIsSafe(id)) { + if (wally::harness::ModelIdIsSafe(id)) { result.details = "accepted an id containing a control character or empty id"; return result; } @@ -100,7 +100,7 @@ TestResult test_model_id_rejects_xml_and_path_structural_characters() { "it's-a-model", }; for (const std::string& id : unsafe) { - if (rcli::harness::ModelIdIsSafe(id)) { + if (wally::harness::ModelIdIsSafe(id)) { result.details = "accepted an XML/path-structural model id: " + id; return result; } @@ -115,7 +115,7 @@ TestResult test_model_id_accepts_ordinary_ids() { const std::string safe[] = {"mlx-qwen3", "whisper-tiny", "qwen3.8-27b-1bit-npu", "smolvlm2"}; for (const std::string& id : safe) { - if (!rcli::harness::ModelIdIsSafe(id)) { + if (!wally::harness::ModelIdIsSafe(id)) { result.details = "rejected an ordinary model id: " + id; return result; } @@ -131,14 +131,14 @@ TestResult test_model_id_accepts_ordinary_ids() { bool Seed(const fs::path& profile, const std::string& access_token, const std::string& refresh_token, long long expires_at, std::string* error) { - Environment scoped_profile("RCLI_PROFILE_DIR", profile.string().c_str()); - rcli::account::Credentials credentials; + Environment scoped_profile("WALLY_PROFILE_DIR", profile.string().c_str()); + wally::account::Credentials credentials; credentials.console_url = "https://console.runanywhere.ai"; credentials.email = "developer@example.test"; credentials.access_token = access_token; credentials.refresh_token = refresh_token; credentials.expires_at = expires_at; - return rcli::account::Save(credentials, error); + return wally::account::Save(credentials, error); } // A hand-written credentials.json with any non-empty access_token and no @@ -148,7 +148,7 @@ TestResult test_verify_cloud_session_rejects_unverifiable_token() { TestResult result; result.test_name = "verify_cloud_session_rejects_unverifiable_token"; TemporaryDirectory temporary; - Environment profile("RCLI_PROFILE_DIR", temporary.path().string().c_str()); + Environment profile("WALLY_PROFILE_DIR", temporary.path().string().c_str()); std::string error; if (!Seed(temporary.path(), "hand-written-garbage-token", "", Now() + 3600, &error)) { result.details = error; @@ -156,22 +156,22 @@ TestResult test_verify_cloud_session_rejects_unverifiable_token() { } bool contacted_console = false; - rcli::account::ConsoleClient console([&](const rcli::account::HttpRequest& request, - rcli::account::HttpResponse* response, std::string*) { + wally::account::ConsoleClient console([&](const wally::account::HttpRequest& request, + wally::account::HttpResponse* response, std::string*) { contacted_console = true; response->status = 401; return true; }); - rcli::account::Credentials credentials; - if (!rcli::account::Load(&credentials, &error)) { + wally::account::Credentials credentials; + if (!wally::account::Load(&credentials, &error)) { result.details = error; return result; } std::string email; std::string verify_error; - const bool ok = rcli::harness::VerifyCloudSession(console, &credentials, &email, &verify_error); + const bool ok = wally::harness::VerifyCloudSession(console, &credentials, &email, &verify_error); if (ok) { result.details = "a garbage token with no refresh token must not verify"; return result; @@ -189,13 +189,13 @@ TestResult test_verify_cloud_session_rejects_unverifiable_token() { } // An expired access token with a good refresh token is refreshed and then -// re-verified, exactly the `rcli usage` dance, and the refreshed session is +// re-verified, exactly the `wally usage` dance, and the refreshed session is // what Resolve() goes on to use. TestResult test_verify_cloud_session_refreshes_and_reverifies() { TestResult result; result.test_name = "verify_cloud_session_refreshes_and_reverifies"; TemporaryDirectory temporary; - Environment profile("RCLI_PROFILE_DIR", temporary.path().string().c_str()); + Environment profile("WALLY_PROFILE_DIR", temporary.path().string().c_str()); std::string error; if (!Seed(temporary.path(), "old-access-token", "refresh-token", Now() - 1, &error)) { result.details = error; @@ -204,8 +204,8 @@ TestResult test_verify_cloud_session_refreshes_and_reverifies() { bool refreshed = false; bool verified_with_new_token = false; - rcli::account::ConsoleClient console([&](const rcli::account::HttpRequest& request, - rcli::account::HttpResponse* response, std::string*) { + wally::account::ConsoleClient console([&](const wally::account::HttpRequest& request, + wally::account::HttpResponse* response, std::string*) { if (request.url.ends_with("/auth/cli/refresh")) { refreshed = true; response->status = 200; @@ -224,15 +224,15 @@ TestResult test_verify_cloud_session_refreshes_and_reverifies() { return false; }); - rcli::account::Credentials credentials; - if (!rcli::account::Load(&credentials, &error)) { + wally::account::Credentials credentials; + if (!wally::account::Load(&credentials, &error)) { result.details = error; return result; } std::string email; std::string verify_error; - const bool ok = rcli::harness::VerifyCloudSession(console, &credentials, &email, &verify_error); + const bool ok = wally::harness::VerifyCloudSession(console, &credentials, &email, &verify_error); if (!ok || !refreshed || !verified_with_new_token) { result.details = "expired token should be refreshed, then verified with the new token"; result.actual = verify_error; @@ -257,7 +257,7 @@ TestResult test_verify_cloud_session_accepts_real_session() { TestResult result; result.test_name = "verify_cloud_session_accepts_real_session"; TemporaryDirectory temporary; - Environment profile("RCLI_PROFILE_DIR", temporary.path().string().c_str()); + Environment profile("WALLY_PROFILE_DIR", temporary.path().string().c_str()); std::string error; if (!Seed(temporary.path(), "real-access-token", "refresh-token", Now() + 3600, &error)) { result.details = error; @@ -265,8 +265,8 @@ TestResult test_verify_cloud_session_accepts_real_session() { } bool refresh_called = false; - rcli::account::ConsoleClient console([&](const rcli::account::HttpRequest& request, - rcli::account::HttpResponse* response, std::string*) { + wally::account::ConsoleClient console([&](const wally::account::HttpRequest& request, + wally::account::HttpResponse* response, std::string*) { if (request.url.ends_with("/auth/cli/refresh")) { refresh_called = true; return false; @@ -279,15 +279,15 @@ TestResult test_verify_cloud_session_accepts_real_session() { return false; }); - rcli::account::Credentials credentials; - if (!rcli::account::Load(&credentials, &error)) { + wally::account::Credentials credentials; + if (!wally::account::Load(&credentials, &error)) { result.details = error; return result; } std::string email; std::string verify_error; - const bool ok = rcli::harness::VerifyCloudSession(console, &credentials, &email, &verify_error); + const bool ok = wally::harness::VerifyCloudSession(console, &credentials, &email, &verify_error); if (!ok || refresh_called) { result.details = "a valid session should verify without refreshing"; result.actual = verify_error; @@ -300,7 +300,7 @@ TestResult test_verify_cloud_session_accepts_real_session() { } // namespace int main(int argc, char** argv) { - TestSuite suite("rcli_harness"); + TestSuite suite("wally_harness"); suite.add("model_id_rejects_empty_and_control_characters", test_model_id_rejects_empty_and_control_characters); suite.add("model_id_rejects_xml_and_path_structural_characters", diff --git a/tests/test_rcli_mlx_e2e.cpp b/tests/test_wally_mlx_e2e.cpp similarity index 95% rename from tests/test_rcli_mlx_e2e.cpp rename to tests/test_wally_mlx_e2e.cpp index c523e85..feb06a6 100644 --- a/tests/test_rcli_mlx_e2e.cpp +++ b/tests/test_wally_mlx_e2e.cpp @@ -1,11 +1,11 @@ /** - * @file test_rcli_mlx_e2e.cpp - * @brief In-process rcli E2E coverage for the MLX backend contract. + * @file test_wally_mlx_e2e.cpp + * @brief In-process wally E2E coverage for the MLX backend contract. * * The production MLX runtime is Swift/MLX. This test installs the same C - * callback table that the Swift runtime installs, then invokes the actual rcli + * callback table that the Swift runtime installs, then invokes the actual wally * command stack against a local MLX-style folder. That keeps the test offline - * and fast while exercising rcli parsing, bootstrap, backend registration, + * and fast while exercising wally parsing, bootstrap, backend registration, * commons lifecycle loading, MLX callback dispatch, and streaming output. */ @@ -456,10 +456,10 @@ class StdoutCapture { int run_cli_capture(const std::vector &args, std::string *stdout_text) { - rcli::GlobalOptions options; + wally::GlobalOptions options; CLI::App app{ "RunAnywhere on-device AI CLI — run, manage and serve local models"}; - rcli::configure_app(app, options); + wally::configure_app(app, options); std::vector mutable_args = args; std::vector argv; @@ -977,9 +977,9 @@ TestResult test_mlx_callback_bridge_all_slots() { return result; } -TestResult test_rcli_mlx_run_end_to_end() { +TestResult test_wally_mlx_run_end_to_end() { TestResult result; - result.test_name = "rcli_mlx_run_end_to_end"; + result.test_name = "wally_mlx_run_end_to_end"; g_mlx_state = {}; if (!install_fake_mlx_callbacks()) { @@ -987,12 +987,12 @@ TestResult test_rcli_mlx_run_end_to_end() { return result; } - const std::filesystem::path home = make_temp_dir("rcli-mlx-home"); - const std::filesystem::path llm_dir = make_temp_dir("rcli-mlx-llm"); - const std::filesystem::path vlm_dir = make_temp_dir("rcli-mlx-vlm"); - const std::filesystem::path embedding_dir = make_temp_dir("rcli-mlx-embed"); - const std::filesystem::path stt_dir = make_temp_dir("rcli-mlx-stt"); - const std::filesystem::path tts_dir = make_temp_dir("rcli-mlx-tts"); + const std::filesystem::path home = make_temp_dir("wally-mlx-home"); + const std::filesystem::path llm_dir = make_temp_dir("wally-mlx-llm"); + const std::filesystem::path vlm_dir = make_temp_dir("wally-mlx-vlm"); + const std::filesystem::path embedding_dir = make_temp_dir("wally-mlx-embed"); + const std::filesystem::path stt_dir = make_temp_dir("wally-mlx-stt"); + const std::filesystem::path tts_dir = make_temp_dir("wally-mlx-tts"); for (const auto &dir : {llm_dir, vlm_dir, embedding_dir, stt_dir, tts_dir}) { if (!write_file(dir / "config.json", R"({"model_type":"qwen3"})") || !write_file(dir / "model.safetensors", "fake-weights") || @@ -1012,18 +1012,18 @@ TestResult test_rcli_mlx_run_end_to_end() { const std::vector pcm_samples = {0, 1024, -1024, 2048, -2048, 1024, -1024, 0}; std::string wav_error; - if (!rcli::wav::write_wav(input_wav.string(), pcm_samples.data(), + if (!wally::wav::write_wav(input_wav.string(), pcm_samples.data(), pcm_samples.size(), 16000, &wav_error)) { result.details = wav_error; return result; } - rcli::GlobalOptions options; + wally::GlobalOptions options; options.home_override = home.string(); options.json = true; options.no_progress = true; - rcli::Bootstrapped bootstrapped; - if (rcli::bootstrap(options, &bootstrapped) != RAC_SUCCESS) { + wally::Bootstrapped bootstrapped; + if (wally::bootstrap(options, &bootstrapped) != RAC_SUCCESS) { result.details = "bootstrap failed"; return result; } @@ -1039,15 +1039,15 @@ TestResult test_rcli_mlx_run_end_to_end() { !register_local_mlx_model(tts_dir, "mlx.fake.tts", "Fake MLX TTS", v1::MODEL_CATEGORY_SPEECH_SYNTHESIS)) { result.details = "failed to register local MLX model"; - rcli::shutdown(); + wally::shutdown(); return result; } std::string backends_json; - if (!run_cli_or_fail({"rcli", "--json", "--no-progress", "--home", + if (!run_cli_or_fail({"wally", "--json", "--no-progress", "--home", home.string(), "backends"}, "backends", &backends_json, &result)) { - rcli::shutdown(); + wally::shutdown(); return result; } std::string backend_error; @@ -1059,15 +1059,15 @@ TestResult test_rcli_mlx_run_end_to_end() { "mlx backend with generate_text/vlm/embed/transcribe/synthesize " "primitives"; result.actual = backend_error.empty() ? backends_json : backend_error + ": " + backends_json; - rcli::shutdown(); + wally::shutdown(); return result; } std::string list_json; - if (!run_cli_or_fail({"rcli", "--json", "--no-progress", "--home", + if (!run_cli_or_fail({"wally", "--json", "--no-progress", "--home", home.string(), "list", "--all"}, "list", &list_json, &result)) { - rcli::shutdown(); + wally::shutdown(); return result; } if (list_json.find("\"id\":\"mlx.fake.vlm\"") == std::string::npos || @@ -1079,18 +1079,18 @@ TestResult test_rcli_mlx_run_end_to_end() { list_json.find("\"backend\":\"MLX\"") == std::string::npos || list_json.find("\"id\":\"mlx.fake.tts\"") == std::string::npos || list_json.find("\"modality\":\"tts\"") == std::string::npos) { - result.expected = "MLX VLM/embedding/STT/TTS rows from rcli list --all"; + result.expected = "MLX VLM/embedding/STT/TTS rows from wally list --all"; result.actual = list_json; - rcli::shutdown(); + wally::shutdown(); return result; } std::string run_json; - if (!run_cli_or_fail({"rcli", "--json", "--no-progress", "--home", + if (!run_cli_or_fail({"wally", "--json", "--no-progress", "--home", home.string(), "run", "mlx.fake.llm", "Hello MLX", "--engine", "mlx", "--max-tokens", "4"}, "LLM", &run_json, &result)) { - rcli::shutdown(); + wally::shutdown(); return result; } if (run_json.find("\"model\":\"mlx.fake.llm\"") == std::string::npos || @@ -1098,7 +1098,7 @@ TestResult test_rcli_mlx_run_end_to_end() { std::string::npos) { result.expected = "JSON response from MLX stream callback"; result.actual = run_json; - rcli::shutdown(); + wally::shutdown(); return result; } if (g_mlx_state.create_count != 1 || g_mlx_state.initialize_count != 1 || @@ -1106,7 +1106,7 @@ TestResult test_rcli_mlx_run_end_to_end() { g_mlx_state.last_kind != RAC_MLX_SESSION_KIND_LLM) { result.details = "MLX LLM callback counts/kind were not exercised as expected"; - rcli::shutdown(); + wally::shutdown(); return result; } if (g_mlx_state.last_model_path != llm_dir.string()) { @@ -1114,18 +1114,18 @@ TestResult test_rcli_mlx_run_end_to_end() { result.actual = g_mlx_state.last_model_path; result.details = "MLX LLM runtime should receive the model folder, not " "model.safetensors"; - rcli::shutdown(); + wally::shutdown(); return result; } std::string vlm_json; - if (!run_cli_or_fail({"rcli", "--json", "--no-progress", "--home", + if (!run_cli_or_fail({"wally", "--json", "--no-progress", "--home", home.string(), "run", "mlx.fake.vlm", "What is in the image?", "--image", input_image.string(), "--engine", "mlx", "--max-tokens", "4"}, "VLM", &vlm_json, &result)) { - rcli::shutdown(); + wally::shutdown(); return result; } if (vlm_json.find("\"model\":\"mlx.fake.vlm\"") == std::string::npos || @@ -1133,14 +1133,14 @@ TestResult test_rcli_mlx_run_end_to_end() { std::string::npos) { result.expected = "JSON VLM response from MLX callback"; result.actual = vlm_json; - rcli::shutdown(); + wally::shutdown(); return result; } if (g_mlx_state.vlm_process_count != 1 || g_mlx_state.last_kind != RAC_MLX_SESSION_KIND_VLM) { result.details = "MLX VLM callback count/kind was not exercised as expected"; - rcli::shutdown(); + wally::shutdown(); return result; } if (g_mlx_state.last_model_path != vlm_dir.string()) { @@ -1148,16 +1148,16 @@ TestResult test_rcli_mlx_run_end_to_end() { result.actual = g_mlx_state.last_model_path; result.details = "MLX VLM runtime should receive the model folder, not " "model.safetensors"; - rcli::shutdown(); + wally::shutdown(); return result; } std::string embed_json; - if (!run_cli_or_fail({"rcli", "--json", "--no-progress", "--home", + if (!run_cli_or_fail({"wally", "--json", "--no-progress", "--home", home.string(), "embed", "Hello MLX embeddings", "--model", "mlx.fake.embed", "--text", "Batch item"}, "embedding", &embed_json, &result)) { - rcli::shutdown(); + wally::shutdown(); return result; } if (embed_json.find("\"model\":\"mlx.fake.embed\"") == std::string::npos || @@ -1166,7 +1166,7 @@ TestResult test_rcli_mlx_run_end_to_end() { embed_json.find("\"values\":[1,0.5]") == std::string::npos) { result.expected = "JSON embedding vectors from MLX callback"; result.actual = embed_json; - rcli::shutdown(); + wally::shutdown(); return result; } if (g_mlx_state.embed_batch_count != 1 || @@ -1174,7 +1174,7 @@ TestResult test_rcli_mlx_run_end_to_end() { g_mlx_state.last_embed_batch_size != 2) { result.details = "MLX embedding callback count/kind/batch size was not exercised as expected"; - rcli::shutdown(); + wally::shutdown(); return result; } if (g_mlx_state.last_model_path != embedding_dir.string()) { @@ -1182,23 +1182,23 @@ TestResult test_rcli_mlx_run_end_to_end() { result.actual = g_mlx_state.last_model_path; result.details = "MLX embedding runtime should receive the model folder, not " "model.safetensors"; - rcli::shutdown(); + wally::shutdown(); return result; } std::string stt_json; - if (!run_cli_or_fail({"rcli", "--json", "--no-progress", "--home", + if (!run_cli_or_fail({"wally", "--json", "--no-progress", "--home", home.string(), "stt", "--model", "mlx.fake.stt", "--input", input_wav.string()}, "STT", &stt_json, &result)) { - rcli::shutdown(); + wally::shutdown(); return result; } if (stt_json.find("\"model\":\"mlx.fake.stt\"") == std::string::npos || stt_json.find("\"text\":\"mlx-stt-stub: ") == std::string::npos) { result.expected = "JSON STT response from MLX callback"; result.actual = stt_json; - rcli::shutdown(); + wally::shutdown(); return result; } if (g_mlx_state.stt_transcribe_count != 1 || @@ -1206,7 +1206,7 @@ TestResult test_rcli_mlx_run_end_to_end() { g_mlx_state.last_audio_size != pcm_samples.size() * sizeof(int16_t)) { result.details = "MLX STT callback counts/kind/audio size were not " "exercised as expected"; - rcli::shutdown(); + wally::shutdown(); return result; } if (g_mlx_state.last_model_path != stt_dir.string()) { @@ -1214,17 +1214,17 @@ TestResult test_rcli_mlx_run_end_to_end() { result.actual = g_mlx_state.last_model_path; result.details = "MLX STT runtime should receive the model folder, not " "model.safetensors"; - rcli::shutdown(); + wally::shutdown(); return result; } std::string tts_json; const bool tts_ok = run_cli_or_fail( - {"rcli", "--json", "--no-progress", "--home", home.string(), "tts", + {"wally", "--json", "--no-progress", "--home", home.string(), "tts", "--model", "mlx.fake.tts", "--text", "Hello MLX audio", "--output", output_wav.string()}, "TTS", &tts_json, &result); - rcli::shutdown(); + wally::shutdown(); if (!tts_ok) { return result; } @@ -1263,8 +1263,8 @@ TestResult test_rcli_mlx_run_end_to_end() { } // namespace int main(int argc, char **argv) { - TestSuite suite("rcli_mlx_e2e"); + TestSuite suite("wally_mlx_e2e"); suite.add("mlx_callback_bridge_all_slots", test_mlx_callback_bridge_all_slots); - suite.add("rcli_mlx_run_end_to_end", test_rcli_mlx_run_end_to_end); + suite.add("wally_mlx_run_end_to_end", test_wally_mlx_run_end_to_end); return suite.run(argc, argv); } diff --git a/tests/test_rcli_opencode.cpp b/tests/test_wally_opencode.cpp similarity index 86% rename from tests/test_rcli_opencode.cpp rename to tests/test_wally_opencode.cpp index 9754a99..bd4479d 100644 --- a/tests/test_rcli_opencode.cpp +++ b/tests/test_wally_opencode.cpp @@ -45,7 +45,7 @@ class TemporaryDirectory { public: TemporaryDirectory() { const auto nonce = std::chrono::steady_clock::now().time_since_epoch().count(); - path_ = fs::temp_directory_path() / ("rcli-opencode-test-" + std::to_string(nonce)); + path_ = fs::temp_directory_path() / ("wally-opencode-test-" + std::to_string(nonce)); fs::create_directories(path_); } ~TemporaryDirectory() { @@ -65,21 +65,21 @@ long long Now() { } bool Seed(const fs::path& profile, long long expires_at, std::string* error) { - Environment scoped_profile("RCLI_PROFILE_DIR", profile.string().c_str()); - rcli::account::Credentials credentials; + Environment scoped_profile("WALLY_PROFILE_DIR", profile.string().c_str()); + wally::account::Credentials credentials; credentials.console_url = "https://console.runanywhere.ai"; credentials.email = "developer@example.test"; credentials.access_token = "old-access-token"; credentials.refresh_token = "refresh-token"; credentials.expires_at = expires_at; - return rcli::account::Save(credentials, error); + return wally::account::Save(credentials, error); } TestResult test_ephemeral_config_and_passthrough() { TestResult result; result.test_name = "ephemeral_config_and_passthrough"; TemporaryDirectory temporary; - Environment profile("RCLI_PROFILE_DIR", temporary.path().string().c_str()); + Environment profile("WALLY_PROFILE_DIR", temporary.path().string().c_str()); Environment existing("OPENCODE_CONFIG_CONTENT", "{\"keep\":true}"); std::string error; if (!Seed(temporary.path(), Now() + 3600, &error)) { @@ -92,8 +92,8 @@ TestResult test_ephemeral_config_and_passthrough() { // The launch path verifies the session against the console before it starts // anything, so the console has to be answered here. A default ConsoleClient // would put a real request on the wire, which a unit test must never do. - const rcli::account::ConsoleClient console( - [](const rcli::account::HttpRequest& request, rcli::account::HttpResponse* response, + const wally::account::ConsoleClient console( + [](const wally::account::HttpRequest& request, wally::account::HttpResponse* response, std::string*) { if (!request.url.ends_with("/v1/me")) { return false; @@ -102,7 +102,7 @@ TestResult test_ephemeral_config_and_passthrough() { response->body = Json{{"email", "developer@example.test"}}.dump(); return true; }); - const int status = rcli::harness::LaunchOpenCodeCloud( + const int status = wally::harness::LaunchOpenCodeCloud( "glm-5.3", arguments, console, [&](const std::string& executable, const std::vector& received) { spawned = true; @@ -143,7 +143,7 @@ TestResult test_refreshes_expired_session_without_sdk_bootstrap() { TestResult result; result.test_name = "refreshes_expired_session_without_sdk_bootstrap"; TemporaryDirectory temporary; - Environment profile("RCLI_PROFILE_DIR", temporary.path().string().c_str()); + Environment profile("WALLY_PROFILE_DIR", temporary.path().string().c_str()); Environment api_key("RUNANYWHERE_API_KEY", nullptr); Environment environment("RUNANYWHERE_ENVIRONMENT", nullptr); Environment config("OPENCODE_CONFIG_CONTENT", nullptr); @@ -154,8 +154,8 @@ TestResult test_refreshes_expired_session_without_sdk_bootstrap() { } bool refreshed = false; - rcli::account::ConsoleClient console([&](const rcli::account::HttpRequest& request, - rcli::account::HttpResponse* response, std::string*) { + wally::account::ConsoleClient console([&](const wally::account::HttpRequest& request, + wally::account::HttpResponse* response, std::string*) { if (request.url.ends_with("/v1/me")) { response->status = 200; response->body = Json{{"email", "developer@example.test"}}.dump(); @@ -174,7 +174,7 @@ TestResult test_refreshes_expired_session_without_sdk_bootstrap() { return true; }); - const int status = rcli::harness::LaunchOpenCodeCloud( + const int status = wally::harness::LaunchOpenCodeCloud( "hosted-model", {}, console, [&](const std::string&, const std::vector&) { const Json value = Json::parse(std::getenv("OPENCODE_CONFIG_CONTENT")); return value.at("provider").at("runanywhere").at("options").at("apiKey") == @@ -188,8 +188,8 @@ TestResult test_refreshes_expired_session_without_sdk_bootstrap() { return result; } - rcli::account::Credentials stored; - if (!rcli::account::Load(&stored, &error) || stored.access_token != "new-access-token" || + wally::account::Credentials stored; + if (!wally::account::Load(&stored, &error) || stored.access_token != "new-access-token" || stored.refresh_token != "new-refresh-token" || stored.expires_at <= Now()) { result.details = error.empty() ? "refreshed session was not stored" : error; return result; @@ -202,7 +202,7 @@ TestResult test_restores_config_when_spawn_throws() { TestResult result; result.test_name = "restores_config_when_spawn_throws"; TemporaryDirectory temporary; - Environment profile("RCLI_PROFILE_DIR", temporary.path().string().c_str()); + Environment profile("WALLY_PROFILE_DIR", temporary.path().string().c_str()); Environment config("OPENCODE_CONFIG_CONTENT", "original-value"); std::string error; if (!Seed(temporary.path(), Now() + 3600, &error)) { @@ -212,8 +212,8 @@ TestResult test_restores_config_when_spawn_throws() { bool threw = false; try { - const rcli::account::ConsoleClient console( - [](const rcli::account::HttpRequest& request, rcli::account::HttpResponse* response, + const wally::account::ConsoleClient console( + [](const wally::account::HttpRequest& request, wally::account::HttpResponse* response, std::string*) { if (!request.url.ends_with("/v1/me")) { return false; @@ -222,7 +222,7 @@ TestResult test_restores_config_when_spawn_throws() { response->body = Json{{"email", "developer@example.test"}}.dump(); return true; }); - static_cast(rcli::harness::LaunchOpenCodeCloud( + static_cast(wally::harness::LaunchOpenCodeCloud( "hosted-model", {}, console, [](const std::string&, const std::vector&) -> int { throw std::runtime_error("synthetic spawn failure"); @@ -242,7 +242,7 @@ TestResult test_restores_config_when_spawn_throws() { } // namespace int main(int argc, char** argv) { - TestSuite suite("rcli_opencode"); + TestSuite suite("wally_opencode"); suite.add("ephemeral_config_and_passthrough", test_ephemeral_config_and_passthrough); suite.add("refreshes_expired_session_without_sdk_bootstrap", test_refreshes_expired_session_without_sdk_bootstrap); diff --git a/tests/test_rcli_segment.cpp b/tests/test_wally_segment.cpp similarity index 87% rename from tests/test_rcli_segment.cpp rename to tests/test_wally_segment.cpp index d543dfc..3d62008 100644 --- a/tests/test_rcli_segment.cpp +++ b/tests/test_wally_segment.cpp @@ -1,13 +1,13 @@ /** - * @file test_rcli_segment.cpp - * @brief rcli `segment` command tests — pure suite: no backend, no models, no + * @file test_wally_segment.cpp + * @brief wally `segment` command tests — pure suite: no backend, no models, no * network, no stream redirection. * * Covers exactly what is reachable for the `segment` command WITHOUT a * segmentation backend: * - io/image_io.cpp read_ppm(): the CLI-owned input gate for `segment`. * - io/image_io.cpp write_png(): the sibling encoder (smoke + guards). - * - The CLI11 parse/validation layer of `rcli segment` (fires before the + * - The CLI11 parse/validation layer of `wally segment` (fires before the * callback), asserting the documented 0/1/2 exit-code contract for usage * errors via src/app.cpp's exact catch ladder. * - Structural wiring of register_segment (arg/flag spec) via introspection. @@ -17,10 +17,10 @@ * run_segment(), resolve_model_path() and print_result() live in an anonymous * namespace in cmd_segment.cpp and are unreachable from here; anything that * needs to *execute* them requires a SEGMENT backend and belongs in a separate - * backend-gated e2e test (cf. test_rcli_mlx_e2e.cpp). + * backend-gated e2e test (cf. test_wally_mlx_e2e.cpp). * * Uses the commons TestSuite harness so ctest drives it the same way as the - * other rcli suites (--run-all / --test-). + * other wally suites (--run-all / --test-). */ #include "test_common.h" @@ -45,7 +45,7 @@ namespace { // Unique temp path (steady_clock stamp + a per-process counter), mirroring the -// uniqueness scheme in test_rcli_mlx_e2e.cpp. Does not create the file. +// uniqueness scheme in test_wally_mlx_e2e.cpp. Does not create the file. std::filesystem::path unique_temp_path(const std::string& stem, const std::string& ext) { static int counter = 0; const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count(); @@ -91,15 +91,15 @@ TestResult test_read_ppm() { const unsigned char pixels[6] = {10, 20, 30, 40, 50, 60}; std::string content = "P6\n2 1\n255\n"; content.append(reinterpret_cast(pixels), sizeof(pixels)); - const auto path = unique_temp_path("rcli-seg-valid", ".ppm"); + const auto path = unique_temp_path("wally-seg-valid", ".ppm"); TempFileGuard guard(path); if (!write_binary_file(path, content)) { result.details = "could not write valid PPM fixture"; return result; } - rcli::image::RgbImage img; + wally::image::RgbImage img; std::string err; - if (!rcli::image::read_ppm(path.string(), &img, &err)) { + if (!wally::image::read_ppm(path.string(), &img, &err)) { result.details = "valid P6 was rejected: " + err; return result; } @@ -123,17 +123,17 @@ TestResult test_read_ppm() { // branch) and parsing still succeeds. { const unsigned char pixels[6] = {1, 2, 3, 4, 5, 6}; - std::string content = "P6\n# rcli comment before dims\n2 1\n# and a trailing one\n255\n"; + std::string content = "P6\n# wally comment before dims\n2 1\n# and a trailing one\n255\n"; content.append(reinterpret_cast(pixels), sizeof(pixels)); - const auto path = unique_temp_path("rcli-seg-comment", ".ppm"); + const auto path = unique_temp_path("wally-seg-comment", ".ppm"); TempFileGuard guard(path); if (!write_binary_file(path, content)) { result.details = "could not write commented PPM fixture"; return result; } - rcli::image::RgbImage img; + wally::image::RgbImage img; std::string err; - if (!rcli::image::read_ppm(path.string(), &img, &err)) { + if (!wally::image::read_ppm(path.string(), &img, &err)) { result.details = "commented P6 header was rejected: " + err; return result; } @@ -165,15 +165,15 @@ TestResult test_read_ppm() { {"P6\n4 4\n255\n" + five, "truncated PPM pixel data", "payload shorter than header promises"}, }; for (const Neg& n : negatives) { - const auto path = unique_temp_path("rcli-seg-neg", ".ppm"); + const auto path = unique_temp_path("wally-seg-neg", ".ppm"); TempFileGuard guard(path); if (!write_binary_file(path, n.content)) { result.details = "could not write negative fixture: " + n.note; return result; } - rcli::image::RgbImage img; + wally::image::RgbImage img; std::string err; - if (rcli::image::read_ppm(path.string(), &img, &err)) { + if (wally::image::read_ppm(path.string(), &img, &err)) { result.details = "expected failure but read_ppm succeeded: " + n.note; return result; } @@ -187,10 +187,10 @@ TestResult test_read_ppm() { // (h) Nonexistent path → false with "cannot open" (file never created). { - const std::string missing = unique_temp_path("rcli-seg-missing", ".ppm").string(); - rcli::image::RgbImage img; + const std::string missing = unique_temp_path("wally-seg-missing", ".ppm").string(); + wally::image::RgbImage img; std::string err; - if (rcli::image::read_ppm(missing, &img, &err)) { + if (wally::image::read_ppm(missing, &img, &err)) { result.details = "read_ppm succeeded on a nonexistent path"; return result; } @@ -219,10 +219,10 @@ TestResult test_write_png_smoke() { rgba[i] = static_cast(i * 7 + 3); } - const auto path = unique_temp_path("rcli-seg-png", ".png"); + const auto path = unique_temp_path("wally-seg-png", ".png"); TempFileGuard guard(path); std::string err; - if (!rcli::image::write_png(path.string(), rgba.data(), width, height, &err)) { + if (!wally::image::write_png(path.string(), rgba.data(), width, height, &err)) { result.details = "write_png failed on a valid RGBA buffer: " + err; return result; } @@ -249,16 +249,16 @@ TestResult test_write_png_smoke() { } // width <= 0 and null data are rejected before any file is opened. - const std::string throwaway = unique_temp_path("rcli-seg-png-bad", ".png").string(); + const std::string throwaway = unique_temp_path("wally-seg-png-bad", ".png").string(); std::string err_dim; - if (rcli::image::write_png(throwaway, rgba.data(), 0, height, &err_dim) || + if (wally::image::write_png(throwaway, rgba.data(), 0, height, &err_dim) || err_dim.find("invalid image dimensions or data") == std::string::npos) { result.details = "width<=0 must fail with 'invalid image dimensions or data'"; result.actual = err_dim; return result; } std::string err_null; - if (rcli::image::write_png(throwaway, nullptr, width, height, &err_null) || + if (wally::image::write_png(throwaway, nullptr, width, height, &err_null) || err_null.find("invalid image dimensions or data") == std::string::npos) { result.details = "null data must fail with 'invalid image dimensions or data'"; result.actual = err_null; @@ -272,7 +272,7 @@ TestResult test_write_png_smoke() { // ----------------------------------------------------------------------------- // Exit-code contract for `segment` usage errors. // -// Builds the real app via rcli::configure_app (which adds the global flags AND +// Builds the real app via wally::configure_app (which adds the global flags AND // every subcommand, including segment) and maps the thrown CLI11 exception to a // process exit code using the EXACT catch ladder from src/app.cpp: // RuntimeError -> get_exit_code() or 1 @@ -286,9 +286,9 @@ TestResult test_write_png_smoke() { // entered. // ----------------------------------------------------------------------------- int segment_exit_code(const std::vector& args) { - rcli::GlobalOptions options; + wally::GlobalOptions options; CLI::App app{"RunAnywhere on-device AI CLI — run, manage and serve local models"}; - rcli::configure_app(app, options); + wally::configure_app(app, options); std::vector mutable_args = args; std::vector argv; @@ -319,7 +319,7 @@ TestResult test_segment_usage_errors() { const unsigned char pixels[3] = {9, 9, 9}; std::string ppm_content = "P6\n1 1\n255\n"; ppm_content.append(reinterpret_cast(pixels), sizeof(pixels)); - const auto ppm = unique_temp_path("rcli-seg-usage", ".ppm"); + const auto ppm = unique_temp_path("wally-seg-usage", ".ppm"); TempFileGuard guard(ppm); if (!write_binary_file(ppm, ppm_content)) { result.details = "could not write usage-test PPM fixture"; @@ -327,7 +327,7 @@ TestResult test_segment_usage_errors() { } const std::string existing = ppm.string(); - const std::string missing = unique_temp_path("rcli-seg-usage-missing", ".ppm").string(); + const std::string missing = unique_temp_path("wally-seg-usage-missing", ".ppm").string(); struct Case { std::vector args; @@ -335,12 +335,12 @@ TestResult test_segment_usage_errors() { std::string note; }; const Case cases[] = { - {{"rcli", "segment"}, 2, "image + --model both missing (RequiredError -> 2)"}, - {{"rcli", "segment", "--model", "seg-model"}, 2, "image missing (RequiredError -> 2)"}, - {{"rcli", "segment", existing}, 2, "--model missing (RequiredError -> 2)"}, - {{"rcli", "segment", missing, "--model", "seg-model"}, 2, + {{"wally", "segment"}, 2, "image + --model both missing (RequiredError -> 2)"}, + {{"wally", "segment", "--model", "seg-model"}, 2, "image missing (RequiredError -> 2)"}, + {{"wally", "segment", existing}, 2, "--model missing (RequiredError -> 2)"}, + {{"wally", "segment", missing, "--model", "seg-model"}, 2, "image not on disk (ExistingFile -> ValidationError -> 2)"}, - {{"rcli", "segment", existing, "--model", "seg-model", "--bogus"}, 2, + {{"wally", "segment", existing, "--model", "seg-model", "--bogus"}, 2, "unknown flag (ExtrasError -> 2)"}, }; for (const Case& c : cases) { @@ -366,9 +366,9 @@ TestResult test_segment_option_spec() { TestResult result; result.test_name = "segment_option_spec"; - rcli::GlobalOptions options; + wally::GlobalOptions options; CLI::App app{"RunAnywhere on-device AI CLI — run, manage and serve local models"}; - rcli::configure_app(app, options); + wally::configure_app(app, options); CLI::App* seg = app.get_subcommand_no_throw("segment"); if (seg == nullptr) { @@ -457,7 +457,7 @@ TestResult test_segment_json_shape() { const std::string model_ref = "unused-fallback-ref"; // model_id is non-null // Reproduced verbatim from cmd_segment.cpp print_result()'s --json branch. - rcli::out::JsonWriter json; + wally::out::JsonWriter json; json.begin_object() .field("model", seg_result.model_id ? seg_result.model_id : model_ref) .field("width", static_cast(seg_result.width)) @@ -494,7 +494,7 @@ TestResult test_segment_json_shape() { } // namespace int main(int argc, char** argv) { - TestSuite suite("rcli_segment"); + TestSuite suite("wally_segment"); suite.add("read_ppm", test_read_ppm); suite.add("write_png_smoke", test_write_png_smoke); suite.add("segment_usage_errors", test_segment_usage_errors); diff --git a/tests/test_rcli_telemetry_live.cpp b/tests/test_wally_telemetry_live.cpp similarity index 91% rename from tests/test_rcli_telemetry_live.cpp rename to tests/test_wally_telemetry_live.cpp index 035445a..ee9b790 100644 --- a/tests/test_rcli_telemetry_live.cpp +++ b/tests/test_wally_telemetry_live.cpp @@ -1,14 +1,14 @@ /** - * @file test_rcli_telemetry_live.cpp + * @file test_wally_telemetry_live.cpp * @brief Live telemetry integration test — sends real, authenticated per-modality * telemetry to the configured backend and asserts each is accepted (2xx). * * This complements the hermetic commons unit test (test_telemetry_extraction), * which validates JSON shape offline against a mock sink. Here we exercise the - * full wire path: rcli bootstrap() registers the desktop adapter + HTTP + * full wire path: wally bootstrap() registers the desktop adapter + HTTP * transport and authenticates (API key -> device register -> JWT); we then * override the process telemetry manager's HTTP callback with a status-recording - * POST (the same recipe as bootstrap's rcli_telemetry_http_callback) so we can + * POST (the same recipe as bootstrap's wally_telemetry_http_callback) so we can * assert the backend's response. A strict-schema rejection (422 extra_forbidden) * fails the test — catching field drift against the real V2 endpoints. * @@ -18,7 +18,7 @@ * safe to leave registered in ctest / CI (which run it with no args). * * RUNANYWHERE_BASE_URL=... RUNANYWHERE_API_KEY=... \ - * ./test_rcli_telemetry_live --live + * ./test_wally_telemetry_live --live */ #include @@ -60,7 +60,7 @@ namespace { // Records the backend's response for the most recent flushed batch, then hands // the result back to the manager. POST recipe mirrors bootstrap's -// rcli_telemetry_http_callback (all-public rac_http_* / rac_auth_* APIs). +// wally_telemetry_http_callback (all-public rac_http_* / rac_auth_* APIs). struct LiveState { rac_telemetry_manager_t* manager = nullptr; bool called = false; @@ -137,7 +137,7 @@ void live_post_cb(void* user_data, const char* endpoint, const char* json_body, } void envelope(v1::SDKEvent* ev, v1::SDKComponent component) { - ev->set_id("rcli-live-test"); + ev->set_id("wally-live-test"); ev->set_timestamp_ms(1); ev->set_component(component); ev->set_source("cpp"); @@ -172,7 +172,7 @@ void send_and_assert(rac_telemetry_manager_t* mgr, LiveState* st, const v1::SDKE #endif // RAC_HAVE_PROTOBUF int main(int argc, char** argv) { - std::fprintf(stdout, "test_rcli_telemetry_live\n"); + std::fprintf(stdout, "test_wally_telemetry_live\n"); bool live = false; for (int i = 1; i < argc; ++i) { @@ -195,19 +195,19 @@ int main(int argc, char** argv) { std::fprintf(stdout, " skip: no protobuf\n"); return 0; #else - rcli::GlobalOptions opts; + wally::GlobalOptions opts; opts.quiet = true; - rcli::Bootstrapped env; - const rac_result_t brc = rcli::bootstrap(opts, &env); + wally::Bootstrapped env; + const rac_result_t brc = wally::bootstrap(opts, &env); CHECK(brc == RAC_SUCCESS, "bootstrap succeeded"); if (brc != RAC_SUCCESS) { return 1; } - rac_telemetry_manager_t* mgr = rcli::active_telemetry_manager(); + rac_telemetry_manager_t* mgr = wally::active_telemetry_manager(); CHECK(mgr != nullptr, "telemetry manager initialized (creds + auth)"); if (mgr == nullptr) { - rcli::shutdown(); + wally::shutdown(); return 1; } @@ -221,7 +221,7 @@ int main(int argc, char** argv) { envelope(&ev, v1::SDK_COMPONENT_LLM); auto* g = ev.mutable_generation(); g->set_kind(v1::GENERATION_EVENT_KIND_COMPLETED); - g->set_model_id("rcli-live-test"); + g->set_model_id("wally-live-test"); g->set_input_tokens(10); g->set_output_tokens(20); g->set_tokens_per_second(40.0); @@ -238,7 +238,7 @@ int main(int argc, char** argv) { auto* c = ev.mutable_capability(); c->set_kind(v1::CAPABILITY_OPERATION_EVENT_KIND_EMBEDDINGS_COMPLETED); c->set_component(v1::SDK_COMPONENT_EMBEDDINGS); - c->set_model_id("rcli-live-test"); + c->set_model_id("wally-live-test"); c->set_input_count(1); c->set_output_count(1); send_and_assert(mgr, &state, ev, "embeddings"); @@ -249,13 +249,13 @@ int main(int argc, char** argv) { envelope(&ev, v1::SDK_COMPONENT_RAG); (*ev.mutable_properties())["top_k"] = "5"; (*ev.mutable_properties())["retrieval_time_ms"] = "1"; - (*ev.mutable_properties())["embedding_model"] = "rcli-live-test"; + (*ev.mutable_properties())["embedding_model"] = "wally-live-test"; (*ev.mutable_properties())["query_token_count"] = "10"; (*ev.mutable_properties())["context_tokens"] = "49"; auto* c = ev.mutable_capability(); c->set_kind(v1::CAPABILITY_OPERATION_EVENT_KIND_RAG_QUERY_COMPLETED); c->set_component(v1::SDK_COMPONENT_RAG); - c->set_model_id("rcli-live-test"); + c->set_model_id("wally-live-test"); c->set_output_count(2); send_and_assert(mgr, &state, ev, "rag"); } @@ -269,7 +269,7 @@ int main(int argc, char** argv) { auto* c = ev.mutable_capability(); c->set_kind(v1::CAPABILITY_OPERATION_EVENT_KIND_VLM_COMPLETED); c->set_component(v1::SDK_COMPONENT_VLM); - c->set_model_id("rcli-live-test"); + c->set_model_id("wally-live-test"); c->set_input_count(1); c->set_output_count(120); send_and_assert(mgr, &state, ev, "vlm"); @@ -278,16 +278,16 @@ int main(int argc, char** argv) { { v1::SDKEvent ev; envelope(&ev, v1::SDK_COMPONENT_LLM); - (*ev.mutable_properties())["adapter_id"] = "rcli-live-test"; + (*ev.mutable_properties())["adapter_id"] = "wally-live-test"; (*ev.mutable_properties())["adapter_size_bytes"] = "4096"; auto* c = ev.mutable_capability(); c->set_kind(v1::CAPABILITY_OPERATION_EVENT_KIND_LORA_FAILED); c->set_component(v1::SDK_COMPONENT_LLM); - c->set_model_id("rcli-live-test"); + c->set_model_id("wally-live-test"); send_and_assert(mgr, &state, ev, "lora"); } - rcli::shutdown(); + wally::shutdown(); std::fprintf(stdout, " %d checks, %d failures\n", g_checks, g_failures); return g_failures == 0 ? 0 : 1; #endif // RAC_HAVE_PROTOBUF diff --git a/tests/test_rcli_unit.cpp b/tests/test_wally_unit.cpp similarity index 86% rename from tests/test_rcli_unit.cpp rename to tests/test_wally_unit.cpp index 39b42de..b6b9e8b 100644 --- a/tests/test_rcli_unit.cpp +++ b/tests/test_wally_unit.cpp @@ -1,6 +1,6 @@ /** - * @file test_rcli_unit.cpp - * @brief rcli unit tests — pure helpers, no models, no network. + * @file test_wally_unit.cpp + * @brief wally unit tests — pure helpers, no models, no network. * * Uses the commons TestSuite harness so the Docker rig and ctest drive every * suite the same way (--run-all / --test-). @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,7 @@ #include "rac/infrastructure/model_management/rac_model_registry.h" #include "app.h" +#include "net/loopback_auth.h" #include "catalog/catalog.h" #include "catalog/model_ref.h" #include "commands/bench_metrics.h" @@ -100,7 +102,7 @@ TestResult test_json_escape() { {std::string("ctl\x01", 4), "ctl\\u0001"}, }; for (const Case &c : cases) { - const std::string actual = rcli::out::json_escape(c.in); + const std::string actual = wally::out::json_escape(c.in); if (actual != c.expected) { result.expected = c.expected; result.actual = actual; @@ -115,7 +117,7 @@ TestResult test_json_writer_shape() { TestResult result; result.test_name = "json_writer_shape"; - rcli::out::JsonWriter json; + wally::out::JsonWriter json; json.begin_object() .field("name", "qwen3-0.6b") .field("size", static_cast(640)) @@ -140,6 +142,30 @@ TestResult test_json_writer_shape() { return result; } +TestResult test_json_writer_nan_is_null() { + TestResult result; + result.test_name = "json_writer_nan_is_null"; + + // A NaN/Inf double (e.g. sherpa's unset STT confidence) has no JSON + // literal; %g used to print it verbatim as the bareword `nan`, producing + // invalid JSON on every stt --json call. + wally::out::JsonWriter json; + const double nan = std::numeric_limits::quiet_NaN(); + const double inf = std::numeric_limits::infinity(); + json.begin_object().field("confidence", nan).field("gain", inf); + json.begin_array("scores").value(nan).value(0.5).end_array(); + json.end_object(); + + const std::string expected = R"({"confidence":null,"gain":null,"scores":[null,0.5]})"; + if (json.str() != expected) { + result.expected = expected; + result.actual = json.str(); + return result; + } + result.passed = true; + return result; +} + TestResult test_human_bytes() { TestResult result; result.test_name = "human_bytes"; @@ -155,7 +181,7 @@ TestResult test_human_bytes() { {3ull * 1024 * 1024 * 1024, "3.0 GB"}, }; for (const Case &c : cases) { - const std::string actual = rcli::out::human_bytes(c.in); + const std::string actual = wally::out::human_bytes(c.in); if (actual != c.expected) { result.expected = c.expected; result.actual = actual; @@ -170,10 +196,10 @@ TestResult test_normalize_dir() { TestResult result; result.test_name = "normalize_dir"; - if (rcli::paths::normalize_dir("/a/b/") != "/a/b" || - rcli::paths::normalize_dir("/a/b///") != "/a/b" || - rcli::paths::normalize_dir("/") != "/" || - !rcli::paths::normalize_dir("").empty()) { + if (wally::paths::normalize_dir("/a/b/") != "/a/b" || + wally::paths::normalize_dir("/a/b///") != "/a/b" || + wally::paths::normalize_dir("/") != "/" || + !wally::paths::normalize_dir("").empty()) { result.details = "trailing-slash handling broken"; return result; } @@ -188,12 +214,12 @@ TestResult test_resolve_home_precedence() { { // Flag override wins over env. EnvVar env("RUNANYWHERE_HOME", "/from-env/runanywhere"); - if (rcli::paths::resolve_home("/from-flag/runanywhere/") != + if (wally::paths::resolve_home("/from-flag/runanywhere/") != "/from-flag/runanywhere") { result.details = "flag override should win and be normalized"; return result; } - if (rcli::paths::resolve_home("") != "/from-env/runanywhere") { + if (wally::paths::resolve_home("") != "/from-env/runanywhere") { result.details = "env should win when no flag given"; return result; } @@ -202,15 +228,15 @@ TestResult test_resolve_home_precedence() { // Default: XDG data dir under runanywhere. EnvVar env("RUNANYWHERE_HOME", nullptr); #if defined(_WIN32) - EnvVar local("LOCALAPPDATA", "C:/rcli-local"); - const std::string home = rcli::paths::resolve_home(""); - if (home != "C:/rcli-local/RunAnywhere") { - result.details = "expected C:/rcli-local/RunAnywhere, got " + home; + EnvVar local("LOCALAPPDATA", "C:/wally-local"); + const std::string home = wally::paths::resolve_home(""); + if (home != "C:/wally-local/RunAnywhere") { + result.details = "expected C:/wally-local/RunAnywhere, got " + home; return result; } #else EnvVar xdg("XDG_DATA_HOME", "/xdg-data"); - const std::string home = rcli::paths::resolve_home(""); + const std::string home = wally::paths::resolve_home(""); if (home != "/xdg-data/runanywhere") { result.details = "expected /xdg-data/runanywhere, got " + home; return result; @@ -227,7 +253,7 @@ TestResult test_state_dir() { { EnvVar xdg("XDG_STATE_HOME", "/xdg-state"); - if (rcli::paths::state_dir() != "/xdg-state/runanywhere") { + if (wally::paths::state_dir() != "/xdg-state/runanywhere") { result.details = "XDG_STATE_HOME not honored"; return result; } @@ -240,10 +266,10 @@ TestResult test_state_dir() { // which is why this never surfaced. EnvVar xdg("XDG_STATE_HOME", nullptr); EnvVar home("HOME", nullptr); - EnvVar local("LOCALAPPDATA", "C:\\rcli-local"); - const std::string state = rcli::paths::state_dir(); - if (state != "C:/rcli-local/RunAnywhere/state") { - result.details = "expected C:/rcli-local/RunAnywhere/state, got " + state; + EnvVar local("LOCALAPPDATA", "C:\\wally-local"); + const std::string state = wally::paths::state_dir(); + if (state != "C:/wally-local/RunAnywhere/state") { + result.details = "expected C:/wally-local/RunAnywhere/state, got " + state; return result; } } @@ -257,35 +283,35 @@ TestResult test_catalog_lookup() { result.test_name = "catalog_lookup"; size_t count = 0; - const rcli::catalog::CatalogEntry *entries = rcli::catalog::all(&count); + const wally::catalog::CatalogEntry *entries = wally::catalog::all(&count); if (!entries || count < 10) { result.details = "catalog unexpectedly small"; return result; } - const rcli::catalog::CatalogEntry *by_id = rcli::catalog::find("qwen3-0.6b"); - const rcli::catalog::CatalogEntry *by_alias = rcli::catalog::find("qwen3"); + const wally::catalog::CatalogEntry *by_id = wally::catalog::find("qwen3-0.6b"); + const wally::catalog::CatalogEntry *by_alias = wally::catalog::find("qwen3"); if (!by_id || by_id != by_alias) { result.details = "alias lookup should resolve to the same entry"; return result; } - if (rcli::catalog::find("definitely-not-a-model") != nullptr) { + if (wally::catalog::find("definitely-not-a-model") != nullptr) { result.details = "unknown id should return nullptr"; return result; } - if (rcli::catalog::suggestions("qwen", 3).empty()) { + if (wally::catalog::suggestions("qwen", 3).empty()) { result.details = "expected suggestions for 'qwen'"; return result; } // Multi-file entries (VLM pairs, embeddings) must carry ≥2 required files. - const rcli::catalog::CatalogEntry *vlm = rcli::catalog::find("smolvlm2"); + const wally::catalog::CatalogEntry *vlm = wally::catalog::find("smolvlm2"); if (!vlm || vlm->files == nullptr || vlm->file_count != 2) { result.details = "smolvlm2 should be a two-file artifact"; return result; } - const rcli::catalog::CatalogEntry *mlx_llm = rcli::catalog::find("mlx-qwen3"); + const wally::catalog::CatalogEntry *mlx_llm = wally::catalog::find("mlx-qwen3"); if (!mlx_llm || mlx_llm->framework != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || mlx_llm->format != runanywhere::v1::MODEL_FORMAT_SAFETENSORS || @@ -296,8 +322,8 @@ TestResult test_catalog_lookup() { return result; } - const rcli::catalog::CatalogEntry *maple_gguf = - rcli::catalog::find("maple-preview"); + const wally::catalog::CatalogEntry *maple_gguf = + wally::catalog::find("maple-preview"); if (!maple_gguf || maple_gguf->framework != runanywhere::v1::INFERENCE_FRAMEWORK_LLAMA_CPP || maple_gguf->format != runanywhere::v1::MODEL_FORMAT_GGUF || @@ -307,8 +333,8 @@ TestResult test_catalog_lookup() { return result; } - const rcli::catalog::CatalogEntry *mlx_maple = - rcli::catalog::find("mlx-maple-preview"); + const wally::catalog::CatalogEntry *mlx_maple = + wally::catalog::find("mlx-maple-preview"); if (!mlx_maple || mlx_maple->framework != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || mlx_maple->format != runanywhere::v1::MODEL_FORMAT_SAFETENSORS || @@ -321,7 +347,7 @@ TestResult test_catalog_lookup() { } int64_t mlx_maple_file_total = 0; for (size_t i = 0; i < mlx_maple->file_count; ++i) { - const rcli::catalog::CatalogFile &file = mlx_maple->files[i]; + const wally::catalog::CatalogFile &file = mlx_maple->files[i]; if (file.size_bytes <= 0 || std::string(file.url).find( "/resolve/d0a7314d6bf14c880201b599d7a701cfbc8717e6/") == @@ -336,8 +362,8 @@ TestResult test_catalog_lookup() { return result; } - const rcli::catalog::CatalogEntry *mlx_vlm = - rcli::catalog::find("mlx-qwen2-vl"); + const wally::catalog::CatalogEntry *mlx_vlm = + wally::catalog::find("mlx-qwen2-vl"); if (!mlx_vlm || mlx_vlm->category != runanywhere::v1::MODEL_CATEGORY_MULTIMODAL || mlx_vlm->framework != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || @@ -357,8 +383,8 @@ TestResult test_catalog_lookup() { return result; } - const rcli::catalog::CatalogEntry *mlx_fastvlm = - rcli::catalog::find("mlx-fastvlm"); + const wally::catalog::CatalogEntry *mlx_fastvlm = + wally::catalog::find("mlx-fastvlm"); if (!mlx_fastvlm || mlx_fastvlm->category != runanywhere::v1::MODEL_CATEGORY_MULTIMODAL || mlx_fastvlm->framework != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || @@ -383,8 +409,8 @@ TestResult test_catalog_lookup() { return result; } - const rcli::catalog::CatalogEntry *mlx_embed = - rcli::catalog::find("mlx-qwen3-embed"); + const wally::catalog::CatalogEntry *mlx_embed = + wally::catalog::find("mlx-qwen3-embed"); if (!mlx_embed || mlx_embed->category != runanywhere::v1::MODEL_CATEGORY_EMBEDDING || mlx_embed->framework != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || @@ -410,9 +436,9 @@ TestResult test_catalog_lookup() { }; for (const PortableNvidiaEmbeddingCase &test_case : portable_nvidia_embeddings) { - const rcli::catalog::CatalogEntry *entry = - rcli::catalog::find(test_case.id); - if (!entry || entry != rcli::catalog::find(test_case.alias) || + const wally::catalog::CatalogEntry *entry = + wally::catalog::find(test_case.id); + if (!entry || entry != wally::catalog::find(test_case.alias) || entry->category != runanywhere::v1::MODEL_CATEGORY_EMBEDDING || entry->framework != runanywhere::v1::INFERENCE_FRAMEWORK_LLAMA_CPP || entry->format != runanywhere::v1::MODEL_FORMAT_GGUF || @@ -425,8 +451,8 @@ TestResult test_catalog_lookup() { } } - const rcli::catalog::CatalogEntry *nemotron_nano = - rcli::catalog::find("mlx-nemotron-nano"); + const wally::catalog::CatalogEntry *nemotron_nano = + wally::catalog::find("mlx-nemotron-nano"); if (!nemotron_nano || nemotron_nano->category != runanywhere::v1::MODEL_CATEGORY_LANGUAGE || nemotron_nano->framework != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || @@ -437,8 +463,8 @@ TestResult test_catalog_lookup() { return result; } - const rcli::catalog::CatalogEntry *nemotron_mini = - rcli::catalog::find("mlx-nemotron-mini"); + const wally::catalog::CatalogEntry *nemotron_mini = + wally::catalog::find("mlx-nemotron-mini"); if (!nemotron_mini || nemotron_mini->category != runanywhere::v1::MODEL_CATEGORY_LANGUAGE || nemotron_mini->framework != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || @@ -470,8 +496,8 @@ TestResult test_catalog_lookup() { {"mlx-nemotron-asr", 755758528LL}, }; for (const NvidiaSpeechCase &test_case : nvidia_speech_cases) { - const rcli::catalog::CatalogEntry *entry = - rcli::catalog::find(test_case.alias); + const wally::catalog::CatalogEntry *entry = + wally::catalog::find(test_case.alias); if (!entry || entry->category != runanywhere::v1::MODEL_CATEGORY_SPEECH_RECOGNITION || entry->framework != runanywhere::v1::INFERENCE_FRAMEWORK_MLX || @@ -530,8 +556,8 @@ TestResult test_overlay_catalog() { runanywhere::v1::INFERENCE_FRAMEWORK_QHEXRT}, }; for (const Row &row : rows) { - const rcli::catalog::CatalogEntry *by_alias = rcli::catalog::find(row.id); - const rcli::catalog::CatalogEntry *by_id = rcli::catalog::find(row.alias); + const wally::catalog::CatalogEntry *by_alias = wally::catalog::find(row.id); + const wally::catalog::CatalogEntry *by_id = wally::catalog::find(row.alias); if (!by_alias || by_alias != by_id || by_alias->category != row.category || by_alias->framework != row.framework || by_alias->url == nullptr) { result.details = std::string(row.id) + @@ -594,9 +620,9 @@ TestResult test_nvidia_sherpa_catalog() { }; for (const Case &test_case : cases) { - const rcli::catalog::CatalogEntry *entry = - rcli::catalog::find(test_case.id); - if (!entry || entry != rcli::catalog::find(test_case.alias)) { + const wally::catalog::CatalogEntry *entry = + wally::catalog::find(test_case.id); + if (!entry || entry != wally::catalog::find(test_case.alias)) { result.details = std::string(test_case.id) + " should resolve by exact id and alias"; return result; @@ -617,7 +643,7 @@ TestResult test_nvidia_sherpa_catalog() { test_case.revision + "/"; int64_t manifest_total = 0; for (size_t i = 0; i < test_case.file_count; ++i) { - const rcli::catalog::CatalogFile &actual = entry->files[i]; + const wally::catalog::CatalogFile &actual = entry->files[i]; const ExpectedFile &expected = test_case.files[i]; const std::string expected_url = base_url + expected.filename; if (actual.url == nullptr || actual.filename == nullptr || @@ -638,9 +664,9 @@ TestResult test_nvidia_sherpa_catalog() { } } - const rcli::catalog::CatalogEntry *parakeet_ctc = - rcli::catalog::find("sherpa-nemo-parakeet-ctc-1.1b-int8"); - if (!parakeet_ctc || parakeet_ctc != rcli::catalog::find("parakeet-ctc") || + const wally::catalog::CatalogEntry *parakeet_ctc = + wally::catalog::find("sherpa-nemo-parakeet-ctc-1.1b-int8"); + if (!parakeet_ctc || parakeet_ctc != wally::catalog::find("parakeet-ctc") || parakeet_ctc->category != runanywhere::v1::MODEL_CATEGORY_SPEECH_RECOGNITION || parakeet_ctc->framework != runanywhere::v1::INFERENCE_FRAMEWORK_SHERPA || @@ -657,8 +683,8 @@ TestResult test_nvidia_sherpa_catalog() { "https://huggingface.co/runanywhere/" "sherpa-onnx-nemo-parakeet-ctc-1.1b-int8/resolve/" "48a549f552774db3cd09dd1548f3d1a2b37bc7c5/"; - const rcli::catalog::CatalogFile &model = parakeet_ctc->files[0]; - const rcli::catalog::CatalogFile &tokens = parakeet_ctc->files[1]; + const wally::catalog::CatalogFile &model = parakeet_ctc->files[0]; + const wally::catalog::CatalogFile &tokens = parakeet_ctc->files[1]; if (std::string(model.url) != base_url + "model.int8.onnx" || std::string(model.filename) != "model.int8.onnx" || !model.required || model.size_bytes != 1110014145LL || model.checksum_sha256 == nullptr || @@ -704,7 +730,7 @@ TestResult test_engine_hint_parsing() { runanywhere::v1::InferenceFramework actual = runanywhere::v1::INFERENCE_FRAMEWORK_UNSPECIFIED; std::string error; - if (!rcli::commands::parse_engine_hint(c.in, &actual, &error) || + if (!wally::commands::parse_engine_hint(c.in, &actual, &error) || actual != c.expected) { result.expected = std::to_string(static_cast(c.expected)); result.actual = std::to_string(static_cast(actual)); @@ -716,7 +742,7 @@ TestResult test_engine_hint_parsing() { runanywhere::v1::InferenceFramework actual = runanywhere::v1::INFERENCE_FRAMEWORK_UNSPECIFIED; std::string error; - if (rcli::commands::parse_engine_hint("banana", &actual, &error) || + if (wally::commands::parse_engine_hint("banana", &actual, &error) || error.find("unsupported engine") == std::string::npos) { result.details = "unsupported engine should fail with an actionable error"; return result; @@ -757,7 +783,7 @@ bool get_registered_model(const std::string &id, rac_proto_buffer_init(&found); const rac_result_t rc = rac_model_registry_get_proto_buffer( rac_get_model_registry(), id.c_str(), &found); - const bool parsed = rcli::proto::parse_proto_buffer(&found, out, error); + const bool parsed = wally::proto::parse_proto_buffer(&found, out, error); if (!parsed && error && error->empty()) { *error = "registry get failed rc=" + std::to_string(rc); } @@ -768,7 +794,7 @@ TestResult test_mlx_catalog_registration() { TestResult result; result.test_name = "mlx_catalog_registration"; - const rac_result_t rc = rcli::catalog::register_all(); + const rac_result_t rc = wally::catalog::register_all(); if (rc != RAC_SUCCESS) { result.details = "catalog registration failed rc=" + std::to_string(rc); return result; @@ -1072,7 +1098,7 @@ TestResult test_hf_ref_registration() { for (const Case &c : cases) { runanywhere::v1::RegisterModelFromUrlRequest request; request.set_url(c.in); - const std::string bytes = rcli::proto::serialize(request); + const std::string bytes = wally::proto::serialize(request); rac_proto_buffer_t out; rac_proto_buffer_init(&out); @@ -1080,7 +1106,7 @@ TestResult test_hf_ref_registration() { reinterpret_cast(bytes.data()), bytes.size(), &out); runanywhere::v1::ModelInfo saved; std::string parse_error; - const bool parsed = rc == RAC_SUCCESS && rcli::proto::parse_proto_buffer( + const bool parsed = rc == RAC_SUCCESS && wally::proto::parse_proto_buffer( &out, &saved, &parse_error); if (!parsed) { result.expected = c.expected_download_url; @@ -1108,7 +1134,7 @@ TestResult test_hf_ref_registration() { // Sortformer model). Two inference-free strategies are used: // 1. Pure introspection: configure_app() then query the CLI11 App/Option // model -- never parse, never run a callback. -// 2. Parse-FAILURE paths via rcli::run(): a usage error makes CLI11 throw a +// 2. Parse-FAILURE paths via wally::run(): a usage error makes CLI11 throw a // ParseError inside parse(), before any callback, and src/app.cpp maps // every ParseError to the production exit code 2 (0 ok, 1 runtime, 2 // usage). @@ -1125,7 +1151,7 @@ class TempWavFile { namespace fs = std::filesystem; static int counter = 0; path_ = (fs::temp_directory_path() / - ("rcli_diarize_test_" + std::to_string(++counter) + ".wav")) + ("wally_diarize_test_" + std::to_string(++counter) + ".wav")) .string(); std::ofstream(path_).close(); } @@ -1139,26 +1165,26 @@ class TempWavFile { std::string path_; }; -// Drive the production entry point rcli::run() with an argv vector. run() builds +// Drive the production entry point wally::run() with an argv vector. run() builds // its own App + GlobalOptions, so a usage error returns the true production exit // code (2) without any bootstrap or inference. -int run_rcli(const std::vector &args) { +int run_wally(const std::vector &args) { std::vector mutable_args = args; std::vector argv; argv.reserve(mutable_args.size()); for (std::string &arg : mutable_args) { argv.push_back(arg.data()); } - return rcli::run(static_cast(argv.size()), argv.data()); + return wally::run(static_cast(argv.size()), argv.data()); } TestResult test_diarize_arg_surface() { TestResult result; result.test_name = "diarize_arg_surface"; - rcli::GlobalOptions options; - CLI::App app{"rcli test app"}; - rcli::configure_app(app, options); + wally::GlobalOptions options; + CLI::App app{"wally test app"}; + wally::configure_app(app, options); const CLI::App *cmd = app.get_subcommand_no_throw("diarize"); if (cmd == nullptr) { @@ -1209,7 +1235,7 @@ TestResult test_diarize_missing_model_exit2() { // audio positional satisfied by an existing temp file -> the only failure is // the missing required --model (RequiredError -> ParseError -> exit 2). TempWavFile audio; - const int code = run_rcli({"rcli", "diarize", audio.path()}); + const int code = run_wally({"wally", "diarize", audio.path()}); if (code != 2) { result.expected = "2"; result.actual = std::to_string(code); @@ -1226,7 +1252,7 @@ TestResult test_diarize_missing_audio_exit2() { // --model consumes "x"; the required audio positional is left unsatisfied // (RequiredError -> ParseError -> exit 2). - const int code = run_rcli({"rcli", "diarize", "--model", "x"}); + const int code = run_wally({"wally", "diarize", "--model", "x"}); if (code != 2) { result.expected = "2"; result.actual = std::to_string(code); @@ -1245,8 +1271,8 @@ TestResult test_diarize_audio_not_found_exit2() { // --model is supplied so the sole failure is the audio ->check(ExistingFile) // validator (ValidationError -> ParseError -> exit 2), a distinct path from a // plain RequiredError. - const int code = run_rcli( - {"rcli", "diarize", "/no/such/rcli-diarize-input.wav", "--model", "x"}); + const int code = run_wally( + {"wally", "diarize", "/no/such/wally-diarize-input.wav", "--model", "x"}); if (code != 2) { result.expected = "2"; result.actual = std::to_string(code); @@ -1272,8 +1298,8 @@ TestResult test_diarize_numeric_option_typing_exit2() { const char *numeric_flags[] = {"--threshold", "--min-duration", "--merge-gap"}; for (const char *flag : numeric_flags) { - const int code = run_rcli( - {"rcli", "diarize", audio.path(), "--model", "x", flag, "notanumber"}); + const int code = run_wally( + {"wally", "diarize", audio.path(), "--model", "x", flag, "notanumber"}); if (code != 2) { result.expected = "2"; result.actual = std::to_string(code); @@ -1295,7 +1321,7 @@ TestResult test_diarize_unknown_flag_exit2() { // exit 2. Guards against silently-ignored typos. TempWavFile audio; const int code = - run_rcli({"rcli", "diarize", audio.path(), "--model", "x", "--bogus"}); + run_wally({"wally", "diarize", audio.path(), "--model", "x", "--bogus"}); if (code != 2) { result.expected = "2"; result.actual = std::to_string(code); @@ -1315,7 +1341,7 @@ TestResult test_diarize_unknown_flag_exit2() { // Unique path under the system temp dir; RAII removes it recursively on scope // exit (recursive so it also covers the never-created parent dirs used by the -// unwritable-path case). Mirrors make_temp_dir() in test_rcli_mlx_e2e.cpp. +// unwritable-path case). Mirrors make_temp_dir() in test_wally_mlx_e2e.cpp. std::string unique_temp_path(const std::string &name) { static uint64_t counter = 0; const auto stamp = @@ -1583,20 +1609,20 @@ TestResult test_read_ppm_errors() { }; for (const Case &c : cases) { - TempFile tf("rcli-ppm-err"); + TempFile tf("wally-ppm-err"); if (c.create && !write_bytes(tf.path(), c.bytes)) { result.details = std::string("setup failed for case: ") + c.label; return result; } // Seed `out` with sentinels: a failed read must leave it untouched. - rcli::image::RgbImage out; + wally::image::RgbImage out; out.width = 12345u; out.height = 67890u; out.rgb = {9, 9, 9}; std::string error; - const bool ok = rcli::image::read_ppm(tf.path(), &out, &error); + const bool ok = wally::image::read_ppm(tf.path(), &out, &error); if (ok) { result.details = std::string("expected failure for case: ") + c.label; return result; @@ -1627,14 +1653,14 @@ TestResult test_read_ppm_happy_path() { // stride = width*3, RAC_SEGMENTATION_PIXEL_FORMAT_RGB8). const std::vector pixels = {10, 20, 30, 200, 210, 220}; { - TempFile tf("rcli-ppm-2x1"); + TempFile tf("wally-ppm-2x1"); if (!write_bytes(tf.path(), make_ppm(2, 1, pixels))) { result.details = "setup: cannot write 2x1 ppm"; return result; } - rcli::image::RgbImage out; + wally::image::RgbImage out; std::string error; - if (!rcli::image::read_ppm(tf.path(), &out, &error)) { + if (!wally::image::read_ppm(tf.path(), &out, &error)) { result.details = "read_ppm failed on valid 2x1: " + error; return result; } @@ -1664,14 +1690,14 @@ TestResult test_read_ppm_happy_path() { std::vector file = make_ppm(w, h, pixels2); file.push_back(0xAB); // trailing byte past the payload - TempFile tf("rcli-ppm-4x3"); + TempFile tf("wally-ppm-4x3"); if (!write_bytes(tf.path(), file)) { result.details = "setup: cannot write 4x3 ppm"; return result; } - rcli::image::RgbImage out; + wally::image::RgbImage out; std::string error; - if (!rcli::image::read_ppm(tf.path(), &out, &error)) { + if (!wally::image::read_ppm(tf.path(), &out, &error)) { result.details = "read_ppm failed on valid 4x3: " + error; return result; } @@ -1706,14 +1732,14 @@ TestResult test_read_ppm_header_lexing() { std::vector file(header.begin(), header.end()); file.insert(file.end(), pixels.begin(), pixels.end()); - TempFile tf("rcli-ppm-comments"); + TempFile tf("wally-ppm-comments"); if (!write_bytes(tf.path(), file)) { result.details = "setup: cannot write commented ppm"; return result; } - rcli::image::RgbImage out; + wally::image::RgbImage out; std::string error; - if (!rcli::image::read_ppm(tf.path(), &out, &error)) { + if (!wally::image::read_ppm(tf.path(), &out, &error)) { result.details = "comments/whitespace not tolerated: " + error; return result; } @@ -1730,14 +1756,14 @@ TestResult test_read_ppm_header_lexing() { std::vector file(header.begin(), header.end()); file.insert(file.end(), pixels.begin(), pixels.end()); - TempFile tf("rcli-ppm-space-sep"); + TempFile tf("wally-ppm-space-sep"); if (!write_bytes(tf.path(), file)) { result.details = "setup: cannot write space-separator ppm"; return result; } - rcli::image::RgbImage out; + wally::image::RgbImage out; std::string error; - if (!rcli::image::read_ppm(tf.path(), &out, &error)) { + if (!wally::image::read_ppm(tf.path(), &out, &error)) { result.details = "single-space separator not accepted: " + error; return result; } @@ -1753,14 +1779,14 @@ TestResult test_read_ppm_header_lexing() { std::vector file(header.begin(), header.end()); file.insert(file.end(), pixels.begin(), pixels.end()); - TempFile tf("rcli-ppm-overflow"); + TempFile tf("wally-ppm-overflow"); if (!write_bytes(tf.path(), file)) { result.details = "setup: cannot write overflow ppm"; return result; } - rcli::image::RgbImage out; + wally::image::RgbImage out; std::string error; - if (rcli::image::read_ppm(tf.path(), &out, &error)) { + if (wally::image::read_ppm(tf.path(), &out, &error)) { result.details = "overflowing dimension should be rejected"; return result; } @@ -1796,10 +1822,10 @@ TestResult test_write_png_invalid_args() { }; for (const Case &c : cases) { - TempFile tf("rcli-png-badarg"); + TempFile tf("wally-png-badarg"); std::string error; const bool ok = - rcli::image::write_png(tf.path(), c.data, c.width, c.height, &error); + wally::image::write_png(tf.path(), c.data, c.width, c.height, &error); if (ok) { result.details = std::string("expected failure for case: ") + c.label; return result; @@ -1832,9 +1858,9 @@ TestResult test_write_png_container() { rgba[i] = static_cast(i * 11 + 3); } - TempFile tf("rcli-png-container"); + TempFile tf("wally-png-container"); std::string error; - if (!rcli::image::write_png(tf.path(), rgba.data(), w, h, &error)) { + if (!wally::image::write_png(tf.path(), rgba.data(), w, h, &error)) { result.details = "write_png failed: " + error; return result; } @@ -1910,9 +1936,9 @@ TestResult test_write_png_byte_exact() { rgba[i] = static_cast(i * 13 + 5); } - TempFile tf("rcli-png-exact"); + TempFile tf("wally-png-exact"); std::string error; - if (!rcli::image::write_png(tf.path(), rgba.data(), w, h, &error)) { + if (!wally::image::write_png(tf.path(), rgba.data(), w, h, &error)) { result.details = "write_png failed: " + error; return result; } @@ -1992,9 +2018,9 @@ TestResult test_write_png_multi_block() { rgba[i] = static_cast((i * 31 + 17) & 0xFF); } - TempFile tf("rcli-png-multiblock"); + TempFile tf("wally-png-multiblock"); std::string error; - if (!rcli::image::write_png(tf.path(), rgba.data(), w, h, &error)) { + if (!wally::image::write_png(tf.path(), rgba.data(), w, h, &error)) { result.details = "write_png failed: " + error; return result; } @@ -2052,7 +2078,7 @@ TestResult test_write_png_unwritable_path() { TestResult result; result.test_name = "write_png_unwritable_path"; - TempFile base("rcli-png-nodir"); + TempFile base("wally-png-nodir"); // A path under a directory that was never created -> fopen("wb") fails. const std::string path = (std::filesystem::path(base.path()) / "no_such_subdir" / "x.png") @@ -2060,7 +2086,7 @@ TestResult test_write_png_unwritable_path() { const std::vector rgba(2 * 2 * 4, 0x40); std::string error; - const bool ok = rcli::image::write_png(path, rgba.data(), 2, 2, &error); + const bool ok = wally::image::write_png(path, rgba.data(), 2, 2, &error); if (ok) { result.details = "write_png should fail into a non-existent directory"; return result; @@ -2096,8 +2122,8 @@ TestResult test_bench_metrics_consume_only() { usage->set_prefill_ms(180); usage->set_ttft_ms(210); // must not alias into prompt_eval_ms - rcli::commands::bench_metrics::LlmVlmMetrics m; - if (!rcli::commands::bench_metrics::fill_llm(r, /*measured_e2e_ms=*/9999.0, &m)) { + wally::commands::bench_metrics::LlmVlmMetrics m; + if (!wally::commands::bench_metrics::fill_llm(r, /*measured_e2e_ms=*/9999.0, &m)) { result.details = "fill_llm rejected a valid result"; return result; } @@ -2119,8 +2145,8 @@ TestResult test_bench_metrics_consume_only() { r.mutable_usage()->set_output_tokens(256); r.mutable_usage()->set_ttft_ms(500); // still must not become prefill - rcli::commands::bench_metrics::LlmVlmMetrics m; - if (!rcli::commands::bench_metrics::fill_llm(r, /*measured_e2e_ms=*/20000.0, &m)) { + wally::commands::bench_metrics::LlmVlmMetrics m; + if (!wally::commands::bench_metrics::fill_llm(r, /*measured_e2e_ms=*/20000.0, &m)) { result.details = "fill_llm should accept tokens with missing rates"; return result; } @@ -2140,8 +2166,8 @@ TestResult test_bench_metrics_consume_only() { { runanywhere::v1::LLMGenerationResult r; r.mutable_usage()->set_decode_tokens_per_second(99.0); - rcli::commands::bench_metrics::LlmVlmMetrics m; - if (rcli::commands::bench_metrics::fill_llm(r, 1000.0, &m)) { + wally::commands::bench_metrics::LlmVlmMetrics m; + if (wally::commands::bench_metrics::fill_llm(r, 1000.0, &m)) { result.details = "fill_llm must reject zero output tokens"; return result; } @@ -2156,8 +2182,8 @@ TestResult test_bench_metrics_consume_only() { usage->set_prefill_ms(120); usage->set_ttft_ms(130); - rcli::commands::bench_metrics::LlmVlmMetrics m; - if (!rcli::commands::bench_metrics::fill_vlm(r, /*measured_e2e_ms=*/9999.0, &m)) { + wally::commands::bench_metrics::LlmVlmMetrics m; + if (!wally::commands::bench_metrics::fill_vlm(r, /*measured_e2e_ms=*/9999.0, &m)) { result.details = "fill_vlm rejected a valid result"; return result; } @@ -2178,8 +2204,8 @@ TestResult test_bench_metrics_consume_only() { runanywhere::v1::LLMGenerationResult r; r.mutable_usage()->set_output_tokens(8); r.mutable_usage()->set_prefill_ms(77); - rcli::commands::bench_metrics::LlmVlmMetrics m; - if (!rcli::commands::bench_metrics::fill_llm(r, 100.0, &m) || m.prompt_eval_ms != 77.0) { + wally::commands::bench_metrics::LlmVlmMetrics m; + if (!wally::commands::bench_metrics::fill_llm(r, 100.0, &m) || m.prompt_eval_ms != 77.0) { result.expected = "prefill_ms=77 from TokenUsage"; result.actual = "prefill=" + std::to_string(m.prompt_eval_ms); return result; @@ -2197,7 +2223,7 @@ TestResult test_bench_negative_trials_exit2() { TestResult result; result.test_name = "bench_negative_trials_exit2"; - const int code = run_rcli({"rcli", "bench", "--trials", "-5"}); + const int code = run_wally({"wally", "bench", "--trials", "-5"}); if (code != 2) { result.expected = "2"; result.actual = std::to_string(code); @@ -2208,11 +2234,73 @@ TestResult test_bench_negative_trials_exit2() { return result; } +TestResult test_run_max_tokens_zero_exit2() { + TestResult result; + result.test_name = "run_max_tokens_zero_exit2"; + + const int code = run_wally({"wally", "run", "qwen3-0.6b", "hi", "--max-tokens", "0"}); + if (code != 2) { + result.expected = "2"; + result.actual = std::to_string(code); + result.details = "`run --max-tokens 0` should be a usage error, not an uncapped run"; + return result; + } + result.passed = true; + return result; +} + +TestResult test_run_max_tokens_negative_exit2() { + TestResult result; + result.test_name = "run_max_tokens_negative_exit2"; + + const int code = run_wally({"wally", "run", "qwen3-0.6b", "hi", "--max-tokens", "-5"}); + if (code != 2) { + result.expected = "2"; + result.actual = std::to_string(code); + result.details = "`run --max-tokens -5` should be a usage error"; + return result; + } + result.passed = true; + return result; +} + +TestResult test_rerank_top_n_zero_exit2() { + TestResult result; + result.test_name = "rerank_top_n_zero_exit2"; + + const int code = run_wally( + {"wally", "rerank", "q", "-m", "m", "-d", "doc", "--top-n", "0"}); + if (code != 2) { + result.expected = "2"; + result.actual = std::to_string(code); + result.details = "`rerank --top-n 0` should be a usage error, not every document back"; + return result; + } + result.passed = true; + return result; +} + +TestResult test_rerank_top_n_negative_exit2() { + TestResult result; + result.test_name = "rerank_top_n_negative_exit2"; + + const int code = run_wally( + {"wally", "rerank", "q", "-m", "m", "-d", "doc", "--top-n", "-1"}); + if (code != 2) { + result.expected = "2"; + result.actual = std::to_string(code); + result.details = "`rerank --top-n -1` should be a usage error"; + return result; + } + result.passed = true; + return result; +} + TestResult test_bench_zero_trials_exit2() { TestResult result; result.test_name = "bench_zero_trials_exit2"; - const int code = run_rcli({"rcli", "bench", "-n", "0"}); + const int code = run_wally({"wally", "bench", "-n", "0"}); if (code != 2) { result.expected = "2"; result.actual = std::to_string(code); @@ -2231,9 +2319,9 @@ TestResult test_models_ls_is_primary_name() { TestResult result; result.test_name = "models_ls_is_primary_name"; - rcli::GlobalOptions options; - CLI::App app{"rcli test app"}; - rcli::configure_app(app, options); + wally::GlobalOptions options; + CLI::App app{"wally test app"}; + wally::configure_app(app, options); const CLI::App *ls = app.get_subcommand_no_throw("ls"); if (ls == nullptr) { @@ -2255,12 +2343,39 @@ TestResult test_models_ls_is_primary_name() { return result; } +TestResult test_loopback_token() { + TestResult result; + result.test_name = "loopback_token"; + + const std::string a = wally::net::GenerateLoopbackToken(); + const std::string b = wally::net::GenerateLoopbackToken(); + // 32 bytes of randomness rendered as hex, and two draws do not collide. + if (a.size() != 64 || b.size() != 64 || a == b) { + result.details = "token is not 64 hex chars, or two draws matched"; + return result; + } + for (const char c : a) { + if (std::strchr("0123456789abcdef", c) == nullptr) { + result.details = "token has a non-hex character"; + return result; + } + } + // The constant-time compare still has to be a correct compare. + if (!wally::net::ConstantTimeEquals(a, a) || wally::net::ConstantTimeEquals(a, b) || + wally::net::ConstantTimeEquals(a, a + "x") || wally::net::ConstantTimeEquals("", "x")) { + result.details = "ConstantTimeEquals gave a wrong answer"; + return result; + } + result.passed = true; + return result; +} + TestResult test_model_labels_format() { TestResult result; result.test_name = "model_labels_format"; namespace v1 = runanywhere::v1; - using rcli::commands::model_labels::format; + using wally::commands::model_labels::format; // `models show --json` used to emit format() as a raw enum int; every // value here has to render as the same kind of human string @@ -2289,9 +2404,10 @@ TestResult test_model_labels_format() { } // namespace int main(int argc, char **argv) { - TestSuite suite("rcli_unit"); + TestSuite suite("wally_unit"); suite.add("json_escape", test_json_escape); suite.add("json_writer_shape", test_json_writer_shape); + suite.add("json_writer_nan_is_null", test_json_writer_nan_is_null); suite.add("human_bytes", test_human_bytes); suite.add("normalize_dir", test_normalize_dir); suite.add("resolve_home_precedence", test_resolve_home_precedence); @@ -2318,9 +2434,14 @@ int main(int argc, char **argv) { suite.add("write_png_multi_block", test_write_png_multi_block); suite.add("write_png_unwritable_path", test_write_png_unwritable_path); suite.add("bench_metrics_consume_only", test_bench_metrics_consume_only); + suite.add("run_max_tokens_zero_exit2", test_run_max_tokens_zero_exit2); + suite.add("run_max_tokens_negative_exit2", test_run_max_tokens_negative_exit2); + suite.add("rerank_top_n_zero_exit2", test_rerank_top_n_zero_exit2); + suite.add("rerank_top_n_negative_exit2", test_rerank_top_n_negative_exit2); suite.add("bench_negative_trials_exit2", test_bench_negative_trials_exit2); suite.add("bench_zero_trials_exit2", test_bench_zero_trials_exit2); suite.add("models_ls_is_primary_name", test_models_ls_is_primary_name); + suite.add("loopback_token", test_loopback_token); suite.add("model_labels_format", test_model_labels_format); return suite.run(argc, argv); } diff --git a/versions.toml b/versions.toml new file mode 100644 index 0000000..63de5df --- /dev/null +++ b/versions.toml @@ -0,0 +1,44 @@ +# The one place every version, pin, and runner image in this repo lives. +# CMake reads it directly (cmake/sdk-pin.cmake); the copies that can't read TOML +# at their own step -- workflow refs and runners, the Homebrew formula, the Swift +# package pin, CMake's own floor -- are held to it by scripts/ci/check-versions.py, +# so drift fails CI. Format is flat `key = "value"`, one per line: both the CMake +# and the Python side parse it by regex. Keep it flat. + +[product] +# wally's own release version; git tag is v (auto-tag.yml reads it here). +version = "0.5.3" + +[sdk] +# The published C++ desktop kit this tree builds against; bump the SHAs with it. +kit_version = "0.20.37" +kit_release_tag = "cpp-desktop-v0.20.37" +idl_version = "1.2.0" +idl_schema_sha256 = "571199c430eead0638472fc55355513831aeb2139e6fee5d5cd841f1b01d5a43" +idl_protoc_version = "35.1" +kit_sha256_macos_arm64 = "d0cdcee4217d251742bc9bc0790d3377123b7dda644e5671ae79de42c852b56b" +kit_sha256_windows_x64 = "017a2ff1814d8261444338399d6984d5eb17442bae0da0c7525f942bdd58cca9" +kit_sha256_windows_arm64 = "36372f8e6e404a2cb25f22aae5b372eef64e9f05ca58b985a1c872610c2b3e65" +kit_sha256_linux_x64 = "4fa3c045df3747e72696738200736bacba471f3ee9c2ed641abb514d761fde11" + +[swift] +# The Apple MLX host's Swift SDK: sdk_package_version pins swift/Package.swift, +# sdk_ci_ref is the runanywhere-sdks tag ci.yml/release.yml check out. Both move +# on their own cadence, distinct from the C++ kit above. +sdk_package_version = "0.20.25" +tools_version = "6.0" +sdk_ci_ref = "0.20.31" + +[toolchain] +# CMake's own floor; CMakeLists.txt declares these and check-versions holds it here. +cmake_minimum = "3.24" +cxx_standard = "20" + +[runners] +# GitHub Actions images. macos_mlx needs Xcode 26 for the Swift host; macos_oss +# is the cheaper C++-only OSS build. check-versions fails an untracked runs-on. +macos_mlx = "macos-26" +macos_oss = "macos-14" +windows_x64 = "windows-2022" +windows_arm64 = "windows-11-arm" +linux = "ubuntu-24.04"