AGENTS.mdis the real file.CLAUDE.mdis a committed symlink to it. Editing either name edits the same bytes.bash scripts/ci/check-agents-sync.sh --fixrecreates the link and mirrors.claude/skills/→.agents/skills/. CI fails if the pair drifts.
This repository is the official wally product CLI. The application is a Rust
crate (Cargo.toml); it consumes a packaged C++ desktop kit via CMake's
find_package(RunAnywhere). CMake stays the build entry (cmake -B build && cmake --build build && ctest --test-dir build) and the owner of the kit — discovery,
pins, link closure, runtime staging, the Swift MLX host — and runs cargo for
the application code. 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 -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.
argv / flags / env
-> src/commands/cmd_*.rs thin: parse → bootstrap() → one rac_* → render
-> C++ desktop kit catalog, download, lifecycle, generate, serve
-> engines (in the kit) llama.cpp, Sherpa, ONNX, MLX (Apple host)
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.
- Command modules stay thin. Business rules do not live in command callbacks
(
src/cli/mod.rs's CLI11-compatible builder), Swift, or the REPL. - Proto is the SOT.
crate::io::proto::v1::*(prost) is generated at build time straight from the kit's own.protofiles (build.rs, viaprotox), after checking the kit'sSCHEMA_LOCKhash against the pin inversions.toml. Wally never runsprotoc, and nothing generated is committed. Parserac_*byte buffers withio::proto(ProtoBuffer,parse_proto_buffer) intov1::*. - Typed contracts at every boundary: the CLI parser → proto messages →
rac_*→ stdout. No parallel hand-written enums for values that exist inidl/*.proto. - Structured errors. Machine-readable codes from the ABI; human text on stderr.
Results on stdout.
--jsonprints exactly one document on stdout. - Never log API keys, tokens, or Authorization headers. Status/progress go to stderr.
Protobuf remains the source of truth for the packaged SDK/ABI boundary. For a direct HTTP API owned by RunAnywhere—especially Wally auth, account, billing, catalog, usage, and inference—the source of truth is the service's pinned OpenAPI 3.1 artifact. Add the operation to that contract first, then consume a generated binding (or a mechanically verified thin adapter) from the exact contract hash.
- Every operation has a stable
operationIdand named request, response, error, parameter, and streaming-event schemas. - No command may invent JSON with string-built paths, ad-hoc objects, untyped arrays, duplicated enums, or unchecked response parsing. Closed values are generated enums; variants are discriminated unions; owned objects are closed; IDs and scalar domains carry their contract constraints.
- The transport may move opaque bytes for the SDK, but business code must never treat opaque JSON as a typed result. SSE/OpenAI streaming adapters use generated event/chunk types plus conformance tests.
- Pin the OpenAPI artifact hash beside
SCHEMA_LOCK. Contract, binding, CLI 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 Wally only
transports SDK-owned bytes, protobuf generation and SCHEMA_LOCK satisfy this
rule. When Wally directly owns an HTTP call, the OpenAPI requirement applies.
The console's eleven CLI calls (/auth/cli/{start,poll,refresh,revoke}, /v1/me,
/v1/cli/usage, /v1/cli/usage/requests, /v1/models, /v1/models/catalog,
/v1/requests/{request_id}/cancel, plus createDecisions at
POST /v1/decisions) 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.rs (typed requests and responses,
DO NOT EDIT), which console.rs 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. One field is exempt: the usage export's provider is a label the
CLI only reports, so console.rs lifts a value the binding does not know out
of the body before parsing and carries it as text instead of failing the page.
One field goes the other way: the export's next_cursor is required, and a
defaulted one would read as the last page, so console.rs fails a page that
omits it rather than end the export early. The decisions error's code and
type are exempt the same way as provider: an unknown value is lifted out
before parsing, so the refusal's message still reaches the person.
test_wally_contract and python3 contracts/sync_from_inferenceinfra.py --check fail the build if the header, the pin, and the artifact drift.
The separately vendored contracts/wally-decisions-public-v1.openapi.json is
pinned to its InferenceInfra commit in versions.toml; the same generator
emits src/account/decisions_contract.rs with --operation-id createDecisions.
wally decisions has two transports behind one contract. Cloud posts the
request as above; local (--local, or a -m that resolves to a local
decision artifact) runs it through the kit's rac_decision_* component and
maps the proto result back onto the same DecisionsResponse, so rendering and
--json are transport-independent. The two local rows (clef-flash-9b,
clef-flash-mlx) are ModelCategory::Decision and are the only
non-Language entries the LLM-only catalog cut surfaces (is_llm in
src/catalog/catalog.rs, mirrored in cmd_list.rs's row filter).
To re-vendor from an InferenceInfra checkout (records the source commit on the extract):
python3 contracts/sync_from_inferenceinfra.py --from /path/to/InferenceInfra--check without --from is hermetic. Freshness against InferenceInfra HEAD
is enforced on the InferenceInfra PR (consumer-impact); this repo cannot
read that private source from CI.
A sync rewrites both generated files, so it refuses to start when either one
has uncommitted changes rather than destroying them; commit or stash first, or
pass --force. The same all-or-nothing rule applies to provenance: the
extractor takes --source-commit and --source-branch together or not at all,
because half a stamp writes a pin that cannot pass --check.
Consistency with the SDK is idl/SCHEMA_LOCK, copied into the kit as
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.
RunAnywhere::commons applies google=runanywhere_internal when the kit was
built with namespace isolation. Never find_package(Protobuf) against Homebrew.
Dual grammar: spec namespaces (llm generate, models download) plus the
terminal alias run. Model verbs (list/pull/rm/show/default) live
under models only — no top-level shortcut. One configure_* wires both. See
src/commands/mod.rs.
Do not reintroduce FetchContent of the SDK, a second inference backend tree, or a retired MetalRT / hardcoded catalog.
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.
The four endpoints it calls are not ours to rename: an installed binary
talks to whatever the console deploys, so a field or path change breaks every
copy in the wild. They are POST /auth/cli/start, /auth/cli/poll,
/auth/cli/refresh, and GET /v1/me. The console side has a test that reads
src/account/console.rs directly and fails if the two drift.
Two secrets do different jobs. request_code is public and names the attempt;
poll_secret proves the process collecting the grant is the one that started
it. The console stores only a hash of the second.
Two hosts, and they are not the same deployment. The API is the control plane
and serves all four endpoints above plus /v1/cli/*; the web console is only
the page a person approves the sign-in on.
Both are configure inputs, not source constants. Configure requires:
| Supplies | Runtime override | |
|---|---|---|
| Control plane | WALLY_BAKED_CONSOLE_API_URL |
WALLY_CONSOLE_URL |
| Approval page | WALLY_BAKED_CONSOLE_WEB_ORIGIN (+ optional …_ALT) |
WALLY_CONSOLE_WEB_URL |
Copy .env.example to .env and fill them in, or export them before
cmake -B build. Configure fails without the first two, so a binary can never
ship without a control plane; the values live in the build tree, never in
committed source. cmake/env-loader.cmake reads .env; real environment
variables win. CI takes them from repository variables, and a fork that has
not defined them sets WALLY_CONSOLE_OPTIONAL=1, producing a binary that
resolves no default console until WALLY_CONSOLE_URL is set.
TrustedBrowserOrigins() pairs the two: it trusts the configured approval
origins while talking to the configured API, and for any other API origin it
trusts only that origin. Pointing at a local dev console needs
WALLY_CONSOLE_URL set explicitly, e.g. WALLY_CONSOLE_URL=http://localhost:8080.
A console URL may carry a path: a deployment reached at https://host/prefix
has the load balancer strip the prefix and forward to the control plane behind
it. Every endpoint is appended to whatever is configured, so the prefix follows
the whole flow, and the approval page lives on its own origin, so that needs
WALLY_CONSOLE_WEB_URL set too.
Do not reach for a 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
it as one: it goes in the profile file at 0600 and nowhere else, and it is
never logged.
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. CMake hands
cargo the kit's link line through the file API reply of a configured-but-
never-built probe target (cmake/WallyRust.cmake), so every binary cargo
links carries the same kit closure a CMake executable had.
Two binaries come out of a build. wally-cxx is the plain cargo-built CLI.
wally is the same crate's static library (src/lib.rs) plus the MLX backend
linked by the Swift host, 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
register, so an install puts both together and points a wrapper at them.
- Read environment in one place (
GlobalOptions/bootstrap()). - Safe local default:
--environment development(keyless). Production requires an API key + https. Tests must not need real keys or network. - Secrets stay in GitHub Actions secrets / the user's env, never in source, formula files, or skill docs.
Hermetic unit tests (tests/test_wally_unit.rs, one sub-file per owner under
tests/test_wally_unit/, plus tests/cli_golden.rs replaying tests/golden/
byte-exact): no models, no network, no real keys. ctest --test-dir build runs
cargo test and is the default CI bar. scripts/test/test-ledger.py checks
that every C++ test case named in tests/cpp_case_ledger.txt exists as a
same-named Rust #[test] fn.
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 WALLY_E2E_<MOD> (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
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.
Minimum: install the pinned Rust toolchain (rust-toolchain.toml, held to
versions.toml [toolchain] rust by scripts/ci/check-versions.py) → fetch the
pinned kit (scripts/build/fetch-kit.sh) → configure → build → ctest → smoke.
Fail on pin / schema mismatch. cargo fmt --check, cargo clippy --all-targets -- -D warnings, python3 scripts/test/test-ledger.py and (macOS)
scripts/build/gen-sys-bindings.sh --check gate every PR.
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.
Linux bottles are not a v1 merge blocker. Windows x64 and macOS arm64 are.
The Linux bottle is packaged on every PR and checked twice. scripts/release/check-linux-abi.py
fails it when any ELF in it needs a glibc/libstdc++ symbol version above
versions.toml [linux_abi], or a shared library it neither ships nor is
allowed to take from the system. scripts/test/linux-install-matrix.sh runs
install.sh against it on clean Ubuntu 22.04/24.04 and Debian 12, and expects
Ubuntu 20.04 and Alpine to be refused before download. install.sh keeps its
own MIN_GLIBC and library list; check-versions.py holds them to
[linux_abi].
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=/path/to/cpp-desktop-<os>-<arch>
cmake --build build -j "$(sysctl -n hw.logicalcpu)"
ctest --test-dir build --output-on-failure
# Apple: ./build/wally Windows: ./build/wally.exe
bash scripts/test/e2e.sh ./build/wallyCMake resolves the kit and runs cargo build --release for the crate
(cmake/WallyRust.cmake); ctest runs cargo test. rustup (reading
rust-toolchain.toml) must already have the pinned toolchain installed.
CMake 3.27+ and Ninja are also prerequisites: cmake/WallyRust.cmake picks
the Cargo profile from CMAKE_BUILD_TYPE at configure time, so it rejects
multi-config generators (Visual Studio, Xcode, Ninja Multi-Config) outright,
which is why the build above passes -G Ninja (Unix Makefiles also works on
macOS/Linux, but Windows' default generator, Visual Studio, is multi-config;
CI uses Ninja everywhere).
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 the host
with -DWALLY_APPLE_MLX_HOST=OFF only for a fast loop that skips the Swift
build (WALLY_APPLE_CXX_BINARY=ON then gets the plain cargo binary at
build/wally-cxx).
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.
Canonical tree: .claude/skills/ (Claude Code). Mirror: .agents/skills/
(Cursor / Codex). Edit the canonical tree, then
bash scripts/ci/check-agents-sync.sh --fix. Never hand-edit the mirror.
| Skill | When |
|---|---|
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) |
- Command is parse →
bootstrap()→ onerac_*→ render. - Proto types from the kit; pin matches
SCHEMA_LOCK. - Errors structured; no secrets on the log line.
- Unit tests hermetic; smoke/e2e green on the platforms you touched.
check-agents-sync.shpasses.- Docs name any mock, scaffold, or deferred work honestly (Linux bottle, NeuRT).